@powrldgr/raydium-sdk-v2
Version:
An SDK for building applications on top of Raydium.
1,239 lines (1,170 loc) • 42.6 kB
text/typescript
import { EpochInfo, PublicKey } from "@solana/web3.js";
import { createTransferInstruction, TOKEN_PROGRAM_ID, TOKEN_2022_PROGRAM_ID } from "@solana/spl-token";
import BN from "bn.js";
import Decimal from "decimal.js";
import { AmmV4Keys, ApiV3Token, ClmmKeys, PoolKeys } from "@/api";
import {
AMM_V4,
BigNumberish,
CLMM_PROGRAM_ID,
CREATE_CPMM_POOL_PROGRAM,
fetchMultipleMintInfos,
getMultipleAccountsInfoWithCustomFlags,
minExpirationTime,
parseBigNumberish,
solToWSol,
WSOLMint,
} from "@/common";
import { MakeMultiTxData, MakeTxData } from "@/common/txTool/txTool";
import { InstructionType, TxVersion } from "@/common/txTool/txType";
import { publicKey, struct } from "../../marshmallow";
import { Price, TokenAmount } from "../../module";
import { ClmmRpcData, ComputeClmmPoolInfo, PoolUtils, ReturnTypeFetchMultiplePoolTickArrays } from "../../raydium/clmm";
import { PoolInfoLayout } from "../../raydium/clmm/layout";
import { CpmmPoolInfoLayout, getPdaPoolAuthority } from "../../raydium/cpmm";
import {
ComputeAmountOutParam,
getLiquidityAssociatedAuthority,
liquidityStateV4Layout,
toAmmComputePoolInfo,
} from "../../raydium/liquidity";
import { ComputeBudgetConfig, ReturnTypeFetchMultipleMintInfos } from "../../raydium/type";
import { closeAccountInstruction, createWSolAccountInstructions } from "../account/instruction";
import { TokenAccount } from "../account/types";
import { CpmmComputeData } from "../cpmm";
import { AmmRpcData } from "../liquidity";
import ModuleBase, { ModuleBaseProps } from "../moduleBase";
import { Market, MARKET_STATE_LAYOUT_V3 } from "../serum";
import { toApiV3Token, toToken, toTokenAmount } from "../token";
import { makeSwapInstruction } from "./instrument";
import {
BasicPoolInfo,
ComputeAmountOutAmmLayout,
ComputeAmountOutLayout,
ComputePoolType,
ComputeRoutePathType,
ReturnTypeFetchMultipleInfo,
ReturnTypeGetAllRoute,
RoutePathType,
} from "./type";
const ZERO = new BN(0);
export default class TradeV2 extends ModuleBase {
constructor(params: ModuleBaseProps) {
super(params);
}
private async getWSolAccounts(): Promise<TokenAccount[]> {
this.scope.checkOwner();
await this.scope.account.fetchWalletTokenAccounts();
const tokenAccounts = this.scope.account.tokenAccounts.filter((acc) => acc.mint.equals(WSOLMint));
tokenAccounts.sort((a, b) => {
if (a.isAssociated) return 1;
if (b.isAssociated) return -1;
return a.amount.lt(b.amount) ? -1 : 1;
});
return tokenAccounts;
}
public async unWrapWSol<T extends TxVersion>(props: {
amount: BigNumberish;
computeBudgetConfig?: ComputeBudgetConfig;
tokenProgram?: PublicKey;
txVersion?: T;
feePayer?: PublicKey;
}): Promise<MakeTxData<T>> {
const { amount, tokenProgram, txVersion = TxVersion.LEGACY, feePayer } = props;
const tokenAccounts = await this.getWSolAccounts();
const txBuilder = this.createTxBuilder(feePayer);
txBuilder.addCustomComputeBudget(props.computeBudgetConfig);
// const ins = await createWSolAccountInstructions({
// connection: this.scope.connection,
// owner: this.scope.ownerPubKey,
// payer: this.scope.ownerPubKey,
// amount: 0,
// });
// txBuilder.addInstruction(ins);
const amountBN = parseBigNumberish(amount);
for (let i = 0; i < tokenAccounts.length; i++) {
if (amountBN.gte(tokenAccounts[i].amount)) {
txBuilder.addInstruction({
instructions: [
closeAccountInstruction({
tokenAccount: tokenAccounts[i].publicKey!,
payer: this.scope.ownerPubKey,
owner: this.scope.ownerPubKey,
programId: tokenProgram,
}),
],
});
amountBN.sub(tokenAccounts[i].amount);
} else {
txBuilder.addInstruction({
instructions: [
closeAccountInstruction({
tokenAccount: tokenAccounts[i].publicKey!,
payer: this.scope.ownerPubKey,
owner: this.scope.ownerPubKey,
programId: tokenProgram,
}),
],
});
}
}
return txBuilder.versionBuild({ txVersion }) as Promise<MakeTxData<T>>;
}
public async wrapWSol<T extends TxVersion>(
amount: BigNumberish,
tokenProgram?: PublicKey,
txVersion?: T,
feePayer?: PublicKey,
): Promise<MakeTxData<T>> {
// const tokenAccounts = await this.getWSolAccounts();
const txBuilder = this.createTxBuilder(feePayer);
const ins = await createWSolAccountInstructions({
connection: this.scope.connection,
owner: this.scope.ownerPubKey,
payer: this.scope.ownerPubKey,
amount,
skipCloseAccount: true,
});
txBuilder.addInstruction(ins);
// if (tokenAccounts.length) {
// // already have wsol account
// txBuilder.addInstruction({
// instructions: [
// makeTransferInstruction({
// destination: tokenAccounts[0].publicKey!,
// source: ins.addresses.newAccount,
// amount,
// owner: this.scope.ownerPubKey,
// tokenProgram,
// }),
// ],
// endInstructions: [
// closeAccountInstruction({
// tokenAccount: ins.addresses.newAccount,
// payer: this.scope.ownerPubKey,
// owner: this.scope.ownerPubKey,
// programId: tokenProgram,
// }),
// ],
// });
// }
return txBuilder.versionBuild({ txVersion: txVersion ?? TxVersion.LEGACY }) as Promise<MakeTxData<T>>;
}
public async swap<T extends TxVersion>({
swapInfo,
swapPoolKeys,
ownerInfo,
computeBudgetConfig,
routeProgram,
txVersion,
feePayer,
}: {
txVersion: T;
swapInfo: ComputeAmountOutLayout;
swapPoolKeys?: PoolKeys[];
ownerInfo: {
associatedOnly: boolean;
checkCreateATAOwner: boolean;
};
routeProgram: PublicKey;
computeBudgetConfig?: ComputeBudgetConfig;
feePayer?: PublicKey;
}): Promise<MakeMultiTxData<T>> {
const txBuilder = this.createTxBuilder(feePayer);
const amountIn = swapInfo.amountIn;
const amountOut = swapInfo.amountOut;
const useSolBalance = amountIn.amount.token.mint.equals(WSOLMint);
const isOutputSol = amountOut.amount.token.mint.equals(WSOLMint);
const inputMint = amountIn.amount.token.mint;
const outputMint = amountOut.amount.token.mint;
const { account: sourceAcc, instructionParams: sourceAccInsParams } =
await this.scope.account.getOrCreateTokenAccount({
tokenProgram: amountIn.amount.token.isToken2022 ? TOKEN_2022_PROGRAM_ID : TOKEN_PROGRAM_ID,
mint: inputMint,
notUseTokenAccount: useSolBalance,
owner: this.scope.ownerPubKey,
skipCloseAccount: !useSolBalance,
createInfo: useSolBalance
? {
payer: this.scope.ownerPubKey,
amount: amountIn.amount.raw,
}
: undefined,
associatedOnly: useSolBalance ? false : ownerInfo.associatedOnly,
checkCreateATAOwner: ownerInfo.checkCreateATAOwner,
});
sourceAccInsParams && txBuilder.addInstruction(sourceAccInsParams);
if (sourceAcc === undefined) {
throw Error("input account check error");
}
let destinationAcc: PublicKey;
if (swapInfo.routeType === "route" && !isOutputSol) {
destinationAcc = this.scope.account.getAssociatedTokenAccount(
outputMint,
amountOut.amount.token.isToken2022 ? TOKEN_2022_PROGRAM_ID : TOKEN_PROGRAM_ID,
);
} else {
const { account, instructionParams } = await this.scope.account.getOrCreateTokenAccount({
tokenProgram: amountOut.amount.token.isToken2022 ? TOKEN_2022_PROGRAM_ID : TOKEN_PROGRAM_ID,
mint: outputMint,
notUseTokenAccount: isOutputSol,
owner: this.scope.ownerPubKey,
skipCloseAccount: true,
createInfo: {
payer: this.scope.ownerPubKey,
amount: 0,
},
associatedOnly: isOutputSol ? false : ownerInfo.associatedOnly,
checkCreateATAOwner: ownerInfo.checkCreateATAOwner,
});
destinationAcc = account!;
instructionParams && txBuilder.addInstruction(instructionParams);
}
if (isOutputSol) {
txBuilder.addInstruction({
endInstructions: [
closeAccountInstruction({
owner: this.scope.ownerPubKey,
payer: this.scope.ownerPubKey,
tokenAccount: destinationAcc,
programId: TOKEN_PROGRAM_ID,
}),
],
endInstructionTypes: [InstructionType.CloseAccount],
});
}
let routeTokenAcc: PublicKey | undefined = undefined;
if (swapInfo.routeType === "route") {
const middleMint = swapInfo.middleToken;
routeTokenAcc = this.scope.account.getAssociatedTokenAccount(
middleMint.mint,
middleMint.isToken2022 ? TOKEN_2022_PROGRAM_ID : TOKEN_PROGRAM_ID,
);
}
const poolKeys = swapPoolKeys ? swapPoolKeys : await this.computePoolToPoolKeys({ pools: swapInfo.poolInfoList });
const swapIns = makeSwapInstruction({
routeProgram,
inputMint,
swapInfo: {
...swapInfo,
poolInfo: [...swapInfo.poolInfoList],
poolKey: poolKeys,
outputMint,
},
ownerInfo: {
wallet: this.scope.ownerPubKey,
sourceToken: sourceAcc,
routeToken: routeTokenAcc,
destinationToken: destinationAcc!,
},
});
if (swapInfo.feeConfig !== undefined) {
const checkTxBuilder = this.createTxBuilder();
checkTxBuilder.addInstruction({
instructions: [
createTransferInstruction(
sourceAcc,
swapInfo.feeConfig.feeAccount,
this.scope.ownerPubKey,
swapInfo.feeConfig.feeAmount.toNumber(),
),
],
instructionTypes: [InstructionType.TransferAmount],
});
checkTxBuilder.addInstruction(swapIns);
const { transactions } =
txVersion === TxVersion.V0 ? await checkTxBuilder.sizeCheckBuildV0() : await checkTxBuilder.sizeCheckBuild();
if (transactions.length < 2) {
txBuilder.addInstruction({
instructions: [
createTransferInstruction(
sourceAcc,
swapInfo.feeConfig.feeAccount,
this.scope.ownerPubKey,
swapInfo.feeConfig.feeAmount.toNumber(),
),
],
instructionTypes: [InstructionType.TransferAmount],
});
}
}
txBuilder.addInstruction(swapIns);
if (txVersion === TxVersion.V0)
return txBuilder.sizeCheckBuildV0({ computeBudgetConfig, address: swapIns.address }) as Promise<
MakeMultiTxData<T>
>;
return txBuilder.sizeCheckBuild({ computeBudgetConfig, address: swapIns.address }) as Promise<MakeMultiTxData<T>>;
}
// get all amm/clmm/cpmm pools data only with id and mint
public async fetchRoutePoolBasicInfo(programIds?: { amm: PublicKey; clmm: PublicKey; cpmm: PublicKey }): Promise<{
ammPools: BasicPoolInfo[];
clmmPools: BasicPoolInfo[];
cpmmPools: BasicPoolInfo[];
}> {
const { amm = AMM_V4, clmm = CLMM_PROGRAM_ID, cpmm = CREATE_CPMM_POOL_PROGRAM } = programIds || {};
const ammPoolsData = await this.scope.connection.getProgramAccounts(amm, {
dataSlice: { offset: liquidityStateV4Layout.offsetOf("baseMint"), length: 64 },
});
const layoutAmm = struct([publicKey("baseMint"), publicKey("quoteMint")]);
const ammData = ammPoolsData.map((data) => ({
id: data.pubkey,
version: 4,
mintA: layoutAmm.decode(data.account.data).baseMint,
mintB: layoutAmm.decode(data.account.data).quoteMint,
}));
const layout = struct([publicKey("mintA"), publicKey("mintB")]);
const clmmPoolsData = await this.scope.connection.getProgramAccounts(clmm, {
filters: [{ dataSize: PoolInfoLayout.span }],
dataSlice: { offset: PoolInfoLayout.offsetOf("mintA"), length: 64 },
});
const clmmData = clmmPoolsData.map((data) => {
const clmm = layout.decode(data.account.data);
return {
id: data.pubkey,
version: 6,
mintA: clmm.mintA,
mintB: clmm.mintB,
};
});
const cpmmPools = await this.scope.connection.getProgramAccounts(cpmm, {
dataSlice: { offset: CpmmPoolInfoLayout.offsetOf("mintA"), length: 64 },
});
const cpmmData = cpmmPools.map((data) => {
const clmm = layout.decode(data.account.data);
return {
id: data.pubkey,
version: 7,
mintA: clmm.mintA,
mintB: clmm.mintB,
};
});
return {
clmmPools: clmmData,
ammPools: ammData,
cpmmPools: cpmmData,
};
}
// get pools with in routes
public getAllRoute({
inputMint,
outputMint,
clmmPools,
ammPools,
cpmmPools,
}: {
inputMint: PublicKey;
outputMint: PublicKey;
clmmPools: BasicPoolInfo[];
ammPools: BasicPoolInfo[];
cpmmPools: BasicPoolInfo[];
}): ReturnTypeGetAllRoute {
inputMint = inputMint.toString() === PublicKey.default.toString() ? WSOLMint : inputMint;
outputMint = outputMint.toString() === PublicKey.default.toString() ? WSOLMint : outputMint;
const needSimulate: { [poolKey: string]: BasicPoolInfo } = {};
const needTickArray: { [poolKey: string]: BasicPoolInfo } = {};
const cpmmPoolList: { [poolKey: string]: BasicPoolInfo } = {};
const directPath: BasicPoolInfo[] = [];
const routePathDict: RoutePathType = {}; // {[route mint: string]: {in: [] , out: []}}
for (const itemClmmPool of clmmPools ?? []) {
if (
(itemClmmPool.mintA.equals(inputMint) && itemClmmPool.mintB.equals(outputMint)) ||
(itemClmmPool.mintA.equals(outputMint) && itemClmmPool.mintB.equals(inputMint))
) {
directPath.push(itemClmmPool);
needTickArray[itemClmmPool.id.toString()] = itemClmmPool;
}
if (itemClmmPool.mintA.equals(inputMint)) {
const t = itemClmmPool.mintB.toString();
if (routePathDict[t] === undefined)
routePathDict[t] = {
mintProgram: TOKEN_PROGRAM_ID, // to fetch later
in: [],
out: [],
mDecimals: 0, // to fetch later
};
routePathDict[t].in.push(itemClmmPool);
}
if (itemClmmPool.mintB.equals(inputMint)) {
const t = itemClmmPool.mintA.toString();
if (routePathDict[t] === undefined)
routePathDict[t] = {
mintProgram: TOKEN_PROGRAM_ID, // to fetch later
in: [],
out: [],
mDecimals: 0, // to fetch later
};
routePathDict[t].in.push(itemClmmPool);
}
if (itemClmmPool.mintA.equals(outputMint)) {
const t = itemClmmPool.mintB.toString();
if (routePathDict[t] === undefined)
routePathDict[t] = {
mintProgram: TOKEN_PROGRAM_ID, // to fetch later
in: [],
out: [],
mDecimals: 0, // to fetch later
};
routePathDict[t].out.push(itemClmmPool);
}
if (itemClmmPool.mintB.equals(outputMint)) {
const t = itemClmmPool.mintA.toString();
if (routePathDict[t] === undefined)
routePathDict[t] = {
mintProgram: TOKEN_PROGRAM_ID, // to fetch later
in: [],
out: [],
mDecimals: 0, // to fetch later
};
routePathDict[t].out.push(itemClmmPool);
}
}
const addLiquidityPools: BasicPoolInfo[] = [];
for (const itemAmmPool of ammPools) {
if (
(itemAmmPool.mintA.equals(inputMint) && itemAmmPool.mintB.equals(outputMint)) ||
(itemAmmPool.mintA.equals(outputMint) && itemAmmPool.mintB.equals(inputMint))
) {
directPath.push(itemAmmPool);
needSimulate[itemAmmPool.id.toBase58()] = itemAmmPool;
addLiquidityPools.push(itemAmmPool);
}
if (itemAmmPool.mintA.equals(inputMint)) {
if (routePathDict[itemAmmPool.mintB.toBase58()] === undefined)
routePathDict[itemAmmPool.mintB.toBase58()] = {
mintProgram: TOKEN_PROGRAM_ID,
in: [],
out: [],
mDecimals: 0, // to fetch later
};
routePathDict[itemAmmPool.mintB.toBase58()].in.push(itemAmmPool);
}
if (itemAmmPool.mintB.equals(inputMint)) {
if (routePathDict[itemAmmPool.mintA.toBase58()] === undefined)
routePathDict[itemAmmPool.mintA.toBase58()] = {
mintProgram: TOKEN_PROGRAM_ID,
in: [],
out: [],
mDecimals: 0, // to fetch later
};
routePathDict[itemAmmPool.mintA.toBase58()].in.push(itemAmmPool);
}
if (itemAmmPool.mintA.equals(outputMint)) {
if (routePathDict[itemAmmPool.mintB.toBase58()] === undefined)
routePathDict[itemAmmPool.mintB.toBase58()] = {
mintProgram: TOKEN_PROGRAM_ID,
in: [],
out: [],
mDecimals: 0, // to fetch later
};
routePathDict[itemAmmPool.mintB.toBase58()].out.push(itemAmmPool);
}
if (itemAmmPool.mintB.equals(outputMint)) {
if (routePathDict[itemAmmPool.mintA.toBase58()] === undefined)
routePathDict[itemAmmPool.mintA.toBase58()] = {
mintProgram: TOKEN_PROGRAM_ID,
in: [],
out: [],
mDecimals: 0, // to fetch later
};
routePathDict[itemAmmPool.mintA.toBase58()].out.push(itemAmmPool);
}
}
for (const itemCpmmPool of cpmmPools) {
if (
(itemCpmmPool.mintA.equals(inputMint) && itemCpmmPool.mintB.equals(outputMint)) ||
(itemCpmmPool.mintA.equals(outputMint) && itemCpmmPool.mintB.equals(inputMint))
) {
directPath.push(itemCpmmPool);
cpmmPoolList[itemCpmmPool.id.toBase58()] = itemCpmmPool;
}
if (itemCpmmPool.mintA.equals(inputMint)) {
if (routePathDict[itemCpmmPool.mintB.toBase58()] === undefined)
routePathDict[itemCpmmPool.mintB.toBase58()] = {
mintProgram: TOKEN_PROGRAM_ID,
in: [],
out: [],
mDecimals: 0, // to fetch later
};
routePathDict[itemCpmmPool.mintB.toBase58()].in.push(itemCpmmPool);
}
if (itemCpmmPool.mintB.equals(inputMint)) {
if (routePathDict[itemCpmmPool.mintA.toBase58()] === undefined)
routePathDict[itemCpmmPool.mintA.toBase58()] = {
mintProgram: TOKEN_PROGRAM_ID,
in: [],
out: [],
mDecimals: 0, // to fetch later
};
routePathDict[itemCpmmPool.mintA.toBase58()].in.push(itemCpmmPool);
}
if (itemCpmmPool.mintA.equals(outputMint)) {
if (routePathDict[itemCpmmPool.mintB.toBase58()] === undefined)
routePathDict[itemCpmmPool.mintB.toBase58()] = {
mintProgram: TOKEN_PROGRAM_ID,
in: [],
out: [],
mDecimals: 0, // to fetch later
};
routePathDict[itemCpmmPool.mintB.toBase58()].out.push(itemCpmmPool);
}
if (itemCpmmPool.mintB.equals(outputMint)) {
if (routePathDict[itemCpmmPool.mintA.toBase58()] === undefined)
routePathDict[itemCpmmPool.mintA.toBase58()] = {
mintProgram: TOKEN_PROGRAM_ID,
in: [],
out: [],
mDecimals: 0, // to fetch later
};
routePathDict[itemCpmmPool.mintA.toBase58()].out.push(itemCpmmPool);
}
}
for (const t of Object.keys(routePathDict)) {
if (
routePathDict[t].in.length === 1 &&
routePathDict[t].out.length === 1 &&
routePathDict[t].in[0].id.equals(routePathDict[t].out[0].id)
) {
delete routePathDict[t];
continue;
}
if (routePathDict[t].in.length === 0 || routePathDict[t].out.length === 0) {
delete routePathDict[t];
continue;
}
const info = routePathDict[t];
for (const infoIn of info.in) {
for (const infoOut of info.out) {
if (infoIn.version === 6 && needTickArray[infoIn.id.toString()] === undefined) {
needTickArray[infoIn.id.toString()] = infoIn;
} else if (infoIn.version === 7 && cpmmPoolList[infoIn.id.toString()] === undefined) {
cpmmPoolList[infoIn.id.toString()] = infoIn;
} else if (
(infoIn.version === 4 || infoIn.version === 5) &&
needSimulate[infoIn.id.toString()] === undefined
) {
needSimulate[infoIn.id.toString()] = infoIn;
}
if (infoOut.version === 6 && needTickArray[infoOut.id.toString()] === undefined) {
needTickArray[infoOut.id.toString()] = infoOut;
} else if (infoOut.version === 7 && cpmmPoolList[infoOut.id.toString()] === undefined) {
cpmmPoolList[infoOut.id.toString()] = infoOut;
} else if (
(infoOut.version === 4 || infoOut.version === 5) &&
needSimulate[infoOut.id.toString()] === undefined
) {
needSimulate[infoOut.id.toString()] = infoOut;
}
}
}
}
return {
directPath,
addLiquidityPools,
routePathDict,
needSimulate: Object.values(needSimulate),
needTickArray: Object.values(needTickArray),
cpmmPoolList: Object.values(cpmmPoolList),
};
}
// fetch pools detail info in route
public async fetchSwapRoutesData({
routes,
inputMint,
outputMint,
}: {
inputMint: string | PublicKey;
outputMint: string | PublicKey;
routes: ReturnTypeGetAllRoute;
}): Promise<{
mintInfos: ReturnTypeFetchMultipleMintInfos;
ammPoolsRpcInfo: Record<string, AmmRpcData>;
ammSimulateCache: Record<string, ComputeAmountOutParam["poolInfo"]>;
clmmPoolsRpcInfo: Record<string, ClmmRpcData>;
computeClmmPoolInfo: Record<string, ComputeClmmPoolInfo>;
computePoolTickData: ReturnTypeFetchMultiplePoolTickArrays;
computeCpmmData: Record<string, CpmmComputeData>;
routePathDict: ComputeRoutePathType;
}> {
const mintSet = new Set([
...routes.needTickArray.map((p) => [p.mintA.toBase58(), p.mintB.toBase58()]).flat(),
inputMint.toString(),
outputMint.toString(),
]);
console.log("fetching amm pools info, total: ", routes.needSimulate.length);
const ammPoolsRpcInfo = await this.scope.liquidity.getRpcPoolInfos(routes.needSimulate.map((p) => p.id));
const ammSimulateCache = toAmmComputePoolInfo(ammPoolsRpcInfo);
let mintInfos: ReturnTypeFetchMultipleMintInfos = {};
// amm doesn't support token2022 yet, so don't need to fetch mint info
Object.values(ammSimulateCache).forEach((p) => {
mintSet.delete(p.mintA.address);
mintInfos[p.mintA.address] = {
address: new PublicKey(p.mintA.address),
programId: TOKEN_PROGRAM_ID,
mintAuthority: null,
supply: BigInt(0),
decimals: p.mintA.decimals,
isInitialized: true,
freezeAuthority: null,
tlvData: Buffer.from("0", "hex"),
feeConfig: undefined,
};
mintSet.delete(p.mintB.address);
mintInfos[p.mintB.address] = {
address: new PublicKey(p.mintB.address),
programId: TOKEN_PROGRAM_ID,
mintAuthority: null,
supply: BigInt(0),
decimals: p.mintB.decimals,
isInitialized: true,
freezeAuthority: null,
tlvData: Buffer.from("0", "hex"),
feeConfig: undefined,
};
});
console.log("fetching cpmm pools info, total: ", routes.cpmmPoolList.length);
const cpmmPoolsRpcInfo = await this.scope.cpmm.getRpcPoolInfos(
routes.cpmmPoolList.map((p) => p.id.toBase58()),
true,
);
Object.values(cpmmPoolsRpcInfo).forEach((p) => {
const [mintA, mintB] = [p.mintA.toBase58(), p.mintB.toBase58()];
if (p.mintProgramA.equals(TOKEN_PROGRAM_ID)) {
mintSet.delete(mintA);
mintInfos[mintA] = {
address: p.mintA,
programId: p.mintProgramA,
mintAuthority: null,
supply: BigInt(0),
decimals: p.mintDecimalA,
isInitialized: true,
freezeAuthority: null,
tlvData: Buffer.from("0", "hex"),
feeConfig: undefined,
};
} else mintSet.add(mintA); // 2022, need to fetch fee config
if (p.mintProgramB.equals(TOKEN_PROGRAM_ID)) {
mintSet.delete(mintB);
mintInfos[mintB] = {
address: p.mintB,
programId: p.mintProgramB,
mintAuthority: null,
supply: BigInt(0),
decimals: p.mintDecimalB,
isInitialized: true,
freezeAuthority: null,
tlvData: Buffer.from("0", "hex"),
feeConfig: undefined,
};
} else mintSet.add(mintB); // 2022, need to fetch fee config
});
console.log("fetching mints info, total: ", mintSet.size);
const fetchMintInfoRes = await fetchMultipleMintInfos({
connection: this.scope.connection,
mints: Array.from(mintSet).map((m) => new PublicKey(m)),
});
mintInfos = {
...mintInfos,
...fetchMintInfoRes,
};
const computeCpmmData = this.scope.cpmm.toComputePoolInfos({
pools: cpmmPoolsRpcInfo,
mintInfos,
});
console.log("fetching clmm pools info, total:", routes.needTickArray.length);
const clmmPoolsRpcInfo = await this.scope.clmm.getRpcClmmPoolInfos({
poolIds: routes.needTickArray.map((p) => p.id),
});
const { computeClmmPoolInfo, computePoolTickData } = await this.scope.clmm.getComputeClmmPoolInfos({
clmmPoolsRpcInfo,
mintInfos,
});
// update route pool mint info
const routePathDict = Object.keys(routes.routePathDict).reduce((acc, cur) => {
return {
...acc,
[cur]: {
...routes.routePathDict[cur],
mintProgram: mintInfos[cur].programId,
mDecimals: mintInfos[cur].decimals,
in: routes.routePathDict[cur].in.map(
(p) =>
ammSimulateCache[p.id.toBase58()] ||
computeClmmPoolInfo[p.id.toBase58()] ||
computeCpmmData[p.id.toBase58()],
),
out: routes.routePathDict[cur].out.map(
(p) =>
ammSimulateCache[p.id.toBase58()] ||
computeClmmPoolInfo[p.id.toBase58()] ||
computeCpmmData[p.id.toBase58()],
),
},
};
}, {} as ComputeRoutePathType);
return {
mintInfos,
ammPoolsRpcInfo,
ammSimulateCache,
clmmPoolsRpcInfo,
computeClmmPoolInfo,
computePoolTickData,
computeCpmmData,
routePathDict,
};
}
// compute amount from routes
public getAllRouteComputeAmountOut({
inputTokenAmount,
outputToken: propOutputToken,
directPath,
routePathDict,
simulateCache,
tickCache,
slippage,
chainTime,
epochInfo,
feeConfig,
}: {
directPath: ComputePoolType[];
routePathDict: ComputeRoutePathType;
simulateCache: ReturnTypeFetchMultipleInfo;
tickCache: ReturnTypeFetchMultiplePoolTickArrays;
mintInfos: ReturnTypeFetchMultipleMintInfos;
inputTokenAmount: TokenAmount;
outputToken: ApiV3Token;
slippage: number;
chainTime: number;
epochInfo: EpochInfo;
feeConfig?: {
feeBps: BN;
feeAccount: PublicKey;
};
}): ComputeAmountOutLayout[] {
const _amountInFee =
feeConfig === undefined
? new BN(0)
: inputTokenAmount.raw.mul(new BN(feeConfig.feeBps.toNumber())).div(new BN(10000));
const _amoutIn = inputTokenAmount.raw.sub(_amountInFee);
const amountIn = new TokenAmount(inputTokenAmount.token, _amoutIn);
const _inFeeConfig =
feeConfig === undefined
? undefined
: {
feeAmount: _amountInFee,
feeAccount: feeConfig.feeAccount,
};
const outputToken = {
...propOutputToken,
address: solToWSol(propOutputToken.address).toString(),
};
const outRoute: ComputeAmountOutLayout[] = [];
for (const itemPool of directPath) {
try {
outRoute.push({
...this.computeAmountOut({
itemPool,
tickCache,
simulateCache,
chainTime,
epochInfo,
slippage,
outputToken,
amountIn,
}),
feeConfig: _inFeeConfig,
});
} catch (e: any) {
this.logDebug("direct error", itemPool.version, itemPool.id.toString(), e.message);
/* empty */
}
}
this.logDebug("direct done");
for (const [routeMint, info] of Object.entries(routePathDict)) {
// const routeToken = new Token(info.mintProgram, routeMint, info.mDecimals);
const routeToken = {
chainId: 101,
address: routeMint,
programId: info.mintProgram.toBase58(),
logoURI: "",
symbol: "",
name: "",
decimals: info.mDecimals,
tags: [],
extensions: {},
};
const maxFirstIn = info.in
.map((i) => {
try {
return {
pool: i,
data: this.computeAmountOut({
itemPool: i,
tickCache,
simulateCache,
chainTime,
epochInfo,
slippage,
outputToken: routeToken,
amountIn,
}),
};
} catch (e: any) {
this.logDebug("route in error", i.version, i.id.toString(), e.message);
return undefined;
}
})
.sort((_a, _b) => {
const a = _a === undefined ? ZERO : _a.data.amountOut.amount.raw.sub(_a.data.amountOut.fee?.raw ?? ZERO);
const b = _b === undefined ? ZERO : _b.data.amountOut.amount.raw.sub(_b.data.amountOut.fee?.raw ?? ZERO);
return a.lt(b) ? 1 : -1;
})[0];
if (maxFirstIn === undefined) continue;
const routeAmountIn = new TokenAmount(
toToken(routeToken),
maxFirstIn.data.amountOut.amount.raw.sub(maxFirstIn.data.amountOut.fee?.raw ?? ZERO),
);
for (const iOutPool of info.out) {
try {
const outC = this.computeAmountOut({
itemPool: iOutPool,
tickCache,
simulateCache,
chainTime,
epochInfo,
slippage,
outputToken,
amountIn: routeAmountIn,
});
outRoute.push({
...outC,
allTrade: maxFirstIn.data.allTrade && outC.allTrade ? true : false,
amountIn: maxFirstIn.data.amountIn,
amountOut: outC.amountOut,
minAmountOut: outC.minAmountOut,
currentPrice: undefined,
executionPrice: new Decimal(
new Price({
baseToken: maxFirstIn.data.amountIn.amount.token,
denominator: maxFirstIn.data.amountIn.amount.raw,
quoteToken: outC.amountOut.amount.token,
numerator: outC.amountOut.amount.raw.sub(outC.amountOut.fee?.raw ?? ZERO),
}).toFixed(),
),
priceImpact: new Decimal(maxFirstIn.data.priceImpact.add(outC.priceImpact).toFixed()),
fee: [maxFirstIn.data.fee[0], outC.fee[0]],
routeType: "route",
poolInfoList: [maxFirstIn.pool, iOutPool],
remainingAccounts: [maxFirstIn.data.remainingAccounts[0], outC.remainingAccounts[0]],
minMiddleAmountFee: outC.amountOut.fee?.raw
? new TokenAmount(
(maxFirstIn.data.amountOut.amount as TokenAmount).token,
(maxFirstIn.data.amountOut.fee?.raw ?? ZERO).add(outC.amountOut.fee?.raw ?? ZERO),
)
: undefined,
middleToken: (maxFirstIn.data.amountOut.amount as TokenAmount).token,
poolReady: maxFirstIn.data.poolReady && outC.poolReady,
poolType: [maxFirstIn.data.poolType, outC.poolType],
feeConfig: _inFeeConfig,
expirationTime: minExpirationTime(maxFirstIn.data.expirationTime, outC.expirationTime),
});
} catch (e: any) {
this.logDebug("route out error", iOutPool.version, iOutPool.id.toString(), e.message);
/* empty */
}
}
}
return outRoute
.filter((i) => {
if (!i.allTrade)
this.logDebug(`pool ${i.poolInfoList.map((p) => p.id.toString()).join(",")} filter out since not all trade`);
return i.allTrade;
})
.sort((a, b) => (a.amountOut.amount.raw.sub(b.amountOut.amount.raw).gt(ZERO) ? -1 : 1));
}
/** trade related utils */
private computeAmountOut({
itemPool,
tickCache,
simulateCache,
chainTime,
epochInfo,
slippage,
outputToken,
amountIn,
}: {
itemPool: ComputePoolType;
tickCache: ReturnTypeFetchMultiplePoolTickArrays;
simulateCache: ReturnTypeFetchMultipleInfo;
chainTime: number;
epochInfo: EpochInfo;
amountIn: TokenAmount;
outputToken: ApiV3Token;
slippage: number;
}): ComputeAmountOutAmmLayout {
if (itemPool.version === 6) {
const {
allTrade,
realAmountIn,
amountOut,
minAmountOut,
expirationTime,
currentPrice,
executionPrice,
priceImpact,
fee,
remainingAccounts,
executionPriceX64,
} = PoolUtils.computeAmountOutFormat({
poolInfo: itemPool,
tickArrayCache: tickCache[itemPool.id.toString()],
amountIn: amountIn.raw,
tokenOut: outputToken,
slippage,
epochInfo,
catchLiquidityInsufficient: true,
});
return {
allTrade,
amountIn: realAmountIn,
amountOut,
minAmountOut,
currentPrice: new Decimal(currentPrice.toFixed()),
executionPrice: new Decimal(executionPrice.toFixed()),
priceImpact: new Decimal(priceImpact.toFixed()),
fee: [fee],
remainingAccounts: [remainingAccounts],
routeType: "amm",
poolInfoList: [itemPool],
poolReady: itemPool.startTime < chainTime,
poolType: "CLMM",
slippage,
clmmExPriceX64: [executionPriceX64],
expirationTime: minExpirationTime(realAmountIn.expirationTime, expirationTime),
};
} else if (itemPool.version === 7) {
const { allTrade, executionPrice, amountOut, minAmountOut, priceImpact, fee } = this.scope.cpmm.computeSwapAmount(
{
pool: itemPool,
outputMint: outputToken.address,
amountIn: amountIn.raw,
slippage,
},
);
return {
allTrade,
amountIn: { amount: amountIn, fee: undefined, expirationTime: undefined },
amountOut: {
amount: toTokenAmount({
...outputToken,
amount: amountOut,
}),
fee: undefined,
expirationTime: undefined,
},
minAmountOut: {
amount: toTokenAmount({
...outputToken,
amount: minAmountOut,
}),
fee: undefined,
expirationTime: undefined,
},
currentPrice: itemPool.poolPrice,
executionPrice,
priceImpact,
fee: [new TokenAmount(amountIn.token, fee)],
remainingAccounts: [],
routeType: "amm",
poolInfoList: [itemPool],
poolReady: itemPool.openTime.toNumber() < chainTime,
poolType: "CPMM",
slippage,
clmmExPriceX64: [undefined],
expirationTime: undefined,
};
} else {
if (![1, 6, 7].includes(simulateCache[itemPool.id.toString()].status)) throw Error("swap error");
const { amountOut, minAmountOut, currentPrice, executionPrice, priceImpact, fee } =
this.scope.liquidity.computeAmountOut({
poolInfo: simulateCache[itemPool.id.toString()],
amountIn: amountIn.raw,
mintIn: amountIn.token.mint,
mintOut: outputToken.address,
slippage,
});
return {
amountIn: { amount: amountIn, fee: undefined, expirationTime: undefined },
amountOut: {
amount: toTokenAmount({
...outputToken,
amount: amountOut,
}),
fee: undefined,
expirationTime: undefined,
},
minAmountOut: {
amount: toTokenAmount({
...outputToken,
amount: minAmountOut,
}),
fee: undefined,
expirationTime: undefined,
},
currentPrice,
executionPrice,
priceImpact,
fee: [new TokenAmount(amountIn.token, fee)],
routeType: "amm",
poolInfoList: [itemPool],
remainingAccounts: [],
poolReady: Number(simulateCache[itemPool.id as string].openTime) < chainTime,
poolType: itemPool.version === 5 ? "STABLE" : undefined,
expirationTime: undefined,
allTrade: true,
slippage,
clmmExPriceX64: [undefined],
};
}
}
public async computePoolToPoolKeys({
pools,
clmmRpcData = {},
ammRpcData = {},
}: {
pools: ComputePoolType[];
clmmRpcData?: Record<string, ClmmRpcData>;
ammRpcData?: Record<string, AmmRpcData>;
}): Promise<PoolKeys[]> {
const clmmFetchKeys = new Set(
pools.filter((p) => p.version === 6 && !clmmRpcData[p.id.toString()]).map((p) => p.id.toString()),
);
if (clmmFetchKeys.size > 0) {
const clmmData = await this.scope.clmm.getRpcClmmPoolInfos({ poolIds: Array.from(clmmFetchKeys) });
Object.keys(clmmData).forEach((poolId) => {
clmmRpcData[poolId] = clmmData[poolId];
});
}
const ammFetchKeys = new Set(
pools.filter((p) => p.version === 4 && !ammRpcData[p.id.toString()]).map((p) => p.id.toString()),
);
if (ammFetchKeys.size > 0) {
const ammData = await this.scope.liquidity.getRpcPoolInfos(Array.from(ammFetchKeys));
Object.keys(ammData).forEach((poolId) => {
ammRpcData[poolId] = ammData[poolId];
});
}
const ammMarketFetchKeys = new Set(
pools.filter((p) => p.version === 4).map((p) => (p as ComputeAmountOutParam["poolInfo"]).marketId),
);
const marketData: Record<
string,
{
marketProgramId: string;
marketId: string;
marketAuthority: string;
marketBaseVault: string;
marketQuoteVault: string;
marketBids: string;
marketAsks: string;
marketEventQueue: string;
}
> = {};
if (ammMarketFetchKeys.size > 0) {
const marketAccount = await getMultipleAccountsInfoWithCustomFlags(
this.scope.connection,
Array.from(ammMarketFetchKeys).map((p) => ({ pubkey: new PublicKey(p) })),
);
marketAccount.forEach((m) => {
if (!m.accountInfo) return;
const itemMarketInfo = MARKET_STATE_LAYOUT_V3.decode(m.accountInfo.data);
marketData[m.pubkey.toBase58()] = {
marketId: m.pubkey.toString(),
marketProgramId: m.accountInfo.owner.toString(),
marketAuthority: Market.getAssociatedAuthority({
programId: m.accountInfo.owner,
marketId: m.pubkey,
}).publicKey.toString(),
marketBaseVault: itemMarketInfo.baseVault.toString(),
marketQuoteVault: itemMarketInfo.quoteVault.toString(),
marketBids: itemMarketInfo.bids.toString(),
marketAsks: itemMarketInfo.asks.toString(),
marketEventQueue: itemMarketInfo.eventQueue.toString(),
};
});
}
const poolKeys: PoolKeys[] = [];
pools.forEach((pool) => {
if (pool.version === 6) {
const rpcInfo = clmmRpcData[pool.id.toString()];
const clmmKeys: ClmmKeys = {
programId: pool.programId.toBase58(),
id: pool.id.toBase58(),
mintA: pool.mintA,
mintB: pool.mintB,
openTime: String(pool.startTime),
vault: {
A: rpcInfo.vaultA.toBase58(),
B: rpcInfo.vaultB.toBase58(),
},
config: {
...pool.ammConfig,
id: pool.ammConfig.id.toString(),
defaultRange: 0,
defaultRangePoint: [],
},
rewardInfos: [],
observationId: pool.observationId.toBase58(),
exBitmapAccount: pool.exBitmapAccount.toBase58(),
};
poolKeys.push(clmmKeys);
} else if (pool.version === 4) {
const rpcInfo = ammRpcData[pool.id.toString()];
const ammKeys: AmmV4Keys = {
programId: pool.programId,
id: pool.id,
mintA: pool.mintA,
mintB: pool.mintB,
openTime: String(pool.openTime),
vault: {
A: rpcInfo.baseVault.toBase58(),
B: rpcInfo.quoteVault.toBase58(),
},
authority: getLiquidityAssociatedAuthority({ programId: new PublicKey(pool.programId) }).publicKey.toString(),
openOrders: rpcInfo.openOrders.toBase58(),
targetOrders: rpcInfo.targetOrders.toBase58(),
mintLp: pool.lpMint,
...marketData[pool.marketId],
};
poolKeys.push(ammKeys);
} else if (pool.version === 7) {
poolKeys.push({
observationId: pool.observationId.toBase58(),
programId: pool.programId.toBase58(),
id: pool.id.toBase58(),
mintA: pool.mintA,
mintB: pool.mintB,
openTime: String(pool.openTime),
authority: getPdaPoolAuthority(pool.programId).publicKey.toBase58(),
vault: {
A: pool.vaultA.toBase58(),
B: pool.vaultB.toBase58(),
},
mintLp: toApiV3Token({
address: pool.mintLp.toBase58(),
programId: TOKEN_PROGRAM_ID.toBase58(),
decimals: pool.lpDecimals,
}),
config: {
id: pool.configId.toBase58(),
...pool.configInfo,
protocolFeeRate: pool.configInfo.protocolFeeRate.toNumber(),
tradeFeeRate: pool.configInfo.tradeFeeRate.toNumber(),
fundFeeRate: pool.configInfo.fundFeeRate.toNumber(),
createPoolFee: pool.configInfo.createPoolFee.toString(),
},
});
}
});
return poolKeys;
}
}