wip margin trade with spl token transfers

This commit is contained in:
tjs 2022-06-23 12:58:43 +02:00 committed by microwavedcola1
parent 54c0306ae7
commit 997d363195
8 changed files with 637 additions and 1193 deletions

View File

@ -55,11 +55,10 @@
"trailingComma": "all"
},
"dependencies": {
"@orca-so/sdk": "^1.2.24",
"@project-serum/anchor": "^0.24.2",
"@project-serum/serum": "^0.13.65",
"@pythnetwork/client": "^2.7.0",
"@solana/spl-token": "~0.1.8",
"@solana/spl-token": "^0.2.0",
"big.js": "^6.1.1",
"bs58": "^5.0.0"
},

View File

@ -6,8 +6,13 @@ import {
initializeAccount,
WRAPPED_SOL_MINT,
} from '@project-serum/serum/lib/token-instructions';
import { parsePriceData, PriceData } from '@pythnetwork/client';
import { TOKEN_PROGRAM_ID } from '@solana/spl-token';
import { parsePriceData } from '@pythnetwork/client';
import {
ASSOCIATED_TOKEN_PROGRAM_ID,
createAssociatedTokenAccountInstruction,
createTransferInstruction,
TOKEN_PROGRAM_ID,
} from '@solana/spl-token';
import {
AccountMeta,
Cluster,
@ -36,10 +41,10 @@ import {
} from './accounts/serum3';
import { SERUM3_PROGRAM_ID } from './constants';
import { Id } from './ids';
import {
buildOrcaInstruction,
ORCA_TOKEN_SWAP_ID_DEVNET,
} from './integrations/orca/index';
// import {
// buildOrcaInstruction,
// ORCA_TOKEN_SWAP_ID_DEVNET,
// } from './integrations/orca/index';
import { IDL, MangoV4 } from './mango_v4';
import { FlashLoanWithdraw } from './types';
import {
@ -267,8 +272,6 @@ export class MangoClient {
if (banks[index].name === 'USDC') {
banks[index].price = 1;
} else {
console.log('parsePriceData(price.data)', parsePriceData(price.data));
banks[index].price = parsePriceData(price.data).previousPrice;
}
}
@ -1135,41 +1138,128 @@ export class MangoClient {
} as AccountMeta),
);
const targetProgramId = ORCA_TOKEN_SWAP_ID_DEVNET;
const { instruction, signers } = await buildOrcaInstruction(
targetProgramId,
inputBank,
outputBank,
toU64(amountIn, 9),
toU64(minimumAmountOut, 6),
/*
*
* Find or create associated token account
*
*/
let tokenAccountPk = await getAssociatedTokenAddress(
inputBank.mint,
mangoAccount.owner,
);
const targetRemainingAccounts = instruction.keys;
const tokenAccExists =
await this.program.provider.connection.getAccountInfo(tokenAccountPk);
let preInstructions = [];
if (!tokenAccExists) {
preInstructions.push(
createAssociatedTokenAccountInstruction(
mangoAccount.owner,
tokenAccountPk,
mangoAccount.owner,
inputBank.mint,
TOKEN_PROGRAM_ID,
ASSOCIATED_TOKEN_PROGRAM_ID,
),
);
}
/*
*
* Borrow a token and transfer to wallet then transfer back
*
*/
// TODO don't hard code decimal #
const decimals = 6;
const nativeAmount = toU64(amountIn, decimals);
const instructions: TransactionInstruction[] = [];
const transferIx = createTransferInstruction(
inputBank.vault,
tokenAccountPk,
inputBank.publicKey,
nativeAmount,
[],
TOKEN_PROGRAM_ID,
);
const inputBankKey = transferIx.keys[2];
transferIx.keys[2] = { ...inputBankKey, isWritable: true, isSigner: false };
instructions.push(transferIx);
const transferIx2 = createTransferInstruction(
tokenAccountPk,
inputBank.vault,
mangoAccount.owner,
nativeAmount,
[],
TOKEN_PROGRAM_ID,
);
instructions.push(transferIx2);
/*
*
* Build data objects for margin trade instructions
*
*/
const targetRemainingAccounts = instructions
.map((ix) => [
{
pubkey: ix.programId,
isWritable: false,
isSigner: false,
} as AccountMeta,
...ix.keys,
])
.flat();
const banks = Array.from(group.banksMap.values());
const bankPks = banks.map((b) => b.publicKey.toString());
const bankIndex = bankPks.indexOf(inputBank.publicKey.toString());
const withdraws: FlashLoanWithdraw[] = [
{ index: 3, amount: toU64(amountIn, 9) },
];
const cpiData = instruction.data;
let cpiDatas = [];
for (const [index, ix] of instructions.entries()) {
if (index === 0) {
cpiDatas.push({
accountStart: new BN(parsedHealthAccounts.length),
data: ix.data,
});
} else {
cpiDatas.push({
accountStart: cpiDatas[index - 1].accountStart.add(
new BN(instructions[index - 1].keys.length + 1),
),
data: ix.data,
});
}
}
console.log('instruction1', transferIx);
console.log('instruction2', transferIx2);
console.log('cpiDatas', cpiDatas);
console.log(
'targetRemainingAccounts',
targetRemainingAccounts.map((t) => ({
...t,
pubkey: t.pubkey.toString(),
})),
);
return await this.program.methods
.flashLoan(withdraws, [
{ accountStart: new BN(parsedHealthAccounts.length), data: cpiData },
{ accountStart: new BN(parsedHealthAccounts.length), data: cpiDatas },
])
.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)
.remainingAccounts([...parsedHealthAccounts, ...targetRemainingAccounts])
.preInstructions(preInstructions)
.signers([])
.rpc({ skipPreflight: true });
}

View File

@ -16,7 +16,7 @@ export {
Serum3Side,
} from './accounts/serum3';
export * from './constants';
export * from './integrations/orca/index';
// export * from './integrations/orca/index';
export {
Group,
StubOracle,

View File

@ -1,200 +1,200 @@
import {
OrcaPoolToken,
ORCA_TOKEN_SWAP_ID_DEVNET,
PoolTokenCount,
} from '@orca-so/sdk';
import { orcaDevnetPoolConfigs } from '@orca-so/sdk/dist/constants/devnet/pools';
import { OrcaPoolParams } from '@orca-so/sdk/dist/model/orca/pool/pool-types';
import { OrcaPoolConfig as OrcaDevnetPoolConfig } from '@orca-so/sdk/dist/public/devnet/pools/config';
import { BN } from '@project-serum/anchor';
import {
AccountInfo,
AccountLayout,
TOKEN_PROGRAM_ID,
u64,
} from '@solana/spl-token';
import { TokenSwap } from '@solana/spl-token-swap';
import { Connection, PublicKey } from '@solana/web3.js';
// import {
// OrcaPoolToken,
// ORCA_TOKEN_SWAP_ID_DEVNET,
// PoolTokenCount,
// } from '@orca-so/sdk';
// import { orcaDevnetPoolConfigs } from '@orca-so/sdk/dist/constants/devnet/pools';
// import { OrcaPoolParams } from '@orca-so/sdk/dist/model/orca/pool/pool-types';
// import { OrcaPoolConfig as OrcaDevnetPoolConfig } from '@orca-so/sdk/dist/public/devnet/pools/config';
// import { BN } from '@project-serum/anchor';
// import {
// AccountInfo,
// AccountLayout,
// TOKEN_PROGRAM_ID,
// u64,
// } from '@solana/spl-token';
// import { TokenSwap } from '@solana/spl-token-swap';
// import { Connection, PublicKey } from '@solana/web3.js';
import { Bank } from '../../accounts/bank';
import { toNativeDecimals, toUiDecimals } from '../../utils';
import * as Tokens from './tokens';
// import { Bank } from '../../accounts/bank';
// import { toNativeDecimals, toUiDecimals } from '../../utils';
// import * as Tokens from './tokens';
export { ORCA_TOKEN_SWAP_ID_DEVNET };
// export { ORCA_TOKEN_SWAP_ID_DEVNET };
/*
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
*/
export const buildOrcaInstruction = async (
orcaTokenSwapId: PublicKey,
inputBank: Bank,
outputBank: Bank,
amountInU64: BN,
minimumAmountOutU64: BN,
) => {
// TODO: select the correct pool params based on passed in banks
const poolParams = orcaDevnetPoolConfigs[OrcaDevnetPoolConfig.ORCA_SOL];
// /*
// 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
// */
// export const buildOrcaInstruction = async (
// orcaTokenSwapId: PublicKey,
// inputBank: Bank,
// outputBank: Bank,
// amountInU64: BN,
// minimumAmountOutU64: BN,
// ) => {
// // TODO: select the correct pool params based on passed in banks
// const poolParams = orcaDevnetPoolConfigs[OrcaDevnetPoolConfig.ORCA_SOL];
const [authorityForPoolAddress] = await PublicKey.findProgramAddress(
[poolParams.address.toBuffer()],
orcaTokenSwapId,
);
// 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,
);
// 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;
// instruction.keys[2].isSigner = false;
// instruction.keys[2].isWritable = true;
return { instruction, signers: [] };
};
// return { instruction, signers: [] };
// };
export const getOrcaOutputAmount = async (
connection: Connection,
inputToken: string,
outputToken: string,
amountIn: number,
): Promise<number> => {
// TODO: select the correct pool params based on passed in banks
const inputMint = Tokens.solToken;
const poolParams = getOrcaPoolParams(inputToken, outputToken);
// export const getOrcaOutputAmount = async (
// connection: Connection,
// inputToken: string,
// outputToken: string,
// amountIn: number,
// ): Promise<number> => {
// // TODO: select the correct pool params based on passed in banks
// const inputMint = Tokens.solToken;
// const poolParams = getOrcaPoolParams(inputToken, outputToken);
const { inputPoolToken, outputPoolToken } = getTokens(
poolParams,
inputMint.mint.toString(),
);
// const { inputPoolToken, outputPoolToken } = getTokens(
// poolParams,
// inputMint.mint.toString(),
// );
const { inputTokenCount, outputTokenCount } = await getTokenCount(
connection,
poolParams,
inputPoolToken,
outputPoolToken,
);
// const { inputTokenCount, outputTokenCount } = await getTokenCount(
// connection,
// poolParams,
// inputPoolToken,
// outputPoolToken,
// );
const [poolInputAmount, poolOutputAmount] = [
inputTokenCount,
outputTokenCount,
];
// const [poolInputAmount, poolOutputAmount] = [
// inputTokenCount,
// outputTokenCount,
// ];
const invariant = poolInputAmount.mul(poolOutputAmount);
const nativeAmountIn = toNativeDecimals(amountIn, 9);
// const invariant = poolInputAmount.mul(poolOutputAmount);
// const nativeAmountIn = toNativeDecimals(amountIn, 9);
const [newPoolOutputAmount] = ceilingDivision(
invariant,
poolInputAmount.add(nativeAmountIn),
);
// const [newPoolOutputAmount] = ceilingDivision(
// invariant,
// poolInputAmount.add(nativeAmountIn),
// );
const outputAmount = poolOutputAmount.sub(newPoolOutputAmount);
// const outputAmount = poolOutputAmount.sub(newPoolOutputAmount);
return toUiDecimals(outputAmount.toNumber(), 6);
};
// return toUiDecimals(outputAmount.toNumber(), 6);
// };
function getTokens(poolParams: OrcaPoolParams, inputTokenId: string) {
if (poolParams.tokens[inputTokenId] == undefined) {
throw new Error('Input token not part of pool');
}
// function getTokens(poolParams: OrcaPoolParams, inputTokenId: string) {
// if (poolParams.tokens[inputTokenId] == undefined) {
// throw new Error('Input token not part of pool');
// }
const tokenAId = poolParams.tokenIds[0];
const tokenBId = poolParams.tokenIds[1];
// const tokenAId = poolParams.tokenIds[0];
// const tokenBId = poolParams.tokenIds[1];
const forward = tokenAId == inputTokenId;
// const forward = tokenAId == inputTokenId;
const inputOrcaToken = forward
? poolParams.tokens[tokenAId]
: poolParams.tokens[tokenBId];
const outputOrcaToken = forward
? poolParams.tokens[tokenBId]
: poolParams.tokens[tokenAId];
return { inputPoolToken: inputOrcaToken, outputPoolToken: outputOrcaToken };
}
// const inputOrcaToken = forward
// ? poolParams.tokens[tokenAId]
// : poolParams.tokens[tokenBId];
// const outputOrcaToken = forward
// ? poolParams.tokens[tokenBId]
// : poolParams.tokens[tokenAId];
// return { inputPoolToken: inputOrcaToken, outputPoolToken: outputOrcaToken };
// }
const getOrcaPoolParams = (inputToken: string, outputToken: string) => {
return orcaDevnetPoolConfigs[OrcaDevnetPoolConfig.ORCA_SOL];
};
// const getOrcaPoolParams = (inputToken: string, outputToken: string) => {
// return orcaDevnetPoolConfigs[OrcaDevnetPoolConfig.ORCA_SOL];
// };
async function getTokenCount(
connection: Connection,
poolParams: OrcaPoolParams,
inputPoolToken: OrcaPoolToken,
outputPoolToken: OrcaPoolToken,
): Promise<PoolTokenCount> {
if (poolParams.tokens[inputPoolToken.mint.toString()] == undefined) {
throw new Error('Input token not part of pool');
}
// async function getTokenCount(
// connection: Connection,
// poolParams: OrcaPoolParams,
// inputPoolToken: OrcaPoolToken,
// outputPoolToken: OrcaPoolToken,
// ): Promise<PoolTokenCount> {
// if (poolParams.tokens[inputPoolToken.mint.toString()] == undefined) {
// throw new Error('Input token not part of pool');
// }
if (poolParams.tokens[outputPoolToken.mint.toString()] == undefined) {
throw new Error('Output token not part of pool');
}
// if (poolParams.tokens[outputPoolToken.mint.toString()] == undefined) {
// throw new Error('Output token not part of pool');
// }
const accountInfos = await connection.getMultipleAccountsInfo([
inputPoolToken.addr,
outputPoolToken.addr,
]);
// const accountInfos = await connection.getMultipleAccountsInfo([
// inputPoolToken.addr,
// outputPoolToken.addr,
// ]);
const tokens = accountInfos.map((info) =>
info != undefined ? deserializeAccount(info.data) : undefined,
);
const inputTokenAccount = tokens[0],
outputTokenAccount = tokens[1];
// const tokens = accountInfos.map((info) =>
// info != undefined ? deserializeAccount(info.data) : undefined,
// );
// const inputTokenAccount = tokens[0],
// outputTokenAccount = tokens[1];
if (inputTokenAccount === undefined || outputTokenAccount === undefined) {
throw new Error('Unable to fetch accounts for specified tokens.');
}
// if (inputTokenAccount === undefined || outputTokenAccount === undefined) {
// throw new Error('Unable to fetch accounts for specified tokens.');
// }
return {
inputTokenCount: inputTokenAccount.amount,
outputTokenCount: outputTokenAccount.amount,
};
}
// return {
// inputTokenCount: inputTokenAccount.amount,
// outputTokenCount: outputTokenAccount.amount,
// };
// }
const deserializeAccount = (
data: Buffer | undefined,
): AccountInfo | undefined => {
if (data == undefined || data.length == 0) {
return undefined;
}
// const deserializeAccount = (
// data: Buffer | undefined,
// ): AccountInfo | undefined => {
// if (data == undefined || data.length == 0) {
// return undefined;
// }
const accountInfo = AccountLayout.decode(data);
accountInfo.mint = new PublicKey(accountInfo.mint);
accountInfo.owner = new PublicKey(accountInfo.owner);
accountInfo.amount = u64.fromBuffer(accountInfo.amount);
// const accountInfo = AccountLayout.decode(data);
// accountInfo.mint = new PublicKey(accountInfo.mint);
// accountInfo.owner = new PublicKey(accountInfo.owner);
// accountInfo.amount = u64.fromBuffer(accountInfo.amount);
return accountInfo;
};
// return accountInfo;
// };
const ZERO = new BN(0);
const ONE = new BN(1);
const ceilingDivision = (dividend: u64, divisor: u64): [u64, u64] => {
let quotient = dividend.div(divisor);
if (quotient.eq(ZERO)) {
return [ZERO, divisor];
}
// const ZERO = new BN(0);
// const ONE = new BN(1);
// const ceilingDivision = (dividend: u64, divisor: u64): [u64, u64] => {
// let quotient = dividend.div(divisor);
// if (quotient.eq(ZERO)) {
// return [ZERO, divisor];
// }
let remainder = dividend.mod(divisor);
if (remainder.gt(ZERO)) {
quotient = quotient.add(ONE);
divisor = dividend.div(quotient);
remainder = dividend.mod(quotient);
if (remainder.gt(ZERO)) {
divisor = divisor.add(ONE);
}
}
// let remainder = dividend.mod(divisor);
// if (remainder.gt(ZERO)) {
// quotient = quotient.add(ONE);
// divisor = dividend.div(quotient);
// remainder = dividend.mod(quotient);
// if (remainder.gt(ZERO)) {
// divisor = divisor.add(ONE);
// }
// }
return [quotient, divisor];
};
// return [quotient, divisor];
// };

View File

@ -1,41 +1,41 @@
import { OrcaToken } from '@orca-so/sdk';
import { PublicKey } from '@solana/web3.js';
// import { OrcaToken } from '@orca-so/sdk';
// import { PublicKey } from '@solana/web3.js';
/**
* The following content is auto-generated.
*/
// /**
// * The following content is auto-generated.
// */
export const ethToken: OrcaToken = Object.freeze({
tag: 'ETH',
name: 'Ethereum',
mint: new PublicKey('Ff5JqsAYUD4vAfQUtfRprT4nXu9e28tTBZTDFMnJNdvd'),
scale: 9,
});
// export const ethToken: OrcaToken = Object.freeze({
// tag: 'ETH',
// name: 'Ethereum',
// mint: new PublicKey('Ff5JqsAYUD4vAfQUtfRprT4nXu9e28tTBZTDFMnJNdvd'),
// scale: 9,
// });
export const orcaToken: OrcaToken = Object.freeze({
tag: 'ORCA',
name: 'Orca',
mint: new PublicKey('orcarKHSqC5CDDsGbho8GKvwExejWHxTqGzXgcewB9L'),
scale: 6,
});
// export const orcaToken: OrcaToken = Object.freeze({
// tag: 'ORCA',
// name: 'Orca',
// mint: new PublicKey('orcarKHSqC5CDDsGbho8GKvwExejWHxTqGzXgcewB9L'),
// scale: 6,
// });
export const solToken: OrcaToken = Object.freeze({
tag: 'SOL',
name: 'Solana',
mint: new PublicKey('So11111111111111111111111111111111111111112'),
scale: 9,
});
// export const solToken: OrcaToken = Object.freeze({
// tag: 'SOL',
// name: 'Solana',
// mint: new PublicKey('So11111111111111111111111111111111111111112'),
// scale: 9,
// });
export const usdcToken: OrcaToken = Object.freeze({
tag: 'USDC',
name: 'USD Coin',
mint: new PublicKey('EmXq3Ni9gfudTiyNKzzYvpnQqnJEMRw2ttnVXoJXjLo1'),
scale: 6,
});
// export const usdcToken: OrcaToken = Object.freeze({
// tag: 'USDC',
// name: 'USD Coin',
// mint: new PublicKey('EmXq3Ni9gfudTiyNKzzYvpnQqnJEMRw2ttnVXoJXjLo1'),
// scale: 6,
// });
export const usdtToken: OrcaToken = Object.freeze({
tag: 'USDT',
name: 'Tether USD',
mint: new PublicKey('6PE3Mwjzx9h8kCoBp5YPed9TFoG7du8L98yucBP5ps3x'),
scale: 6,
});
// export const usdtToken: OrcaToken = Object.freeze({
// tag: 'USDT',
// name: 'Tether USD',
// mint: new PublicKey('6PE3Mwjzx9h8kCoBp5YPed9TFoG7du8L98yucBP5ps3x'),
// scale: 6,
// });

View File

@ -0,0 +1,40 @@
// import { Connection, PublicKey } from '@solana/web3.js';
// export const getSerumOutputAmount = async (
// connection: Connection,
// inputMint: string,
// outputToken: string,
// amountIn: number,
// ): Promise<number> => {
// // TODO: select the correct pool params based on passed in banks
// const poolParams = getOrcaPoolParams(inputToken, outputToken);
// const { inputPoolToken, outputPoolToken } = getTokens(
// poolParams,
// inputMint.mint.toString(),
// );
// const { inputTokenCount, outputTokenCount } = await getTokenCount(
// connection,
// poolParams,
// inputPoolToken,
// outputPoolToken,
// );
// const [poolInputAmount, poolOutputAmount] = [
// inputTokenCount,
// outputTokenCount,
// ];
// const invariant = poolInputAmount.mul(poolOutputAmount);
// const nativeAmountIn = toNativeDecimals(amountIn, 9);
// const [newPoolOutputAmount] = ceilingDivision(
// invariant,
// poolInputAmount.add(nativeAmountIn),
// );
// const outputAmount = poolOutputAmount.sub(newPoolOutputAmount);
// return toUiDecimals(outputAmount.toNumber(), 6);
// };

View File

@ -1,7 +1,6 @@
import {
ASSOCIATED_TOKEN_PROGRAM_ID,
TOKEN_PROGRAM_ID,
u64,
} from '@solana/spl-token';
import { AccountMeta, PublicKey } from '@solana/web3.js';
import BN from 'bn.js';
@ -81,7 +80,7 @@ export function toU64(amount: number, decimals): BN {
const bn = toNativeDecimals(amount, decimals).toString();
console.log('bn', bn);
return new u64(bn);
return new BN(bn);
}
export function nativeI80F48ToUi(amount: I80F48, decimals: number): I80F48 {

1228
yarn.lock

File diff suppressed because it is too large Load Diff