Flash loan 3 minimal example (#90)
Signed-off-by: microwavedcola1 <microwavedcola@gmail.com>
This commit is contained in:
parent
6a99eb893b
commit
39284c5705
|
@ -0,0 +1,4 @@
|
|||
RPC_URL=https://mango.devnet.rpcpool.com
|
||||
PAYER_KEYPAIR=~/.config/solana/mango-devnet.json
|
||||
ADMIN_KEYPAIR=~/.config/solana/admin.json
|
||||
MANGO_ACCOUNT_NAME=Account
|
|
@ -0,0 +1,4 @@
|
|||
RPC_URL=https://mango.rpcpool.com/
|
||||
PAYER_KEYPAIR=~/.config/solana/mango-mainnet.json
|
||||
ADMIN_KEYPAIR=~/.config/solana/mango-mainnet.json
|
||||
MANGO_ACCOUNT_NAME=Account
|
|
@ -79,15 +79,15 @@ pub async fn loop_update_index(mango_client: Arc<MangoClient>, token_index: Toke
|
|||
&mango_v4::instruction::UpdateIndex {},
|
||||
),
|
||||
};
|
||||
let mut foo = bank_pubkeys_for_a_token
|
||||
let mut banks = bank_pubkeys_for_a_token
|
||||
.iter()
|
||||
.map(|bank_pubkey| AccountMeta {
|
||||
pubkey: *bank_pubkey,
|
||||
is_signer: false,
|
||||
is_writable: false,
|
||||
is_writable: true,
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
ix.accounts.append(&mut foo);
|
||||
ix.accounts.append(&mut banks);
|
||||
ix
|
||||
})
|
||||
.send();
|
||||
|
|
|
@ -57,6 +57,8 @@ pub fn serum3_close_open_orders(ctx: Context<Serum3CloseOpenOrders>) -> Result<(
|
|||
// close OO
|
||||
//
|
||||
cpi_close_open_orders(ctx.accounts)?;
|
||||
|
||||
// TODO: decrement in_use_count on the base token and quote token
|
||||
account.serum3.deactivate(serum_market.market_index)?;
|
||||
|
||||
Ok(())
|
||||
|
|
|
@ -60,7 +60,7 @@ pub fn new_fixed_order_account_retriever<'a, 'info>(
|
|||
let expected_ais = cm!(active_token_len * 2 // banks + oracles
|
||||
+ active_perp_len // PerpMarkets
|
||||
+ active_serum3_len); // open_orders
|
||||
require!(ais.len() == expected_ais, MangoError::SomeError);
|
||||
require_eq!(ais.len(), expected_ais, MangoError::SomeError);
|
||||
|
||||
Ok(FixedOrderAccountRetriever {
|
||||
ais: ais
|
||||
|
|
|
@ -70,16 +70,16 @@
|
|||
"mangoProgramId": "m43thNJ58XCjL798ZSq6JGAG1BnWskhdq5or6kcnfsD",
|
||||
"banks": [
|
||||
{
|
||||
"name": "USDC",
|
||||
"publicKey": "BYq994HWWuXC1UC7ByTiDLQBsnBQQAzJaLvMCQU3priu"
|
||||
"name": "SOL",
|
||||
"publicKey": "6XDZbWax9imnCEaKokorQtwRY7dGj9HnZNk612ibZu4v"
|
||||
},
|
||||
{
|
||||
"name": "BTC",
|
||||
"publicKey": "96e336hgaPWbbHqPbpPcmr1pbTY1CALtFWkNk2tD4ZmK"
|
||||
"publicKey": "9pJ3V2WvGWtWomiUsRLHvMguoZ9SUCWwZxfvaqdRDq44"
|
||||
},
|
||||
{
|
||||
"name": "SOL",
|
||||
"publicKey": "J3dxEFhJG1maNva8W9u4XzxCR1DQm6qfkgrkY29Ujrh3"
|
||||
"name": "USDC",
|
||||
"publicKey": "41gvXTAYM4xckLYqxPXWwjpr6WM3GZEytM9QZSGKqfjX"
|
||||
}
|
||||
],
|
||||
"stubOracles": [
|
||||
|
@ -89,10 +89,6 @@
|
|||
}
|
||||
],
|
||||
"mintInfos": [
|
||||
{
|
||||
"name": "USDC",
|
||||
"publicKey": "42bXYX5sidUkrijuArv4D11g1aAm9947ht6qUB8Me5Q3"
|
||||
},
|
||||
{
|
||||
"name": "BTC",
|
||||
"publicKey": "4KZkrJQRvrVy8BWmbUKk7YWezMAsSb97ZMHFLUtj6Q5u"
|
||||
|
@ -100,6 +96,10 @@
|
|||
{
|
||||
"name": "SOL",
|
||||
"publicKey": "EvjoLBh7ej78C1yA7jaLcHQfoGx3KkDBnZbrztSZFGmC"
|
||||
},
|
||||
{
|
||||
"name": "USDC",
|
||||
"publicKey": "42bXYX5sidUkrijuArv4D11g1aAm9947ht6qUB8Me5Q3"
|
||||
}
|
||||
],
|
||||
"serum3Markets": [],
|
||||
|
|
|
@ -41,6 +41,12 @@ export class Group {
|
|||
);
|
||||
}
|
||||
|
||||
public findSerum3Market(marketIndex: number): Serum3Market | undefined {
|
||||
return Array.from(this.serum3MarketsMap.values()).find(
|
||||
(serum3Market) => serum3Market.marketIndex === marketIndex,
|
||||
);
|
||||
}
|
||||
|
||||
public async reloadAll(client: MangoClient) {
|
||||
let ids: Id | undefined = undefined;
|
||||
|
||||
|
@ -52,7 +58,7 @@ export class Group {
|
|||
await Promise.all([
|
||||
this.reloadBanks(client, ids),
|
||||
this.reloadMintInfos(client, ids),
|
||||
this.reloadSerum3Markets(client, ids).then,
|
||||
this.reloadSerum3Markets(client, ids),
|
||||
this.reloadPerpMarkets(client, ids),
|
||||
]);
|
||||
// requires reloadSerum3Markets to have finished loading
|
||||
|
@ -62,6 +68,7 @@ export class Group {
|
|||
|
||||
public async reloadBanks(client: MangoClient, ids?: Id) {
|
||||
let banks: Bank[];
|
||||
|
||||
if (ids) {
|
||||
banks = (
|
||||
await client.program.account.bank.fetchMultiple(ids.getBanks())
|
||||
|
|
|
@ -99,37 +99,44 @@ export class MangoAccount {
|
|||
return ta ? ta.uiBorrows(bank) : 0;
|
||||
}
|
||||
|
||||
tokens_active(): TokenPosition[] {
|
||||
tokensActive(): TokenPosition[] {
|
||||
return this.tokens.filter((token) => token.isActive());
|
||||
}
|
||||
|
||||
serum3Active(): Serum3Orders[] {
|
||||
return this.serum3.filter((serum3) => serum3.isActive());
|
||||
}
|
||||
|
||||
perpActive(): PerpPositions[] {
|
||||
return this.perps.filter((perp) => perp.isActive());
|
||||
}
|
||||
|
||||
toString(group?: Group): string {
|
||||
return (
|
||||
'tokens:' +
|
||||
let res = '';
|
||||
res = res + ' name: ' + this.name;
|
||||
|
||||
res =
|
||||
this.tokensActive().length > 0
|
||||
? res +
|
||||
'\n tokens:' +
|
||||
JSON.stringify(
|
||||
this.tokens
|
||||
.filter((token) => token.tokenIndex != TokenPosition.TokenIndexUnset)
|
||||
.map((token) => token.toString(group)),
|
||||
null,
|
||||
4,
|
||||
) +
|
||||
'\nserum:' +
|
||||
JSON.stringify(
|
||||
this.serum3.filter(
|
||||
(serum3) => serum3.marketIndex != Serum3Orders.Serum3MarketIndexUnset,
|
||||
),
|
||||
null,
|
||||
4,
|
||||
) +
|
||||
'\nperps:' +
|
||||
JSON.stringify(
|
||||
this.perps.filter(
|
||||
(perp) => perp.marketIndex != PerpPositions.PerpMarketIndexUnset,
|
||||
),
|
||||
this.tokensActive().map((token) => token.toString(group)),
|
||||
null,
|
||||
4,
|
||||
)
|
||||
);
|
||||
: res + '';
|
||||
|
||||
res =
|
||||
this.serum3Active().length > 0
|
||||
? res + '\n serum:' + JSON.stringify(this.serum3Active(), null, 4)
|
||||
: res + '';
|
||||
|
||||
res =
|
||||
this.perpActive().length > 0
|
||||
? res + '\n perps:' + JSON.stringify(this.perpActive(), null, 4)
|
||||
: res + '';
|
||||
|
||||
return res;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -229,6 +236,10 @@ export class Serum3Orders {
|
|||
public baseTokenIndex: number,
|
||||
public quoteTokenIndex: number,
|
||||
) {}
|
||||
|
||||
public isActive(): boolean {
|
||||
return this.marketIndex !== Serum3Orders.Serum3MarketIndexUnset;
|
||||
}
|
||||
}
|
||||
|
||||
export class Serum3PositionDto {
|
||||
|
@ -264,6 +275,10 @@ export class PerpPositions {
|
|||
public takerBaseLots: number,
|
||||
public takerQuoteLots: number,
|
||||
) {}
|
||||
|
||||
isActive(): boolean {
|
||||
return this.marketIndex != PerpPositions.PerpMarketIndexUnset;
|
||||
}
|
||||
}
|
||||
|
||||
export class PerpPositionDto {
|
||||
|
|
|
@ -51,6 +51,8 @@ import {
|
|||
toU64,
|
||||
} from './utils';
|
||||
|
||||
// TODO: replace ui values with native as input wherever possible
|
||||
// TODO: replace token/market names with token or market indices
|
||||
export class MangoClient {
|
||||
constructor(
|
||||
public program: Program<MangoV4>,
|
||||
|
@ -438,11 +440,13 @@ export class MangoClient {
|
|||
}
|
||||
|
||||
public async closeMangoAccount(
|
||||
group: Group,
|
||||
mangoAccount: MangoAccount,
|
||||
): Promise<TransactionSignature> {
|
||||
return await this.program.methods
|
||||
.closeAccount()
|
||||
.accounts({
|
||||
group: group.publicKey,
|
||||
account: mangoAccount.publicKey,
|
||||
owner: (this.program.provider as AnchorProvider).wallet.publicKey,
|
||||
solDestination: mangoAccount.owner,
|
||||
|
@ -521,6 +525,9 @@ export class MangoClient {
|
|||
.rpc({ skipPreflight: true });
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
public async tokenWithdraw(
|
||||
group: Group,
|
||||
mangoAccount: MangoAccount,
|
||||
|
@ -556,6 +563,41 @@ export class MangoClient {
|
|||
.rpc({ skipPreflight: true });
|
||||
}
|
||||
|
||||
public async tokenWithdraw2(
|
||||
group: Group,
|
||||
mangoAccount: MangoAccount,
|
||||
tokenName: string,
|
||||
nativeAmount: number,
|
||||
allowBorrow: boolean,
|
||||
) {
|
||||
const bank = group.banksMap.get(tokenName)!;
|
||||
|
||||
const tokenAccountPk = await getAssociatedTokenAddress(
|
||||
bank.mint,
|
||||
mangoAccount.owner,
|
||||
);
|
||||
|
||||
const healthRemainingAccounts: PublicKey[] =
|
||||
await this.buildHealthRemainingAccounts(group, mangoAccount, [bank]);
|
||||
|
||||
return await this.program.methods
|
||||
.tokenWithdraw(new BN(nativeAmount), allowBorrow)
|
||||
.accounts({
|
||||
group: group.publicKey,
|
||||
account: mangoAccount.publicKey,
|
||||
bank: bank.publicKey,
|
||||
vault: bank.vault,
|
||||
tokenAccount: tokenAccountPk,
|
||||
})
|
||||
.remainingAccounts(
|
||||
healthRemainingAccounts.map(
|
||||
(pk) =>
|
||||
({ pubkey: pk, isWritable: false, isSigner: false } as AccountMeta),
|
||||
),
|
||||
)
|
||||
.rpc({ skipPreflight: true });
|
||||
}
|
||||
|
||||
// Serum
|
||||
|
||||
public async serum3RegisterMarket(
|
||||
|
@ -961,12 +1003,13 @@ export class MangoClient {
|
|||
payer: (this.program.provider as AnchorProvider).wallet.publicKey,
|
||||
})
|
||||
.preInstructions([
|
||||
// TODO: try to pick up sizes of bookside and eventqueue from IDL, so we can stay in sync with program
|
||||
SystemProgram.createAccount({
|
||||
programId: this.program.programId,
|
||||
space: 8 + 90152,
|
||||
space: 8 + 90136,
|
||||
lamports:
|
||||
await this.program.provider.connection.getMinimumBalanceForRentExemption(
|
||||
90160,
|
||||
90144,
|
||||
),
|
||||
fromPubkey: (this.program.provider as AnchorProvider).wallet
|
||||
.publicKey,
|
||||
|
@ -974,10 +1017,10 @@ export class MangoClient {
|
|||
}),
|
||||
SystemProgram.createAccount({
|
||||
programId: this.program.programId,
|
||||
space: 8 + 90152,
|
||||
space: 8 + 90136,
|
||||
lamports:
|
||||
await this.program.provider.connection.getMinimumBalanceForRentExemption(
|
||||
90160,
|
||||
90144,
|
||||
),
|
||||
fromPubkey: (this.program.provider as AnchorProvider).wallet
|
||||
.publicKey,
|
||||
|
@ -985,10 +1028,10 @@ export class MangoClient {
|
|||
}),
|
||||
SystemProgram.createAccount({
|
||||
programId: this.program.programId,
|
||||
space: 8 + 102424,
|
||||
space: 8 + 102416,
|
||||
lamports:
|
||||
await this.program.provider.connection.getMinimumBalanceForRentExemption(
|
||||
102432,
|
||||
102424,
|
||||
),
|
||||
fromPubkey: (this.program.provider as AnchorProvider).wallet
|
||||
.publicKey,
|
||||
|
@ -1607,7 +1650,7 @@ export class MangoClient {
|
|||
|
||||
/// private
|
||||
|
||||
private async buildHealthRemainingAccounts(
|
||||
public async buildHealthRemainingAccounts(
|
||||
group: Group,
|
||||
mangoAccount: MangoAccount,
|
||||
banks?: Bank[] /** TODO for serum3PlaceOrder we are just ingoring this atm */,
|
||||
|
|
|
@ -0,0 +1,126 @@
|
|||
import { AnchorProvider, Wallet } from '@project-serum/anchor';
|
||||
import { Connection, Keypair } from '@solana/web3.js';
|
||||
import fs from 'fs';
|
||||
import { Serum3Side } from '../accounts/serum3';
|
||||
import { MangoClient } from '../client';
|
||||
import { MANGO_V4_ID } from '../constants';
|
||||
|
||||
// note: either use finalized or expect closing certain things to fail and having to runs scrript multiple times
|
||||
async function main() {
|
||||
const options = AnchorProvider.defaultOptions();
|
||||
|
||||
// note: see note above
|
||||
// options.commitment = 'finalized';
|
||||
|
||||
const connection = new Connection(
|
||||
'https://mango.devnet.rpcpool.com',
|
||||
options,
|
||||
);
|
||||
|
||||
// user
|
||||
const user = Keypair.fromSecretKey(
|
||||
Buffer.from(
|
||||
JSON.parse(fs.readFileSync(process.env.USER_KEYPAIR!, 'utf-8')),
|
||||
),
|
||||
);
|
||||
const userWallet = new Wallet(user);
|
||||
const userProvider = new AnchorProvider(connection, userWallet, options);
|
||||
const client = await MangoClient.connect(
|
||||
userProvider,
|
||||
'devnet',
|
||||
MANGO_V4_ID['devnet'],
|
||||
);
|
||||
console.log(`User ${userWallet.publicKey.toBase58()}`);
|
||||
|
||||
try {
|
||||
// fetch group
|
||||
const admin = Keypair.fromSecretKey(
|
||||
Buffer.from(
|
||||
JSON.parse(fs.readFileSync(process.env.ADMIN_KEYPAIR!, 'utf-8')),
|
||||
),
|
||||
);
|
||||
const group = await client.getGroupForAdmin(admin.publicKey, 0);
|
||||
console.log(`Found group ${group.publicKey.toBase58()}`);
|
||||
|
||||
// fetch account
|
||||
const mangoAccount = (
|
||||
await client.getMangoAccountForOwner(group, user.publicKey)
|
||||
)[0];
|
||||
console.log(`...found mangoAccount ${mangoAccount.publicKey}`);
|
||||
console.log(mangoAccount.toString());
|
||||
|
||||
// close mango account serum3 positions, closing might require cancelling orders and settling
|
||||
for (const serum3Account of mangoAccount.serum3Active()) {
|
||||
let orders = await client.getSerum3Orders(
|
||||
group,
|
||||
group.findSerum3Market(serum3Account.marketIndex).name,
|
||||
);
|
||||
for (const order of orders) {
|
||||
console.log(
|
||||
` - Order orderId ${order.orderId}, ${order.side}, ${order.price}, ${order.size}`,
|
||||
);
|
||||
console.log(` - Cancelling order with ${order.orderId}`);
|
||||
await client.serum3CancelOrder(
|
||||
group,
|
||||
mangoAccount,
|
||||
|
||||
'BTC/USDC',
|
||||
order.side === 'buy' ? Serum3Side.bid : Serum3Side.ask,
|
||||
order.orderId,
|
||||
);
|
||||
}
|
||||
await client.serum3SettleFunds(
|
||||
group,
|
||||
mangoAccount,
|
||||
group.findSerum3Market(serum3Account.marketIndex).name,
|
||||
);
|
||||
await client.serum3CloseOpenOrders(
|
||||
group,
|
||||
mangoAccount,
|
||||
group.findSerum3Market(serum3Account.marketIndex).name,
|
||||
);
|
||||
}
|
||||
|
||||
// we closed a serum account, this changes the health accounts we are passing in for future ixs
|
||||
await mangoAccount.reload(client);
|
||||
|
||||
// withdraw all tokens
|
||||
for (const token of mangoAccount.tokensActive()) {
|
||||
let native = token.native(group.findBank(token.tokenIndex));
|
||||
|
||||
// to avoid rounding issues
|
||||
if (native.toNumber() < 1) {
|
||||
continue;
|
||||
}
|
||||
let nativeFlooredNumber = Math.floor(native.toNumber());
|
||||
console.log(
|
||||
`withdrawing token ${
|
||||
group.findBank(token.tokenIndex).name
|
||||
} native amount ${nativeFlooredNumber} `,
|
||||
);
|
||||
|
||||
await client.tokenWithdraw2(
|
||||
group,
|
||||
mangoAccount,
|
||||
group.findBank(token.tokenIndex).name,
|
||||
nativeFlooredNumber,
|
||||
false,
|
||||
);
|
||||
}
|
||||
|
||||
// reload and print current positions
|
||||
await mangoAccount.reload(client);
|
||||
console.log(`...mangoAccount ${mangoAccount.publicKey}`);
|
||||
console.log(mangoAccount.toString());
|
||||
|
||||
// close account
|
||||
console.log(`Close mango account...`);
|
||||
const res = await client.closeMangoAccount(group, mangoAccount);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
|
||||
process.exit();
|
||||
}
|
||||
|
||||
main();
|
|
@ -86,6 +86,8 @@ async function main() {
|
|||
})),
|
||||
);
|
||||
|
||||
console.log(toDump);
|
||||
|
||||
// adds ids for group in existing ids.json
|
||||
const existingGroup = idsJson.groups.find((group) => group.name == groupName);
|
||||
if (existingGroup) {
|
||||
|
|
|
@ -0,0 +1,118 @@
|
|||
import { AnchorProvider, Wallet } from '@project-serum/anchor';
|
||||
import { Connection, Keypair } from '@solana/web3.js';
|
||||
import fs from 'fs';
|
||||
import { Serum3Side } from '../accounts/serum3';
|
||||
import { MangoClient } from '../client';
|
||||
|
||||
async function main() {
|
||||
const options = AnchorProvider.defaultOptions();
|
||||
const connection = new Connection(process.env.CLUSTER_URL, options);
|
||||
|
||||
// user
|
||||
const user = Keypair.fromSecretKey(
|
||||
Buffer.from(
|
||||
JSON.parse(
|
||||
fs.readFileSync(process.env.MANGO_MAINNET_PAYER_KEYPAIR!, 'utf-8'),
|
||||
),
|
||||
),
|
||||
);
|
||||
const userWallet = new Wallet(user);
|
||||
const userProvider = new AnchorProvider(connection, userWallet, options);
|
||||
const client = await MangoClient.connectForGroupName(
|
||||
userProvider,
|
||||
'mainnet-beta.microwavedcola' /* Use ids json instead of getProgramAccounts */,
|
||||
);
|
||||
console.log(`User ${userWallet.publicKey.toBase58()}`);
|
||||
|
||||
// admin
|
||||
const admin = Keypair.fromSecretKey(
|
||||
Buffer.from(
|
||||
JSON.parse(
|
||||
fs.readFileSync(process.env.MANGO_MAINNET_PAYER_KEYPAIR!, 'utf-8'),
|
||||
),
|
||||
),
|
||||
);
|
||||
console.log(`Admin ${admin.publicKey.toBase58()}`);
|
||||
|
||||
try {
|
||||
// fetch group
|
||||
const group = await client.getGroupForAdmin(admin.publicKey);
|
||||
console.log(`Found group ${group.publicKey.toBase58()}`);
|
||||
|
||||
// account
|
||||
console.log(`Creating mangoaccount...`);
|
||||
const mangoAccount = (
|
||||
await client.getMangoAccountForOwner(group, user.publicKey)
|
||||
)[0];
|
||||
console.log(`...created/found mangoAccount ${mangoAccount.publicKey}`);
|
||||
console.log(mangoAccount.toString(group));
|
||||
|
||||
// cancel serum3 accounts, closing might require cancelling orders and settling
|
||||
for (const serum3Account of mangoAccount.serum3Active()) {
|
||||
let orders = await client.getSerum3Orders(
|
||||
group,
|
||||
group.findSerum3Market(serum3Account.marketIndex).name,
|
||||
);
|
||||
for (const order of orders) {
|
||||
console.log(
|
||||
` - Order orderId ${order.orderId}, ${order.side}, ${order.price}, ${order.size}`,
|
||||
);
|
||||
console.log(` - Cancelling order with ${order.orderId}`);
|
||||
await client.serum3CancelOrder(
|
||||
group,
|
||||
mangoAccount,
|
||||
|
||||
'BTC/USDC',
|
||||
order.side === 'buy' ? Serum3Side.bid : Serum3Side.ask,
|
||||
order.orderId,
|
||||
);
|
||||
}
|
||||
|
||||
await client.serum3SettleFunds(
|
||||
group,
|
||||
mangoAccount,
|
||||
group.findSerum3Market(serum3Account.marketIndex).name,
|
||||
);
|
||||
await client.serum3CloseOpenOrders(
|
||||
group,
|
||||
mangoAccount,
|
||||
group.findSerum3Market(serum3Account.marketIndex).name,
|
||||
);
|
||||
}
|
||||
|
||||
// we closed a serum account, this changes the health accounts we are passing in for future ixs
|
||||
await mangoAccount.reload(client);
|
||||
|
||||
// withdraw all tokens
|
||||
for (const token of mangoAccount.tokensActive()) {
|
||||
const native = token.native(group.findBank(token.tokenIndex));
|
||||
console.log(
|
||||
`token native ${native} ${group.findBank(token.tokenIndex).name}`,
|
||||
);
|
||||
if (native.toNumber() < 1) {
|
||||
continue;
|
||||
}
|
||||
|
||||
await client.tokenWithdraw2(
|
||||
group,
|
||||
mangoAccount,
|
||||
group.findBank(token.tokenIndex).name,
|
||||
token.native(group.findBank(token.tokenIndex)).toNumber(),
|
||||
false,
|
||||
);
|
||||
}
|
||||
|
||||
await mangoAccount.reload(client);
|
||||
console.log(`...mangoAccount ${mangoAccount.publicKey}`);
|
||||
console.log(mangoAccount.toString());
|
||||
|
||||
console.log(`Close mango account...`);
|
||||
const res = await client.closeMangoAccount(group, mangoAccount);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
|
||||
process.exit();
|
||||
}
|
||||
|
||||
main();
|
|
@ -0,0 +1,273 @@
|
|||
import { Jupiter } from '@jup-ag/core';
|
||||
import { AnchorProvider, Wallet } from '@project-serum/anchor';
|
||||
import {
|
||||
AccountMeta,
|
||||
Connection,
|
||||
Keypair,
|
||||
SYSVAR_INSTRUCTIONS_PUBKEY,
|
||||
TransactionInstruction,
|
||||
} from '@solana/web3.js';
|
||||
import BN from 'bn.js';
|
||||
import fs from 'fs';
|
||||
import { QUOTE_DECIMALS } from '../accounts/bank';
|
||||
import { MangoClient } from '../client';
|
||||
import { getAssociatedTokenAddress } from '../utils';
|
||||
|
||||
// NOTE: we assume that ATA for source and target already exist for wallet
|
||||
async function main() {
|
||||
const options = AnchorProvider.defaultOptions();
|
||||
const connection = new Connection(process.env.CLUSTER_URL, options);
|
||||
|
||||
// load user key
|
||||
const user = Keypair.fromSecretKey(
|
||||
Buffer.from(
|
||||
JSON.parse(
|
||||
fs.readFileSync(process.env.MANGO_MAINNET_PAYER_KEYPAIR!, 'utf-8'),
|
||||
),
|
||||
),
|
||||
);
|
||||
const userWallet = new Wallet(user);
|
||||
const userProvider = new AnchorProvider(connection, userWallet, options);
|
||||
const client = await MangoClient.connectForGroupName(
|
||||
userProvider,
|
||||
'mainnet-beta.microwavedcola',
|
||||
);
|
||||
console.log(`User ${userWallet.publicKey.toBase58()}`);
|
||||
|
||||
// load admin key
|
||||
const admin = Keypair.fromSecretKey(
|
||||
Buffer.from(
|
||||
JSON.parse(
|
||||
fs.readFileSync(process.env.MANGO_MAINNET_PAYER_KEYPAIR!, 'utf-8'),
|
||||
),
|
||||
),
|
||||
);
|
||||
console.log(`Admin ${admin.publicKey.toBase58()}`);
|
||||
|
||||
// fetch group
|
||||
const group = await client.getGroupForAdmin(admin.publicKey, 0);
|
||||
console.log(`Found group ${group.publicKey.toBase58()}`);
|
||||
console.log(`start btc bank ${group.banksMap.get('BTC').toString()}`);
|
||||
|
||||
// create + fetch account
|
||||
console.log(`Creating mangoaccount...`);
|
||||
const mangoAccount = await client.getOrCreateMangoAccount(
|
||||
group,
|
||||
user.publicKey,
|
||||
0,
|
||||
'my_mango_account',
|
||||
);
|
||||
console.log(`...created/found mangoAccount ${mangoAccount.publicKey}`);
|
||||
console.log(`start balance \n${mangoAccount.toString(group)}`);
|
||||
|
||||
//
|
||||
// flash loan 3
|
||||
//
|
||||
if (true) {
|
||||
// source of swap
|
||||
const sourceBank = group.banksMap.get('USDC');
|
||||
// target of swap
|
||||
const targetBank = group.banksMap.get('BTC');
|
||||
// 0.2$, at 1BTC=20,000$, 0.2$=0.00001BTC
|
||||
const sourceAmount = 2 * Math.pow(10, QUOTE_DECIMALS - 1);
|
||||
|
||||
console.log(`Flash loaning ${sourceBank.name} to ${targetBank.name}`);
|
||||
|
||||
// jupiter route
|
||||
const jupiter = await Jupiter.load({
|
||||
connection: client.program.provider.connection,
|
||||
cluster: 'mainnet-beta',
|
||||
user: mangoAccount.owner, // or public key
|
||||
// platformFeeAndAccounts: NO_PLATFORM_FEE,
|
||||
routeCacheDuration: 10_000, // Will not refetch data on computeRoutes for up to 10 seconds
|
||||
});
|
||||
const routes = await jupiter.computeRoutes({
|
||||
inputMint: sourceBank.mint, // Mint address of the input token
|
||||
outputMint: targetBank.mint, // Mint address of the output token
|
||||
inputAmount: sourceAmount, // raw input amount of tokens
|
||||
slippage: 5, // The slippage in % terms
|
||||
forceFetch: false, // false is the default value => will use cache if not older than routeCacheDuration
|
||||
});
|
||||
const routesInfosWithoutRaydium = routes.routesInfos.filter((r) => {
|
||||
if (r.marketInfos.length > 1) {
|
||||
for (const mkt of r.marketInfos) {
|
||||
if (mkt.amm.label === 'Raydium' || mkt.amm.label === 'Serum')
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
// loop until we manage first successful swap
|
||||
let res;
|
||||
let i = 0;
|
||||
while (true) {
|
||||
const instructions: TransactionInstruction[] = [];
|
||||
|
||||
// select a route and fetch+build its tx
|
||||
const selectedRoute = routesInfosWithoutRaydium[i];
|
||||
const { transactions } = await jupiter.exchange({
|
||||
routeInfo: selectedRoute,
|
||||
});
|
||||
|
||||
const { setupTransaction, swapTransaction } = transactions;
|
||||
for (const ix of swapTransaction.instructions) {
|
||||
if (
|
||||
ix.programId.toBase58() ===
|
||||
'JUP2jxvXaqu7NQY1GmNF4m1vodw12LVXYxbFL2uJvfo'
|
||||
) {
|
||||
instructions.push(ix);
|
||||
}
|
||||
}
|
||||
|
||||
// run jup setup in a separate tx, ideally this should be packed before flashLoanBegin in same tx,
|
||||
// but it increases chance of flash loan tx to exceed tx size limit
|
||||
if (setupTransaction) {
|
||||
await this.program.provider.sendAndConfirm(setupTransaction);
|
||||
}
|
||||
|
||||
// flash loan start ix - takes a loan for source token,
|
||||
// flash loan end ix - returns increase in all token account's amounts to respective vaults,
|
||||
const healthRemainingAccounts = await client.buildHealthRemainingAccounts(
|
||||
group,
|
||||
mangoAccount,
|
||||
[sourceBank, targetBank], // we would be taking a sol loan potentially
|
||||
);
|
||||
// 1. build flash loan end ix
|
||||
const flashLoadnEndIx = await client.program.methods
|
||||
.flashLoan3End()
|
||||
.accounts({
|
||||
account: mangoAccount.publicKey,
|
||||
owner: (client.program.provider as AnchorProvider).wallet.publicKey,
|
||||
})
|
||||
.remainingAccounts([
|
||||
...healthRemainingAccounts.map(
|
||||
(pk) =>
|
||||
({
|
||||
pubkey: pk,
|
||||
isWritable: false,
|
||||
isSigner: false,
|
||||
} as AccountMeta),
|
||||
),
|
||||
{
|
||||
pubkey: sourceBank.vault,
|
||||
isWritable: true,
|
||||
isSigner: false,
|
||||
} as AccountMeta,
|
||||
{
|
||||
pubkey: targetBank.vault,
|
||||
isWritable: true,
|
||||
isSigner: false,
|
||||
} as AccountMeta,
|
||||
{
|
||||
pubkey: await getAssociatedTokenAddress(
|
||||
sourceBank.mint,
|
||||
mangoAccount.owner,
|
||||
),
|
||||
isWritable: true, // increase in this address amount is transferred back to the sourceBank.vault above in this case whatever is residual of source bank loan
|
||||
isSigner: false,
|
||||
} as AccountMeta,
|
||||
{
|
||||
pubkey: await getAssociatedTokenAddress(
|
||||
targetBank.mint,
|
||||
mangoAccount.owner,
|
||||
),
|
||||
isWritable: true, // increase in this address amount is transferred back to the targetBank.vault above in this case whatever is result of swap
|
||||
isSigner: false,
|
||||
} as AccountMeta,
|
||||
])
|
||||
.instruction();
|
||||
instructions.push(flashLoadnEndIx);
|
||||
// 2. build flash loan start ix, add end ix as a post ix
|
||||
try {
|
||||
res = await client.program.methods
|
||||
.flashLoan3Begin([
|
||||
new BN(sourceAmount),
|
||||
new BN(
|
||||
0,
|
||||
) /* we don't care about borrowing the target amount, this is just a dummy */,
|
||||
])
|
||||
.accounts({
|
||||
group: group.publicKey,
|
||||
// for observing ixs in the entire tx,
|
||||
// e.g. apart from flash loan start and end no other ix should target mango v4 program
|
||||
// e.g. forbid FlashLoan3Begin been called from CPI
|
||||
instructions: SYSVAR_INSTRUCTIONS_PUBKEY,
|
||||
})
|
||||
.remainingAccounts([
|
||||
{
|
||||
pubkey: sourceBank.publicKey,
|
||||
isWritable: true, // metadata for flash loan is updated
|
||||
isSigner: false,
|
||||
} as AccountMeta,
|
||||
{
|
||||
pubkey: targetBank.publicKey,
|
||||
isWritable: true, // this is a dummy, its just done so that we match flash loan start and end ix
|
||||
isSigner: false,
|
||||
} as AccountMeta,
|
||||
{
|
||||
pubkey: sourceBank.vault,
|
||||
isWritable: true,
|
||||
isSigner: false,
|
||||
} as AccountMeta,
|
||||
{
|
||||
pubkey: targetBank.vault,
|
||||
isWritable: true, // this is a dummy, its just done so that we match flash loan start and end ix
|
||||
isSigner: false,
|
||||
} as AccountMeta,
|
||||
{
|
||||
pubkey: await getAssociatedTokenAddress(
|
||||
sourceBank.mint,
|
||||
mangoAccount.owner,
|
||||
),
|
||||
isWritable: true, // token transfer i.e. loan to a desired token account e.g. user's ATA when using a route made for a specific user
|
||||
isSigner: false,
|
||||
} as AccountMeta,
|
||||
{
|
||||
pubkey: await getAssociatedTokenAddress(
|
||||
targetBank.mint,
|
||||
mangoAccount.owner,
|
||||
),
|
||||
isWritable: false, // this is a dummy, its just done so that we match flash loan start and end ix
|
||||
isSigner: false,
|
||||
} as AccountMeta,
|
||||
])
|
||||
.postInstructions(instructions)
|
||||
.rpc();
|
||||
|
||||
// break when success
|
||||
break;
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
if (
|
||||
(error.toString() as string).includes('Transaction too large:') ||
|
||||
(error.toString() as string).includes(
|
||||
'encoding overruns Uint8Array',
|
||||
) ||
|
||||
(error.toString() as string).includes(
|
||||
'The value of "offset" is out of range. It must be >= 0 and <= 1231. Received 1232',
|
||||
) ||
|
||||
(error.toString() as string).includes(
|
||||
'The value of "value" is out of range. It must be >= 0 and <= 255. Received',
|
||||
) ||
|
||||
i > 10
|
||||
) {
|
||||
console.log(`route ${i} was bad, trying next one...`);
|
||||
i++;
|
||||
} else {
|
||||
throw error; // let others bubble up
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`success tx - https://explorer.solana.com/tx/${res}`);
|
||||
|
||||
group.reloadBanks(client);
|
||||
console.log(`end btc bank ${group.banksMap.get('BTC').toString()}`);
|
||||
|
||||
await mangoAccount.reload(client);
|
||||
console.log(`end balance \n${mangoAccount.toString(group)}`);
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
Loading…
Reference in New Issue