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 {},
|
&mango_v4::instruction::UpdateIndex {},
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
let mut foo = bank_pubkeys_for_a_token
|
let mut banks = bank_pubkeys_for_a_token
|
||||||
.iter()
|
.iter()
|
||||||
.map(|bank_pubkey| AccountMeta {
|
.map(|bank_pubkey| AccountMeta {
|
||||||
pubkey: *bank_pubkey,
|
pubkey: *bank_pubkey,
|
||||||
is_signer: false,
|
is_signer: false,
|
||||||
is_writable: false,
|
is_writable: true,
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
ix.accounts.append(&mut foo);
|
ix.accounts.append(&mut banks);
|
||||||
ix
|
ix
|
||||||
})
|
})
|
||||||
.send();
|
.send();
|
||||||
|
|
|
@ -57,6 +57,8 @@ pub fn serum3_close_open_orders(ctx: Context<Serum3CloseOpenOrders>) -> Result<(
|
||||||
// close OO
|
// close OO
|
||||||
//
|
//
|
||||||
cpi_close_open_orders(ctx.accounts)?;
|
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)?;
|
account.serum3.deactivate(serum_market.market_index)?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
|
@ -60,7 +60,7 @@ pub fn new_fixed_order_account_retriever<'a, 'info>(
|
||||||
let expected_ais = cm!(active_token_len * 2 // banks + oracles
|
let expected_ais = cm!(active_token_len * 2 // banks + oracles
|
||||||
+ active_perp_len // PerpMarkets
|
+ active_perp_len // PerpMarkets
|
||||||
+ active_serum3_len); // open_orders
|
+ active_serum3_len); // open_orders
|
||||||
require!(ais.len() == expected_ais, MangoError::SomeError);
|
require_eq!(ais.len(), expected_ais, MangoError::SomeError);
|
||||||
|
|
||||||
Ok(FixedOrderAccountRetriever {
|
Ok(FixedOrderAccountRetriever {
|
||||||
ais: ais
|
ais: ais
|
||||||
|
|
|
@ -70,16 +70,16 @@
|
||||||
"mangoProgramId": "m43thNJ58XCjL798ZSq6JGAG1BnWskhdq5or6kcnfsD",
|
"mangoProgramId": "m43thNJ58XCjL798ZSq6JGAG1BnWskhdq5or6kcnfsD",
|
||||||
"banks": [
|
"banks": [
|
||||||
{
|
{
|
||||||
"name": "USDC",
|
"name": "SOL",
|
||||||
"publicKey": "BYq994HWWuXC1UC7ByTiDLQBsnBQQAzJaLvMCQU3priu"
|
"publicKey": "6XDZbWax9imnCEaKokorQtwRY7dGj9HnZNk612ibZu4v"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "BTC",
|
"name": "BTC",
|
||||||
"publicKey": "96e336hgaPWbbHqPbpPcmr1pbTY1CALtFWkNk2tD4ZmK"
|
"publicKey": "9pJ3V2WvGWtWomiUsRLHvMguoZ9SUCWwZxfvaqdRDq44"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "SOL",
|
"name": "USDC",
|
||||||
"publicKey": "J3dxEFhJG1maNva8W9u4XzxCR1DQm6qfkgrkY29Ujrh3"
|
"publicKey": "41gvXTAYM4xckLYqxPXWwjpr6WM3GZEytM9QZSGKqfjX"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"stubOracles": [
|
"stubOracles": [
|
||||||
|
@ -89,10 +89,6 @@
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"mintInfos": [
|
"mintInfos": [
|
||||||
{
|
|
||||||
"name": "USDC",
|
|
||||||
"publicKey": "42bXYX5sidUkrijuArv4D11g1aAm9947ht6qUB8Me5Q3"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"name": "BTC",
|
"name": "BTC",
|
||||||
"publicKey": "4KZkrJQRvrVy8BWmbUKk7YWezMAsSb97ZMHFLUtj6Q5u"
|
"publicKey": "4KZkrJQRvrVy8BWmbUKk7YWezMAsSb97ZMHFLUtj6Q5u"
|
||||||
|
@ -100,10 +96,14 @@
|
||||||
{
|
{
|
||||||
"name": "SOL",
|
"name": "SOL",
|
||||||
"publicKey": "EvjoLBh7ej78C1yA7jaLcHQfoGx3KkDBnZbrztSZFGmC"
|
"publicKey": "EvjoLBh7ej78C1yA7jaLcHQfoGx3KkDBnZbrztSZFGmC"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "USDC",
|
||||||
|
"publicKey": "42bXYX5sidUkrijuArv4D11g1aAm9947ht6qUB8Me5Q3"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"serum3Markets": [],
|
"serum3Markets": [],
|
||||||
"perpMarkets": []
|
"perpMarkets": []
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
@ -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) {
|
public async reloadAll(client: MangoClient) {
|
||||||
let ids: Id | undefined = undefined;
|
let ids: Id | undefined = undefined;
|
||||||
|
|
||||||
|
@ -52,7 +58,7 @@ export class Group {
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
this.reloadBanks(client, ids),
|
this.reloadBanks(client, ids),
|
||||||
this.reloadMintInfos(client, ids),
|
this.reloadMintInfos(client, ids),
|
||||||
this.reloadSerum3Markets(client, ids).then,
|
this.reloadSerum3Markets(client, ids),
|
||||||
this.reloadPerpMarkets(client, ids),
|
this.reloadPerpMarkets(client, ids),
|
||||||
]);
|
]);
|
||||||
// requires reloadSerum3Markets to have finished loading
|
// requires reloadSerum3Markets to have finished loading
|
||||||
|
@ -62,6 +68,7 @@ export class Group {
|
||||||
|
|
||||||
public async reloadBanks(client: MangoClient, ids?: Id) {
|
public async reloadBanks(client: MangoClient, ids?: Id) {
|
||||||
let banks: Bank[];
|
let banks: Bank[];
|
||||||
|
|
||||||
if (ids) {
|
if (ids) {
|
||||||
banks = (
|
banks = (
|
||||||
await client.program.account.bank.fetchMultiple(ids.getBanks())
|
await client.program.account.bank.fetchMultiple(ids.getBanks())
|
||||||
|
|
|
@ -99,37 +99,44 @@ export class MangoAccount {
|
||||||
return ta ? ta.uiBorrows(bank) : 0;
|
return ta ? ta.uiBorrows(bank) : 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
tokens_active(): TokenPosition[] {
|
tokensActive(): TokenPosition[] {
|
||||||
return this.tokens.filter((token) => token.isActive());
|
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 {
|
toString(group?: Group): string {
|
||||||
return (
|
let res = '';
|
||||||
'tokens:' +
|
res = res + ' name: ' + this.name;
|
||||||
JSON.stringify(
|
|
||||||
this.tokens
|
res =
|
||||||
.filter((token) => token.tokenIndex != TokenPosition.TokenIndexUnset)
|
this.tokensActive().length > 0
|
||||||
.map((token) => token.toString(group)),
|
? res +
|
||||||
null,
|
'\n tokens:' +
|
||||||
4,
|
JSON.stringify(
|
||||||
) +
|
this.tokensActive().map((token) => token.toString(group)),
|
||||||
'\nserum:' +
|
null,
|
||||||
JSON.stringify(
|
4,
|
||||||
this.serum3.filter(
|
)
|
||||||
(serum3) => serum3.marketIndex != Serum3Orders.Serum3MarketIndexUnset,
|
: res + '';
|
||||||
),
|
|
||||||
null,
|
res =
|
||||||
4,
|
this.serum3Active().length > 0
|
||||||
) +
|
? res + '\n serum:' + JSON.stringify(this.serum3Active(), null, 4)
|
||||||
'\nperps:' +
|
: res + '';
|
||||||
JSON.stringify(
|
|
||||||
this.perps.filter(
|
res =
|
||||||
(perp) => perp.marketIndex != PerpPositions.PerpMarketIndexUnset,
|
this.perpActive().length > 0
|
||||||
),
|
? res + '\n perps:' + JSON.stringify(this.perpActive(), null, 4)
|
||||||
null,
|
: res + '';
|
||||||
4,
|
|
||||||
)
|
return res;
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -229,6 +236,10 @@ export class Serum3Orders {
|
||||||
public baseTokenIndex: number,
|
public baseTokenIndex: number,
|
||||||
public quoteTokenIndex: number,
|
public quoteTokenIndex: number,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
|
public isActive(): boolean {
|
||||||
|
return this.marketIndex !== Serum3Orders.Serum3MarketIndexUnset;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Serum3PositionDto {
|
export class Serum3PositionDto {
|
||||||
|
@ -264,6 +275,10 @@ export class PerpPositions {
|
||||||
public takerBaseLots: number,
|
public takerBaseLots: number,
|
||||||
public takerQuoteLots: number,
|
public takerQuoteLots: number,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
|
isActive(): boolean {
|
||||||
|
return this.marketIndex != PerpPositions.PerpMarketIndexUnset;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class PerpPositionDto {
|
export class PerpPositionDto {
|
||||||
|
|
|
@ -51,6 +51,8 @@ import {
|
||||||
toU64,
|
toU64,
|
||||||
} from './utils';
|
} 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 {
|
export class MangoClient {
|
||||||
constructor(
|
constructor(
|
||||||
public program: Program<MangoV4>,
|
public program: Program<MangoV4>,
|
||||||
|
@ -438,11 +440,13 @@ export class MangoClient {
|
||||||
}
|
}
|
||||||
|
|
||||||
public async closeMangoAccount(
|
public async closeMangoAccount(
|
||||||
|
group: Group,
|
||||||
mangoAccount: MangoAccount,
|
mangoAccount: MangoAccount,
|
||||||
): Promise<TransactionSignature> {
|
): Promise<TransactionSignature> {
|
||||||
return await this.program.methods
|
return await this.program.methods
|
||||||
.closeAccount()
|
.closeAccount()
|
||||||
.accounts({
|
.accounts({
|
||||||
|
group: group.publicKey,
|
||||||
account: mangoAccount.publicKey,
|
account: mangoAccount.publicKey,
|
||||||
owner: (this.program.provider as AnchorProvider).wallet.publicKey,
|
owner: (this.program.provider as AnchorProvider).wallet.publicKey,
|
||||||
solDestination: mangoAccount.owner,
|
solDestination: mangoAccount.owner,
|
||||||
|
@ -521,6 +525,9 @@ export class MangoClient {
|
||||||
.rpc({ skipPreflight: true });
|
.rpc({ skipPreflight: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated
|
||||||
|
*/
|
||||||
public async tokenWithdraw(
|
public async tokenWithdraw(
|
||||||
group: Group,
|
group: Group,
|
||||||
mangoAccount: MangoAccount,
|
mangoAccount: MangoAccount,
|
||||||
|
@ -556,6 +563,41 @@ export class MangoClient {
|
||||||
.rpc({ skipPreflight: true });
|
.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
|
// Serum
|
||||||
|
|
||||||
public async serum3RegisterMarket(
|
public async serum3RegisterMarket(
|
||||||
|
@ -961,12 +1003,13 @@ export class MangoClient {
|
||||||
payer: (this.program.provider as AnchorProvider).wallet.publicKey,
|
payer: (this.program.provider as AnchorProvider).wallet.publicKey,
|
||||||
})
|
})
|
||||||
.preInstructions([
|
.preInstructions([
|
||||||
|
// TODO: try to pick up sizes of bookside and eventqueue from IDL, so we can stay in sync with program
|
||||||
SystemProgram.createAccount({
|
SystemProgram.createAccount({
|
||||||
programId: this.program.programId,
|
programId: this.program.programId,
|
||||||
space: 8 + 90152,
|
space: 8 + 90136,
|
||||||
lamports:
|
lamports:
|
||||||
await this.program.provider.connection.getMinimumBalanceForRentExemption(
|
await this.program.provider.connection.getMinimumBalanceForRentExemption(
|
||||||
90160,
|
90144,
|
||||||
),
|
),
|
||||||
fromPubkey: (this.program.provider as AnchorProvider).wallet
|
fromPubkey: (this.program.provider as AnchorProvider).wallet
|
||||||
.publicKey,
|
.publicKey,
|
||||||
|
@ -974,10 +1017,10 @@ export class MangoClient {
|
||||||
}),
|
}),
|
||||||
SystemProgram.createAccount({
|
SystemProgram.createAccount({
|
||||||
programId: this.program.programId,
|
programId: this.program.programId,
|
||||||
space: 8 + 90152,
|
space: 8 + 90136,
|
||||||
lamports:
|
lamports:
|
||||||
await this.program.provider.connection.getMinimumBalanceForRentExemption(
|
await this.program.provider.connection.getMinimumBalanceForRentExemption(
|
||||||
90160,
|
90144,
|
||||||
),
|
),
|
||||||
fromPubkey: (this.program.provider as AnchorProvider).wallet
|
fromPubkey: (this.program.provider as AnchorProvider).wallet
|
||||||
.publicKey,
|
.publicKey,
|
||||||
|
@ -985,10 +1028,10 @@ export class MangoClient {
|
||||||
}),
|
}),
|
||||||
SystemProgram.createAccount({
|
SystemProgram.createAccount({
|
||||||
programId: this.program.programId,
|
programId: this.program.programId,
|
||||||
space: 8 + 102424,
|
space: 8 + 102416,
|
||||||
lamports:
|
lamports:
|
||||||
await this.program.provider.connection.getMinimumBalanceForRentExemption(
|
await this.program.provider.connection.getMinimumBalanceForRentExemption(
|
||||||
102432,
|
102424,
|
||||||
),
|
),
|
||||||
fromPubkey: (this.program.provider as AnchorProvider).wallet
|
fromPubkey: (this.program.provider as AnchorProvider).wallet
|
||||||
.publicKey,
|
.publicKey,
|
||||||
|
@ -1607,7 +1650,7 @@ export class MangoClient {
|
||||||
|
|
||||||
/// private
|
/// private
|
||||||
|
|
||||||
private async buildHealthRemainingAccounts(
|
public async buildHealthRemainingAccounts(
|
||||||
group: Group,
|
group: Group,
|
||||||
mangoAccount: MangoAccount,
|
mangoAccount: MangoAccount,
|
||||||
banks?: Bank[] /** TODO for serum3PlaceOrder we are just ingoring this atm */,
|
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
|
// adds ids for group in existing ids.json
|
||||||
const existingGroup = idsJson.groups.find((group) => group.name == groupName);
|
const existingGroup = idsJson.groups.find((group) => group.name == groupName);
|
||||||
if (existingGroup) {
|
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