Merge branch 'ts/orca-margin-trade' into dev

This commit is contained in:
tjs 2022-05-31 21:39:55 -04:00
commit 1ae00aed0a
11 changed files with 1338 additions and 291 deletions

View File

@ -10,6 +10,7 @@
"clean": "rm -rf dist",
"example1-user": "ts-node ts/client/src/scripts/example1-user.ts",
"example1-admin": "ts-node ts/client/src/scripts/example1-admin.ts",
"scratch": "ts-node ts/client/src/scripts/scratch.ts",
"format": "prettier --check .",
"lint": "eslint . --ext ts --ext tsx --ext js --quiet",
"type-check": "tsc --pretty --noEmit",
@ -25,6 +26,7 @@
],
"devDependencies": {
"@tsconfig/recommended": "^1.0.1",
"@types/bs58": "^4.0.1",
"@types/chai": "^4.3.0",
"@types/mocha": "^9.1.0",
"@types/node": "^14.14.37",
@ -40,9 +42,6 @@
"typedoc": "^0.22.5",
"typescript": "^4.4.4"
},
"resolutions": {
"@types/bn.js": "^4.11.6"
},
"publishConfig": {
"access": "public"
},
@ -51,10 +50,10 @@
"trailingComma": "all"
},
"dependencies": {
"@orca-so/sdk": "^1.2.24",
"@project-serum/anchor": "^0.24.2",
"@project-serum/serum": "^0.13.65",
"@solana/spl-token": "^0.2.0",
"@types/bs58": "^4.0.1",
"@solana/spl-token": "~0.1.8",
"big.js": "^6.1.1",
"bs58": "^5.0.0"
},

View File

@ -10,6 +10,7 @@ export class Bank {
public depositIndex: I80F48;
public borrowIndex: I80F48;
public indexedTotalDeposits: I80F48;
public indexedTotalBorrows: I80F48;
static from(
publicKey: PublicKey,
@ -106,6 +107,7 @@ export class Bank {
this.depositIndex = I80F48.from(depositIndex);
this.borrowIndex = I80F48.from(borrowIndex);
this.indexedTotalDeposits = I80F48.from(indexedTotalDeposits);
this.indexedTotalBorrows = I80F48.from(indexedTotalBorrows);
}
toString(): string {

View File

@ -1,7 +1,13 @@
import { AnchorProvider, BN, Program, Provider } from '@project-serum/anchor';
import { getFeeRates, getFeeTier, Market } from '@project-serum/serum';
import { Order } from '@project-serum/serum/lib/market';
import * as spl from '@solana/spl-token';
import {
closeAccount,
initializeAccount,
WRAPPED_SOL_MINT,
} from '@project-serum/serum/lib/token-instructions';
import { TokenSwap } from '@solana/spl-token-swap';
import { TOKEN_PROGRAM_ID, u64 } from '@solana/spl-token';
import {
AccountMeta,
Keypair,
@ -10,8 +16,14 @@ import {
SystemProgram,
SYSVAR_RENT_PUBKEY,
TransactionSignature,
TransactionInstruction,
LAMPORTS_PER_SOL,
Signer,
} from '@solana/web3.js';
import bs58 from 'bs58';
import { ORCA_TOKEN_SWAP_ID_DEVNET } from '@orca-so/sdk';
import { orcaDevnetPoolConfigs } from '@orca-so/sdk/dist/constants/devnet/pools';
import { OrcaPoolConfig as OrcaDevnetPoolConfig } from '@orca-so/sdk/dist/public/devnet/pools';
import { Bank, getMintInfoForTokenIndex } from './accounts/bank';
import { Group } from './accounts/group';
import { I80F48 } from './accounts/I80F48';
@ -25,6 +37,9 @@ import {
Serum3Side,
} from './accounts/serum3';
import { IDL, MangoV4 } from './mango_v4';
import { getAssociatedTokenAddress, toNativeDecimals, toU64 } from './utils';
import { getTokenDecimals } from './constants/tokens';
import { MarginTradeWithdraw } from './types';
export const MANGO_V4_ID = new PublicKey(
'm43thNJ58XCjL798ZSq6JGAG1BnWskhdq5or6kcnfsD',
@ -285,22 +300,56 @@ export class MangoClient {
) {
const bank = group.banksMap.get(tokenName)!;
const tokenAccountPk = await spl.getAssociatedTokenAddress(
const tokenAccountPk = await getAssociatedTokenAddress(
bank.mint,
mangoAccount.owner,
);
let wrappedSolAccount: Keypair | undefined;
let preInstructions: TransactionInstruction[] = [];
let postInstructions: TransactionInstruction[] = [];
let additionalSigners: Signer[] = [];
if (bank.mint.equals(WRAPPED_SOL_MINT)) {
wrappedSolAccount = new Keypair();
const lamports = Math.round(amount * LAMPORTS_PER_SOL) + 1e7;
preInstructions = [
SystemProgram.createAccount({
fromPubkey: mangoAccount.owner,
newAccountPubkey: wrappedSolAccount.publicKey,
lamports,
space: 165,
programId: TOKEN_PROGRAM_ID,
}),
initializeAccount({
account: wrappedSolAccount.publicKey,
mint: WRAPPED_SOL_MINT,
owner: mangoAccount.owner,
}),
];
postInstructions = [
closeAccount({
source: wrappedSolAccount.publicKey,
destination: mangoAccount.owner,
owner: mangoAccount.owner,
}),
];
additionalSigners.push(wrappedSolAccount);
}
const healthRemainingAccounts: PublicKey[] =
await this.buildHealthRemainingAccounts(group, mangoAccount, bank);
await this.buildHealthRemainingAccounts(group, mangoAccount, [bank]);
const tokenDecimals = getTokenDecimals(tokenName);
return await this.program.methods
.deposit(new BN(amount))
.deposit(toNativeDecimals(amount, tokenDecimals))
.accounts({
group: group.publicKey,
account: mangoAccount.publicKey,
bank: bank.publicKey,
vault: bank.vault,
tokenAccount: tokenAccountPk,
tokenAccount: wrappedSolAccount?.publicKey ?? tokenAccountPk,
tokenAuthority: (this.program.provider as AnchorProvider).wallet
.publicKey,
})
@ -310,7 +359,10 @@ export class MangoClient {
({ pubkey: pk, isWritable: false, isSigner: false } as AccountMeta),
),
)
.rpc();
.preInstructions(preInstructions)
.postInstructions(postInstructions)
.signers(additionalSigners)
.rpc({ skipPreflight: true });
}
public async withdraw(
@ -322,13 +374,13 @@ export class MangoClient {
) {
const bank = group.banksMap.get(tokenName)!;
const tokenAccountPk = await spl.getAssociatedTokenAddress(
const tokenAccountPk = await getAssociatedTokenAddress(
bank.mint,
mangoAccount.owner,
);
const healthRemainingAccounts: PublicKey[] =
await this.buildHealthRemainingAccounts(group, mangoAccount, bank);
await this.buildHealthRemainingAccounts(group, mangoAccount, [bank]);
return await this.program.methods
.withdraw(new BN(amount), allowBorrow)
@ -820,6 +872,81 @@ export class MangoClient {
.rpc();
}
/// margin trade (orca)
public async marginTrade({
group,
mangoAccount,
inputToken,
amountIn,
outputToken,
minimumAmountOut,
}: {
group: Group;
mangoAccount: MangoAccount;
inputToken: string;
amountIn: number;
outputToken: string;
minimumAmountOut: number;
}): Promise<TransactionSignature> {
const inputBank = group.banksMap.get(inputToken);
const outputBank = group.banksMap.get(outputToken);
if (!inputBank || !outputBank) throw new Error('Invalid token');
const healthRemainingAccounts: PublicKey[] =
await this.buildHealthRemainingAccounts(group, mangoAccount, [
inputBank,
outputBank,
]);
const parsedHealthAccounts = healthRemainingAccounts.map(
(pk) =>
({
pubkey: pk,
isWritable:
pk.equals(inputBank.publicKey) || pk.equals(outputBank.publicKey)
? true
: false,
isSigner: false,
} as AccountMeta),
);
const targetProgramId = ORCA_TOKEN_SWAP_ID_DEVNET;
const { instruction, signers } = await this.buildOrcaInstruction(
targetProgramId,
inputBank,
outputBank,
toU64(amountIn, 9),
toU64(minimumAmountOut, 6),
);
const targetRemainingAccounts = instruction.keys;
const withdraws: MarginTradeWithdraw[] = [
{ index: 3, amount: toU64(amountIn, 9) },
];
const cpiData = instruction.data;
return await this.program.methods
.marginTrade(new BN(parsedHealthAccounts.length), withdraws, cpiData)
.accounts({
group: group.publicKey,
account: mangoAccount.publicKey,
owner: (this.program.provider as AnchorProvider).wallet.publicKey,
})
.remainingAccounts([
...parsedHealthAccounts,
{
pubkey: targetProgramId,
isWritable: false,
isSigner: false,
} as AccountMeta,
...targetRemainingAccounts,
])
.signers(signers)
.rpc({ skipPreflight: true });
}
/// static
static connect(provider?: Provider, devnet?: boolean): MangoClient {
@ -839,7 +966,7 @@ export class MangoClient {
private async buildHealthRemainingAccounts(
group: Group,
mangoAccount: MangoAccount,
bank?: Bank /** TODO for serum3PlaceOrde we are just ingoring this atm */,
banks?: Bank[] /** TODO for serum3PlaceOrder we are just ingoring this atm */,
) {
const healthRemainingAccounts: PublicKey[] = [];
@ -847,8 +974,10 @@ export class MangoClient {
.filter((token) => token.tokenIndex !== 65535)
.map((token) => token.tokenIndex);
if (bank) {
tokenIndices.push(bank.tokenIndex);
if (banks?.length) {
for (const bank of banks) {
tokenIndices.push(bank.tokenIndex);
}
}
const mintInfos = await Promise.all(
@ -888,4 +1017,46 @@ export class MangoClient {
return healthRemainingAccounts;
}
/*
Orca ix references:
swap fn: https://github.com/orca-so/typescript-sdk/blob/main/src/model/orca/pool/orca-pool.ts#L162
swap ix: https://github.com/orca-so/typescript-sdk/blob/main/src/public/utils/web3/instructions/pool-instructions.ts#L41
*/
private async buildOrcaInstruction(
orcaTokenSwapId: PublicKey,
inputBank: Bank,
outputBank: Bank,
amountInU64: BN,
minimumAmountOutU64: BN,
) {
const poolParams = orcaDevnetPoolConfigs[OrcaDevnetPoolConfig.ORCA_SOL];
const [authorityForPoolAddress] = await PublicKey.findProgramAddress(
[poolParams.address.toBuffer()],
orcaTokenSwapId,
);
const instruction = TokenSwap.swapInstruction(
poolParams.address,
authorityForPoolAddress,
inputBank.publicKey, // userTransferAuthority
inputBank.vault, // inputTokenUserAddress
poolParams.tokens[inputBank.mint.toString()].addr, // inputToken.addr
poolParams.tokens[outputBank.mint.toString()].addr, // outputToken.addr
outputBank.vault, // outputTokenUserAddress
poolParams.poolTokenMint,
poolParams.feeAccount,
null, // hostFeeAccount
orcaTokenSwapId,
TOKEN_PROGRAM_ID,
amountInU64,
minimumAmountOutU64,
);
instruction.keys[2].isSigner = false;
instruction.keys[2].isWritable = true;
return { instruction, signers: [] };
}
}

View File

@ -0,0 +1,15 @@
export const getTokenDecimals = (symbol: string) => {
const tokenMeta = tokens.find((t) => t.symbol === symbol);
if (!tokenMeta) throw new Error('TokenDecimalError: Token not found');
return tokenMeta.decimals;
};
const tokens = [
{ symbol: 'USDC', decimals: 6 },
{ symbol: 'SOL', decimals: 9 },
{ symbol: 'BTC', decimals: 6 },
];
export default tokens;

View File

@ -6,13 +6,18 @@ import { DEVNET_SERUM3_PROGRAM_ID } from '../constants';
const DEVNET_SERUM3_MARKETS = new Map([
['BTC/USDC', 'DW83EpHFywBxCHmyARxwj3nzxJd7MUdSeznmrdzZKNZB'],
['SOL/USDC', '5xWpt56U1NCuHoAEtpLeUrQcxDkEpNfScjfLFaRzLPgR'],
]);
const DEVNET_MINTS = new Map([
['USDC', '8FRFC6MoGGkMFQwngccyu69VnYbzykGeez7ignHVAFSN'],
['USDC', 'EmXq3Ni9gfudTiyNKzzYvpnQqnJEMRw2ttnVXoJXjLo1'], // use devnet usdc
['BTC', '3UNBZ6o52WTWwjac2kPUb4FyodhU1vFkRJheu1Sh2TvU'],
['SOL', 'So11111111111111111111111111111111111111112'],
['ORCA', 'orcarKHSqC5CDDsGbho8GKvwExejWHxTqGzXgcewB9L'],
]);
const DEVNET_ORACLES = new Map([
['BTC', 'HovQMDrbAgAYPCmHVSrezcSmkMtXSSUsLDFANExrZh2J'],
['SOL', 'J83w4HKfqxwcq3BEMMkPFSppX3gqekLyLJBexebFVkix'],
['ORCA', 'A1WttWF7X3Rg6ZRpB2YQUFHCRh1kiXV8sKKLV3S9neJV'],
]);
//
@ -106,6 +111,60 @@ async function main() {
await group.reload(client);
} catch (error) {}
// register token 2
console.log(`Registering SOL...`);
const solDevnetMint = new PublicKey(DEVNET_MINTS.get('SOL')!);
const solDevnetOracle = new PublicKey(DEVNET_ORACLES.get('SOL')!);
try {
await client.registerToken(
group,
solDevnetMint,
solDevnetOracle,
2, // tokenIndex
'SOL',
0.4,
0.07,
0.8,
0.9,
0.0005,
0.0005,
1.5,
0.8,
0.6,
1.2,
1.4,
0.02,
);
await group.reload(client);
} catch (error) {}
// register token 3
console.log(`Registering ORCA...`);
const orcaDevnetMint = new PublicKey(DEVNET_MINTS.get('ORCA')!);
const orcaDevnetOracle = new PublicKey(DEVNET_ORACLES.get('ORCA')!);
try {
await client.registerToken(
group,
orcaDevnetMint,
orcaDevnetOracle,
3, // tokenIndex
'ORCA',
0.4,
0.07,
0.8,
0.9,
0.0005,
0.0005,
1.5,
0.8,
0.6,
1.2,
1.4,
0.02,
);
await group.reload(client);
} catch (error) {}
// log tokens/banks
for (const bank of await group.banksMap.values()) {
console.log(
@ -159,6 +218,7 @@ async function main() {
0.05,
100,
);
console.log('done');
} catch (error) {
console.log(error);
}

View File

@ -0,0 +1,46 @@
import fs from 'fs';
import * as os from 'os';
import { AnchorProvider, Wallet } from '@project-serum/anchor';
import { Market } from '@project-serum/serum';
import { Connection, Keypair, PublicKey } from '@solana/web3.js';
import { MangoClient } from '../client';
const main = async () => {
const options = AnchorProvider.defaultOptions();
const connection = new Connection(
'https://mango.devnet.rpcpool.com',
options,
);
const admin = Keypair.fromSecretKey(
Buffer.from(
JSON.parse(
fs.readFileSync(os.homedir() + '/.config/solana/admin.json', 'utf-8'),
),
),
);
const adminWallet = new Wallet(admin);
console.log(`Admin ${adminWallet.publicKey.toBase58()}`);
const adminProvider = new AnchorProvider(connection, adminWallet, options);
const client = await MangoClient.connect(adminProvider, true);
const btcMint = new PublicKey('3UNBZ6o52WTWwjac2kPUb4FyodhU1vFkRJheu1Sh2TvU');
const usdcMint = new PublicKey(
'EmXq3Ni9gfudTiyNKzzYvpnQqnJEMRw2ttnVXoJXjLo1',
);
const serumProgramId = new PublicKey(
'DESVgJVGajEgKGXhb6XmqDHGz3VjdgP7rEVESBgxmroY',
);
const market = await Market.findAccountsByMints(
connection,
btcMint,
usdcMint,
serumProgramId,
);
console.log('market', market);
};
main();

6
ts/client/src/types.ts Normal file
View File

@ -0,0 +1,6 @@
import { BN } from '@project-serum/anchor';
export class MarginTradeWithdraw {
static index: number;
static amount: BN;
}

View File

@ -1,4 +1,10 @@
import { AccountMeta } from '@solana/web3.js';
import BN from 'bn.js';
import {
ASSOCIATED_TOKEN_PROGRAM_ID,
TOKEN_PROGRAM_ID,
u64,
} from '@solana/spl-token';
import { AccountMeta, PublicKey } from '@solana/web3.js';
export function debugAccountMetas(ams: AccountMeta[]) {
for (const am of ams) {
@ -30,3 +36,43 @@ export async function findOrCreate<T>(
one = many[0];
return one;
}
/**
* Get the address of the associated token account for a given mint and owner
*
* @param mint Token mint account
* @param owner Owner of the new account
* @param allowOwnerOffCurve Allow the owner account to be a PDA (Program Derived Address)
* @param programId SPL Token program account
* @param associatedTokenProgramId SPL Associated Token program account
*
* @return Address of the associated token account
*/
export async function getAssociatedTokenAddress(
mint: PublicKey,
owner: PublicKey,
allowOwnerOffCurve = false,
programId = TOKEN_PROGRAM_ID,
associatedTokenProgramId = ASSOCIATED_TOKEN_PROGRAM_ID,
): Promise<PublicKey> {
if (!allowOwnerOffCurve && !PublicKey.isOnCurve(owner.toBuffer()))
throw new Error('TokenOwnerOffCurve');
const [address] = await PublicKey.findProgramAddress(
[owner.toBuffer(), programId.toBuffer(), mint.toBuffer()],
associatedTokenProgramId,
);
return address;
}
export function toNativeDecimals(amount: number, decimals: number): BN {
return new BN(Math.trunc(amount * Math.pow(10, decimals)));
}
export function toU64(amount: number, decimals): BN {
const bn = toNativeDecimals(amount, decimals).toString();
console.log('bn', bn);
return new u64(bn);
}

View File

@ -1,29 +0,0 @@
{
"extends": "@tsconfig/recommended/tsconfig.json",
"compilerOptions": {
"allowJs": true,
"checkJs": true,
"declaration": true,
"declarationDir": "dist",
"declarationMap": true,
"noErrorTruncation": true,
"esModuleInterop": true,
"lib": [
"es2019"
],
"noImplicitAny": false,
"outDir": "dist",
"resolveJsonModule": true,
"sourceMap": true,
"strictNullChecks": true,
"target": "es6"
},
"include": [
"./src/**/*"
],
"exclude": [
"./src/**/*.test.js",
"node_modules",
"**/node_modules"
]
}

1219
yarn.lock

File diff suppressed because it is too large Load Diff