goosefx-amm-sdk
Version:
SDK for the GooseFx AMM
1,316 lines (1,186 loc) • 47.6 kB
text/typescript
import { PublicKey, SystemProgram } from "@solana/web3.js";
import { NATIVE_MINT, TOKEN_PROGRAM_ID, createSyncNativeInstruction } from "@solana/spl-token";
import { PoolInfo, PoolKeys, PoolStats } from "@/api/type";
import { Percent } from "@/module";
import { BN_ZERO } from "@/common/number";
import { WSOLMint } from "@/common/pubKey";
import { InstructionType, TxVersion } from "@/common/txTool/txType";
import { MakeTxData } from "@/common/txTool/txTool";
import { CurveCalculator, RoundDirection } from "./curve/calculator";
import ModuleBase, { ModuleBaseProps } from "../moduleBase";
import {
CreateCpmmPoolParam,
CreateCpmmPoolAddress,
AddCpmmLiquidityParams,
WithdrawCpmmLiquidityParams,
CpmmSwapParams,
ComputePairAmountParams,
CpmmRpcData,
CpmmComputeData,
UserLiquidityAccount,
CpmmObservationState,
CpmmPool,
CpmmPoolPartners,
CpmmConfig,
} from "./type";
import { getCreatePoolKeys, getPdaObservationId, getPdaPoolPartners, getPdaUserLiquidity } from "./pda";
import {
makeCreateCpmmPoolInInstruction,
makeDepositCpmmInInstruction,
makeInitUserPoolLiquidityInstruction,
makeWithdrawCpmmInInstruction,
makeSwapCpmmBaseInInstruction,
makeSwapCpmmBaseOutInstruction,
makeSwapCpmmOracleBaseInInstruction,
makeInitializePartnerInstruction,
makeAddPartnerInstruction,
} from "./instruction";
import BN from "bn.js";
import Decimal from "decimal.js";
import { fetchMultipleMintInfos, getTransferAmountFeeV2 } from "@/common";
import { ComputeBudgetConfig, GetTransferAmountFee, ReturnTypeFetchMultipleMintInfos } from "@/gfx/type";
import { toGammaApiToken, toFeeConfig, SOL_INFO } from "../token";
import { getPdaPoolAuthority } from "./pda";
import { ConstantProductCurve } from "./curve/constantProduct";
import { Program } from "@coral-xyz/anchor";
import { Gamma } from "../idl/gamma.type";
import { getReserveAccountsForWithdraw, getReservesForMarket, KaminoReserve } from "./kamino";
export default class CpmmModule extends ModuleBase {
private program: Program<Gamma>;
private kamino?: {
market?: PublicKey;
programId?: PublicKey;
};
/** map of liquidity mint to kamino reserve */
private reserves: Map<string, KaminoReserve>;
constructor(params: ModuleBaseProps) {
super(params);
this.program = params.scope.program;
this.kamino = params.scope.kamino;
this.reserves = new Map();
}
public async load(): Promise<void> {
this.checkDisabled();
}
public async getCpmmPoolKeys(poolId: string): Promise<PoolKeys> {
return ((await this.scope.api.fetchPoolKeysById({ idList: [poolId] })) as PoolKeys[])[0];
}
public async getRpcPoolInfo(poolId: string, fetchConfigInfo?: boolean): Promise<CpmmRpcData> {
return (await this.getRpcPoolInfos([poolId], fetchConfigInfo))[poolId];
}
public async getTotalDepositsByPartner(
poolIds: string[],
partner: PublicKey,
): Promise<{
[poolId: string]: {
totalInvestedThoughPartnerToken0: BN;
totalInvestedThoughPartnerToken1: BN;
totalLpTokensLinked: BN;
};
}> {
const partnerIds = poolIds.map((id) => getPdaPoolPartners(this.program.programId, new PublicKey(id)).publicKey);
const poolInfos = await this.program.account.poolState.fetchMultiple(poolIds);
const partnerInfos = await this.program.account.poolPartnerInfos.fetchMultiple(partnerIds);
const returnData: {
[poolId: string]: {
totalInvestedThoughPartnerToken0: BN;
totalInvestedThoughPartnerToken1: BN;
totalLpTokensLinked: BN;
};
} = {};
for (let i = 0; i < poolIds.length; i++) {
const pool = poolInfos[i];
const partners = partnerInfos[i];
if (pool === null) throw Error("fetch pool info error: " + poolIds[i]);
if (partners === null) throw Error("fetch poolPartners info error: " + partnerIds[i]);
const { token0, token1, lp } = await this.getTokenInvestedByPartner(pool, partners, partner);
returnData[poolIds[i]] = {
totalInvestedThoughPartnerToken0: token0,
totalInvestedThoughPartnerToken1: token1,
totalLpTokensLinked: lp,
};
}
return returnData;
}
public async getTokenInvestedByPartner(
poolInfo: CpmmPool,
partnerInfo: CpmmPoolPartners,
partner: PublicKey,
): Promise<{ token0: BN; token1: BN; lp: BN }> {
const lpTokensLinked =
partnerInfo.infos.find((info) => info.partner.toBase58() === partner.toBase58())?.lpTokenLinkedWithPartner ??
new BN(0);
if (lpTokensLinked.eqn(0)) {
return { token0: new BN(0), token1: new BN(0), lp: new BN(0) };
}
const lpToTradingToken = ConstantProductCurve.lpTokensToTradingTokens(
lpTokensLinked,
poolInfo.lpSupply,
poolInfo.token0VaultAmount,
poolInfo.token1VaultAmount,
RoundDirection.Floor,
);
return {
token0: lpToTradingToken.tokenAmount0,
token1: lpToTradingToken.tokenAmount1,
lp: lpTokensLinked,
};
}
public async getRpcPoolInfos(
poolIds: string[],
fetchConfigInfo?: boolean,
): Promise<{
[poolId: string]: CpmmRpcData;
}> {
const partnerIds = poolIds.map((id) => getPdaPoolPartners(this.program.programId, new PublicKey(id)).publicKey);
const observationIds = poolIds.map(
(id) => getPdaObservationId(this.program.programId, new PublicKey(id)).publicKey,
);
const [accounts, partnerInfos, observations] = await Promise.all([
this.program.account.poolState.fetchMultiple(poolIds),
this.program.account.poolPartnerInfos.fetchMultiple(partnerIds),
this.program.account.observationState.fetchMultiple(observationIds),
]);
const poolInfos: {
[poolId: string]: CpmmPool & {
observationAccount: CpmmObservationState;
partnerInfo: CpmmPoolPartners;
programId: PublicKey;
};
} = {};
const needFetchConfigId = new Set<string>();
for (let i = 0; i < poolIds.length; i++) {
const rpc = accounts[i];
const partnerInfo = partnerInfos[i];
const observationAccount = observations[i];
if (rpc === null) throw Error("fetch pool info error: " + String(poolIds[i]));
if (partnerInfo === null) throw Error("failed to fetch poolPartners for pool: " + poolIds[i]);
if (observationAccount === null) throw Error("failed to fetch observation for pool: " + poolIds[i]);
poolInfos[String(poolIds[i])] = {
...rpc,
partnerInfo,
observationAccount,
programId: this.program.programId,
};
needFetchConfigId.add(String(rpc.ammConfig));
}
const configInfo: { [configId: string]: CpmmConfig } = {};
if (fetchConfigInfo) {
const configIds = [...needFetchConfigId];
const configs = await this.program.account.ammConfig.fetchMultiple(configIds);
for (let i = 0; i < configIds.length; i++) {
const configItemInfo = configs[i];
if (configItemInfo === null) throw Error("fetch pool config error: " + configIds[i]);
configInfo[configIds[i]] = configItemInfo;
}
}
const returnData: { [poolId: string]: CpmmRpcData } = {};
for (const [id, info] of Object.entries(poolInfos)) {
const baseReserve = info.token0VaultAmount;
const quoteReserve = info.token1VaultAmount;
returnData[id] = {
...info,
baseReserve: info.token0VaultAmount,
quoteReserve: info.token1VaultAmount,
vaultAAmount: info.token0VaultAmount
.add(info.protocolFeesToken0)
.add(info.fundFeesToken0)
.add(info.partnerProtocolFeesToken0),
vaultBAmount: info.token1VaultAmount
.add(info.protocolFeesToken1)
.add(info.fundFeesToken1)
.add(info.partnerProtocolFeesToken1),
configInfo: configInfo[info.ammConfig.toString()],
poolPrice: new Decimal(quoteReserve.toString())
.div(new Decimal(10).pow(info.mint1Decimals))
.div(new Decimal(baseReserve.toString()).div(new Decimal(10).pow(info.mint0Decimals))),
};
}
return returnData;
}
public toComputePoolInfos({
pools,
mintInfos,
}: {
pools: Record<string, CpmmRpcData>;
mintInfos: ReturnTypeFetchMultipleMintInfos;
}): Record<string, CpmmComputeData> {
return Object.keys(pools).reduce((acc, cur) => {
const pool = pools[cur];
const [mintA, mintB] = [pool.token0Mint.toBase58(), pool.token1Mint.toBase58()];
return {
...acc,
[cur]: {
...pool,
id: new PublicKey(cur),
configInfo: pool.configInfo!,
version: 7 as const,
authority: getPdaPoolAuthority(pool.programId).publicKey,
mintA: toGammaApiToken({
address: mintA,
decimals: pool.mint0Decimals,
programId: pool.token0Program.toBase58(),
extensions: {
feeConfig: mintInfos[mintA]?.feeConfig ? toFeeConfig(mintInfos[mintA]?.feeConfig) : undefined,
},
}),
mintB: toGammaApiToken({
address: mintB,
decimals: pool.mint1Decimals,
programId: pool.token1Program.toBase58(),
extensions: {
feeConfig: mintInfos[mintB]?.feeConfig ? toFeeConfig(mintInfos[mintB]?.feeConfig) : undefined,
},
}),
},
};
}, {} as Record<string, CpmmComputeData>);
}
public async getPoolInfoFromRpc(poolId: string): Promise<{
poolInfo: PoolInfo;
poolKeys: PoolKeys;
rpcData: CpmmRpcData;
}> {
const rpcData = await this.getRpcPoolInfo(poolId, true);
const partnerKeys = rpcData
.partnerInfo!.infos.map((i) => i.partner.toBase58())
.filter((i) => i !== PublicKey.default.toBase58());
const partners =
partnerKeys.length > 0
? await this.program.account.partner.fetchMultiple(partnerKeys).then((res) =>
res
.filter((r) => r !== null)
.map((r, index) => {
return {
acc: r,
address: partnerKeys[index],
};
}),
)
: [];
const mintInfos = await fetchMultipleMintInfos({
connection: this.scope.connection,
mints: [rpcData.token0Mint, rpcData.token1Mint],
});
const mintA = toGammaApiToken({
address: rpcData.token0Mint.toBase58(),
decimals: rpcData.mint0Decimals,
programId: rpcData.token0Program.toBase58(),
extensions: {
feeConfig: mintInfos[rpcData.token0Mint.toBase58()].feeConfig
? toFeeConfig(mintInfos[rpcData.token0Mint.toBase58()].feeConfig)
: undefined,
},
});
const mintB = toGammaApiToken({
address: rpcData.token1Mint.toBase58(),
decimals: rpcData.mint1Decimals,
programId: rpcData.token1Program.toBase58(),
extensions: {
feeConfig: mintInfos[rpcData.token1Mint.toBase58()].feeConfig
? toFeeConfig(mintInfos[rpcData.token1Mint.toBase58()].feeConfig)
: undefined,
},
});
const configInfo = {
id: rpcData.ammConfig.toBase58(),
index: rpcData.configInfo!.index,
protocolFeeRate: rpcData.configInfo!.protocolFeeRate.toNumber(),
tradeFeeRate: rpcData.configInfo!.tradeFeeRate.toNumber(),
fundFeeRate: rpcData.configInfo!.fundFeeRate.toNumber(),
createPoolFee: rpcData.configInfo!.createPoolFee.toString(),
protocolOwner: rpcData.configInfo!.protocolOwner.toString(),
fundOwner: rpcData.configInfo!.fundOwner.toString(),
};
const mockPoolStats: PoolStats = {
range: "24H",
feesUsd: 0,
volumeTokenAUsd: 0,
volumeTokenBUsd: 0,
feesAprUsd: 0,
volumeAprUsd: 0,
withdrawnKaminoProfitTokenAUsd: 0,
withdrawnKaminoProfitTokenBUsd: 0,
withdrawnKaminoProfitTokenAAprUsd: 0,
withdrawnKaminoProfitTokenBAprUsd: 0,
};
return {
poolInfo: {
programId: rpcData.programId.toBase58(),
id: poolId,
config: configInfo,
mintA,
mintB,
price: rpcData.poolPrice.toString(),
liquidityTokenA: new Decimal(rpcData.vaultAAmount.toString()).div(10 ** mintA.decimals).toString(),
liquidityTokenB: new Decimal(rpcData.vaultBAmount.toString()).div(10 ** mintB.decimals).toString(),
lpSupply: rpcData.lpSupply.toString(),
poolCreator: rpcData.poolCreator.toBase58(),
poolType: "hyper",
mintAVault: rpcData.token0Vault.toBase58(),
mintBVault: rpcData.token1Vault.toBase58(),
authority: getPdaPoolAuthority(rpcData.programId).publicKey.toBase58(),
openTime: rpcData.openTime.toString(),
tvl: "0",
stats: {
daily: mockPoolStats,
weekly: mockPoolStats,
monthly: mockPoolStats,
},
partners: partners.map((p) => {
return {
address: p.address,
name: p.acc.name,
authority: p.acc.authority.toBase58(),
token0TokenAccount: p.acc.token0TokenAccount.toBase58(),
token1TokenAccount: p.acc.token1TokenAccount.toBase58(),
};
}),
},
poolKeys: {
programId: rpcData.programId.toBase58(),
id: poolId,
mintA: mintA.address,
mintB: mintB.address,
openTime: rpcData.openTime.toString(),
mintAVault: rpcData.token0Vault.toBase58(),
mintBVault: rpcData.token1Vault.toBase58(),
authority: getPdaPoolAuthority(rpcData.programId).publicKey.toBase58(),
config: configInfo,
mintAProgram: rpcData.token0Program.toBase58(),
mintBProgram: rpcData.token1Program.toBase58(),
poolType: "hyper",
},
rpcData,
};
}
public async createPool<T extends TxVersion>({
programId,
poolFeeAccount,
startTime,
maxTradeFeeRate,
volatilityFactor,
ownerInfo,
associatedOnly = false,
checkCreateATAOwner = false,
txVersion,
feeConfig,
computeBudgetConfig,
...params
}: CreateCpmmPoolParam<T>): Promise<MakeTxData<T, { address: CreateCpmmPoolAddress }>> {
const payer = ownerInfo.feePayer || this.scope.owner?.publicKey;
const isFront = new BN(new PublicKey(params.mintA.address).toBuffer()).lte(
new BN(new PublicKey(params.mintB.address).toBuffer()),
);
const [mintA, mintB] = isFront ? [params.mintA, params.mintB] : [params.mintB, params.mintA];
const [mintAAmount, mintBAmount] = isFront
? [params.mintAAmount, params.mintBAmount]
: [params.mintBAmount, params.mintAAmount];
const mintAUseSOLBalance = ownerInfo.useSOLBalance && mintA.address === NATIVE_MINT.toBase58();
const mintBUseSOLBalance = ownerInfo.useSOLBalance && mintB.address === NATIVE_MINT.toBase58();
const [mintAPubkey, mintBPubkey] = [new PublicKey(mintA.address), new PublicKey(mintB.address)];
const txBuilder = this.createTxBuilder();
const { account: userVaultA, instructionParams: userVaultAInstruction } =
await this.scope.account.getOrCreateTokenAccount({
mint: mintAPubkey,
tokenProgram: mintA.programId,
owner: this.scope.ownerPubKey,
createInfo: mintAUseSOLBalance
? {
payer: payer!,
amount: mintAAmount,
}
: undefined,
notUseTokenAccount: mintAUseSOLBalance,
skipCloseAccount: !mintAUseSOLBalance,
associatedOnly: mintAUseSOLBalance ? false : associatedOnly,
checkCreateATAOwner,
});
txBuilder.addInstruction(userVaultAInstruction || {});
const { account: userVaultB, instructionParams: userVaultBInstruction } =
await this.scope.account.getOrCreateTokenAccount({
mint: new PublicKey(mintB.address),
tokenProgram: mintB.programId,
owner: this.scope.ownerPubKey,
createInfo: mintBUseSOLBalance
? {
payer: payer!,
amount: mintBAmount,
}
: undefined,
notUseTokenAccount: mintBUseSOLBalance,
skipCloseAccount: !mintBUseSOLBalance,
associatedOnly: mintBUseSOLBalance ? false : associatedOnly,
checkCreateATAOwner,
});
txBuilder.addInstruction(userVaultBInstruction || {});
if (userVaultA === undefined || userVaultB === undefined)
throw Error("One or both of user's token account does not exist");
const poolKeys = getCreatePoolKeys({
programId,
configId: new PublicKey(feeConfig.id),
mintA: mintAPubkey,
mintB: mintBPubkey,
});
txBuilder.addInstruction({
instructions: [
await makeCreateCpmmPoolInInstruction(
this.program,
this.scope.ownerPubKey,
new PublicKey(feeConfig.id),
mintAPubkey,
mintBPubkey,
userVaultA,
userVaultB,
new PublicKey(mintA.programId ?? TOKEN_PROGRAM_ID),
new PublicKey(mintB.programId ?? TOKEN_PROGRAM_ID),
mintAAmount,
mintBAmount,
startTime,
maxTradeFeeRate,
volatilityFactor,
),
],
instructionTypes: [InstructionType.CpmmCreatePool],
});
txBuilder.addCustomComputeBudget(computeBudgetConfig);
return txBuilder.versionBuild({
txVersion,
extInfo: {
address: { ...poolKeys, mintA, mintB, programId, poolFeeAccount, feeConfig },
},
}) as Promise<MakeTxData<T, { address: CreateCpmmPoolAddress }>>;
}
public async addLiquidity<T extends TxVersion>(params: AddCpmmLiquidityParams<T>): Promise<MakeTxData<T>> {
const {
poolInfo,
poolKeys: propPoolKeys,
inputAmount,
baseSpecified,
slippage,
computeResult,
computeBudgetConfig,
config,
txVersion,
partner,
} = params;
// if (this.scope.availability.addStandardPosition === false)
// this.logAndCreateError("add liquidity feature disabled in your region");
if (inputAmount.isZero())
this.logAndCreateError("amounts must greater than zero", "amountInA", {
amountInA: inputAmount.toString(),
});
const { account } = this.scope;
const { bypassAssociatedCheck, checkCreateATAOwner } = {
// default
...{ bypassAssociatedCheck: false, checkCreateATAOwner: false },
// custom
...config,
};
const rpcPoolData = computeResult ? undefined : await this.getRpcPoolInfo(poolInfo.id);
const {
liquidity,
inputAmountFee,
anotherAmount: _anotherAmount,
} = computeResult ||
this.computePairAmount({
poolInfo: {
...poolInfo,
},
baseReserve: rpcPoolData!.baseReserve,
quoteReserve: rpcPoolData!.quoteReserve,
slippage: new Percent(0),
baseSpecified,
epochInfo: await this.scope.fetchEpochInfo(),
amount: new Decimal(inputAmount.toString()).div(
10 ** (baseSpecified? poolInfo.mintA.decimals : poolInfo.mintB.decimals),
),
});
const anotherAmount = _anotherAmount.amount;
const mintAUseSOLBalance = poolInfo.mintA.address === NATIVE_MINT.toString();
const mintBUseSOLBalance = poolInfo.mintB.address === NATIVE_MINT.toString();
const txBuilder = this.createTxBuilder();
const [mintA, mintB] = [new PublicKey(poolInfo.mintA.address), new PublicKey(poolInfo.mintB.address)];
const poolKeys = propPoolKeys ?? (await this.getCpmmPoolKeys(poolInfo.id));
const { account: tokenAccountA, instructionParams: _tokenAccountAInstruction } =
await this.scope.account.getOrCreateTokenAccount({
tokenProgram: poolKeys.mintAProgram,
mint: new PublicKey(poolInfo.mintA.address),
owner: this.scope.ownerPubKey,
createInfo:
mintAUseSOLBalance || (baseSpecified ? inputAmount : anotherAmount).isZero()
? {
payer: this.scope.ownerPubKey,
amount: baseSpecified ? inputAmount : anotherAmount,
}
: undefined,
skipCloseAccount: !mintAUseSOLBalance,
notUseTokenAccount: mintAUseSOLBalance,
associatedOnly: false,
checkCreateATAOwner,
});
txBuilder.addInstruction(_tokenAccountAInstruction || {});
const { account: tokenAccountB, instructionParams: _tokenAccountBInstruction } =
await this.scope.account.getOrCreateTokenAccount({
tokenProgram: poolKeys.mintBProgram,
mint: new PublicKey(poolInfo.mintB.address),
owner: this.scope.ownerPubKey,
createInfo:
mintBUseSOLBalance || (baseSpecified ? anotherAmount : inputAmount).isZero()
? {
payer: this.scope.ownerPubKey,
amount: baseSpecified ? anotherAmount : inputAmount,
}
: undefined,
skipCloseAccount: !mintBUseSOLBalance,
notUseTokenAccount: mintBUseSOLBalance,
associatedOnly: false,
checkCreateATAOwner,
});
txBuilder.addInstruction(_tokenAccountBInstruction || {});
if (!tokenAccountA && !tokenAccountB)
this.logAndCreateError("cannot found target token accounts", "tokenAccounts", account.tokenAccounts);
const userLiquidityPda = getPdaUserLiquidity(
new PublicKey(poolInfo.programId),
new PublicKey(poolInfo.id),
this.scope.ownerPubKey,
);
const userLiquidity = await this.getRpcUserLiquidityAccounts([userLiquidityPda.publicKey]).then((a) => a[0]);
if (!userLiquidity) {
txBuilder.addInstruction({
instructions: [
await makeInitUserPoolLiquidityInstruction(
this.program,
this.scope.ownerPubKey,
new PublicKey(poolInfo.id),
partner ? partner : null,
),
],
instructionTypes: [InstructionType.CpmmInitUserLiquidity],
lookupTableAddress: [],
});
}
const _slippage = new Percent(new BN(1)).sub(slippage);
txBuilder.addInstruction({
instructions: [
await makeDepositCpmmInInstruction(
this.program,
this.scope.ownerPubKey,
new PublicKey(poolInfo.id),
tokenAccountA!,
tokenAccountB!,
new PublicKey(poolKeys.mintAVault),
new PublicKey(poolKeys.mintBVault),
mintA,
mintB,
computeResult ? computeResult?.liquidity : _slippage.mul(liquidity).quotient,
baseSpecified ? inputAmountFee.amount : anotherAmount,
baseSpecified ? anotherAmount : inputAmountFee.amount,
),
],
instructionTypes: [InstructionType.CpmmAddLiquidity],
lookupTableAddress: [],
});
txBuilder.addCustomComputeBudget(computeBudgetConfig);
const builder = txBuilder.versionBuild({ txVersion }) as Promise<MakeTxData<T>>;
return txBuilder.versionBuild({ txVersion }) as Promise<MakeTxData<T>>;
}
public async withdrawLiquidity<T extends TxVersion>(params: WithdrawCpmmLiquidityParams<T>): Promise<MakeTxData<T>> {
const { poolInfo, poolKeys: propPoolKeys, lpAmount, slippage, computeBudgetConfig, txVersion } = params;
let tokenAReserve: KaminoReserve | undefined = this.reserves.get(poolInfo.mintA.address);
let tokenBReserve: KaminoReserve | undefined = this.reserves.get(poolInfo.mintB.address);
if (!tokenAReserve || !tokenBReserve) {
this.reserves = await getReservesForMarket(this.program.provider.connection);
tokenAReserve = this.reserves.get(poolInfo.mintA.address);
tokenBReserve = this.reserves.get(poolInfo.mintB.address);
}
// if (this.scope.availability.addStandardPosition === false)
// this.logAndCreateError("add liquidity feature disabled in your region");
const rpcPoolData = await this.getRpcPoolInfo(poolInfo.id);
const kaminoAccounts = getReserveAccountsForWithdraw(
new PublicKey(poolInfo.id),
tokenAReserve,
tokenBReserve,
rpcPoolData.maxSharedToken0,
rpcPoolData.maxSharedToken1,
this.program.programId,
this.kamino?.programId,
);
const _slippage = new Percent(new BN(1)).sub(slippage);
const [amountMintA, amountMintB] = [
_slippage.mul(lpAmount.mul(rpcPoolData.baseReserve).div(rpcPoolData.lpSupply)).quotient,
_slippage.mul(lpAmount.mul(rpcPoolData.quoteReserve).div(rpcPoolData.lpSupply)).quotient,
];
const epochInfo = await this.scope.fetchEpochInfo();
const [mintAAmountFee, mintBAmountFee] = [
getTransferAmountFeeV2(amountMintA, poolInfo.mintA.extensions?.feeConfig, epochInfo, false),
getTransferAmountFeeV2(amountMintB, poolInfo.mintB.extensions?.feeConfig, epochInfo, false),
];
const { account } = this.scope;
const txBuilder = this.createTxBuilder();
const [mintA, mintB] = [new PublicKey(poolInfo.mintA.address), new PublicKey(poolInfo.mintB.address)];
const mintAUseSOLBalance = mintA.equals(WSOLMint);
const mintBUseSOLBalance = mintB.equals(WSOLMint);
const poolKeys = propPoolKeys ?? (await this.getCpmmPoolKeys(poolInfo.id));
let tokenAccountA: PublicKey | undefined = undefined;
let tokenAccountB: PublicKey | undefined = undefined;
const { account: _ownerTokenAccountA, instructionParams: accountAInstructions } =
await this.scope.account.getOrCreateTokenAccount({
tokenProgram: poolKeys.mintAProgram,
mint: new PublicKey(poolInfo.mintA.address),
notUseTokenAccount: mintAUseSOLBalance,
owner: this.scope.ownerPubKey,
createInfo: {
payer: this.scope.ownerPubKey,
amount: 0,
},
skipCloseAccount: !mintAUseSOLBalance,
associatedOnly: mintAUseSOLBalance ? false : true,
checkCreateATAOwner: false,
});
tokenAccountA = _ownerTokenAccountA;
accountAInstructions && txBuilder.addInstruction(accountAInstructions);
const { account: _ownerTokenAccountB, instructionParams: accountBInstructions } =
await this.scope.account.getOrCreateTokenAccount({
tokenProgram: poolKeys.mintBProgram,
mint: new PublicKey(poolInfo.mintB.address),
notUseTokenAccount: mintBUseSOLBalance,
owner: this.scope.ownerPubKey,
createInfo: {
payer: this.scope.ownerPubKey,
amount: 0,
},
skipCloseAccount: !mintBUseSOLBalance,
associatedOnly: mintBUseSOLBalance ? false : true,
checkCreateATAOwner: false,
});
tokenAccountB = _ownerTokenAccountB;
accountBInstructions && txBuilder.addInstruction(accountBInstructions);
if (!tokenAccountA || !tokenAccountB)
this.logAndCreateError("cannot found target token accounts", "tokenAccounts", account.tokenAccounts);
const userLiquidityPda = getPdaUserLiquidity(
new PublicKey(poolInfo.programId),
new PublicKey(poolInfo.id),
this.scope.ownerPubKey,
);
const userLiquidity = await this.getRpcUserLiquidityAccounts([userLiquidityPda.publicKey]).then((a) => a[0]);
if (!userLiquidity) this.logAndCreateError("cannot found userLiquidityAccount");
txBuilder.addInstruction({
instructions: [
await makeWithdrawCpmmInInstruction(
this.program,
this.scope.ownerPubKey,
new PublicKey(poolInfo.id),
tokenAccountA!,
tokenAccountB!,
new PublicKey(poolKeys.mintAVault),
new PublicKey(poolKeys.mintBVault),
mintA,
mintB,
lpAmount,
amountMintA.sub(mintAAmountFee.fee ?? new BN(0)),
amountMintB.sub(mintBAmountFee.fee ?? new BN(0)),
kaminoAccounts,
),
],
instructionTypes: [InstructionType.CpmmWithdrawLiquidity],
lookupTableAddress: [],
});
txBuilder.addCustomComputeBudget(computeBudgetConfig);
return txBuilder.versionBuild({ txVersion }) as Promise<MakeTxData<T>>;
}
public async swap<T extends TxVersion>(params: CpmmSwapParams<T>): Promise<MakeTxData<T>> {
const {
poolInfo,
poolKeys: propPoolKeys,
zeroForOne,
baseIn,
swapResult,
slippage = 0,
config,
computeBudgetConfig,
txVersion,
wrapSol,
} = params;
const { bypassAssociatedCheck, checkCreateATAOwner, associatedOnly } = {
// default
...{ bypassAssociatedCheck: false, checkCreateATAOwner: false, associatedOnly: true },
// custom
...config,
};
const txBuilder = this.createTxBuilder();
const [mintA, mintB] = [new PublicKey(poolInfo.mintA.address), new PublicKey(poolInfo.mintB.address)];
if (baseIn) {
swapResult.destinationAmountSwapped = swapResult.destinationAmountSwapped
.mul(new BN((1 - slippage) * 10000))
.div(new BN(10000));
} else {
swapResult.sourceAmountSwapped = swapResult.sourceAmountSwapped
.mul(new BN((1 + slippage) * 10000))
.div(new BN(10000));
}
const poolKeys = propPoolKeys ?? (await this.getCpmmPoolKeys(poolInfo.id));
const mintAUseSOLBalance = poolInfo.mintA.address === WSOLMint.toBase58();
const mintBUseSOLBalance = poolInfo.mintB.address === WSOLMint.toBase58();
const { account: mintATokenAcc, instructionParams: mintATokenAccInstruction } =
await this.scope.account.getOrCreateTokenAccount({
mint: mintA,
tokenProgram: new PublicKey(poolKeys.mintAProgram),
owner: this.scope.ownerPubKey,
createInfo:
mintAUseSOLBalance || !zeroForOne
? {
payer: this.scope.ownerPubKey,
amount: zeroForOne ? swapResult.sourceAmountSwapped : 0,
}
: undefined,
notUseTokenAccount: mintAUseSOLBalance,
skipCloseAccount: !mintAUseSOLBalance,
associatedOnly: mintAUseSOLBalance ? false : associatedOnly,
checkCreateATAOwner,
});
mintATokenAccInstruction && txBuilder.addInstruction(mintATokenAccInstruction);
const { account: mintBTokenAcc, instructionParams: mintBTokenAccInstruction } =
await this.scope.account.getOrCreateTokenAccount({
mint: mintB,
tokenProgram: new PublicKey(poolKeys.mintBProgram),
owner: this.scope.ownerPubKey,
createInfo:
mintBUseSOLBalance || zeroForOne
? {
payer: this.scope.ownerPubKey,
amount: zeroForOne ? 0 : swapResult.sourceAmountSwapped,
}
: undefined,
notUseTokenAccount: mintBUseSOLBalance,
skipCloseAccount: !mintBUseSOLBalance,
associatedOnly: mintBUseSOLBalance ? false : associatedOnly,
checkCreateATAOwner,
});
mintBTokenAccInstruction && txBuilder.addInstruction(mintBTokenAccInstruction);
if (!mintATokenAcc || !mintBTokenAcc)
this.logAndCreateError("user do not have token account", {
mintA: poolInfo.mintA.symbol || poolInfo.mintA.address,
mintB: poolInfo.mintB.symbol || poolInfo.mintB.address,
mintATokenAcc,
mintBTokenAcc,
mintAUseSOLBalance,
mintBUseSOLBalance,
associatedOnly,
});
const inputMint = zeroForOne ? poolInfo.mintA.address : poolInfo.mintB.address;
if (wrapSol && inputMint === SOL_INFO.address) {
txBuilder.addInstruction({
instructions: [
SystemProgram.transfer({
fromPubkey: this.scope.ownerPubKey,
toPubkey: zeroForOne ? mintATokenAcc! : mintBTokenAcc!,
lamports: BigInt(swapResult.sourceAmountSwapped.toString()),
}),
createSyncNativeInstruction(new PublicKey(zeroForOne ? mintATokenAcc! : mintBTokenAcc!)),
],
});
}
txBuilder.addInstruction({
instructions: [
baseIn
? await makeSwapCpmmBaseInInstruction(
this.program,
this.scope.ownerPubKey,
new PublicKey(poolKeys.config.id),
new PublicKey(poolInfo.id),
zeroForOne ? mintATokenAcc! : mintBTokenAcc!,
zeroForOne ? mintBTokenAcc! : mintATokenAcc!,
new PublicKey(zeroForOne ? poolKeys.mintAVault : poolKeys.mintBVault),
new PublicKey(zeroForOne ? poolKeys.mintBVault : poolKeys.mintAVault),
new PublicKey(zeroForOne ? poolKeys.mintAProgram : poolKeys.mintBProgram),
new PublicKey(zeroForOne ? poolKeys.mintBProgram : poolKeys.mintAProgram),
zeroForOne ? mintA : mintB,
zeroForOne ? mintB : mintA,
getPdaObservationId(new PublicKey(poolInfo.programId), new PublicKey(poolInfo.id)).publicKey,
swapResult.sourceAmountSwapped,
swapResult.destinationAmountSwapped,
params.dflowSegmenterOptions,
params.referralAccounts,
)
: await makeSwapCpmmBaseOutInstruction(
this.program,
this.scope.ownerPubKey,
new PublicKey(poolKeys.config.id),
new PublicKey(poolInfo.id),
zeroForOne ? mintATokenAcc! : mintBTokenAcc!,
zeroForOne ? mintBTokenAcc! : mintATokenAcc!,
new PublicKey(zeroForOne ? poolKeys.mintAVault : poolKeys.mintBVault),
new PublicKey(zeroForOne ? poolKeys.mintBVault : poolKeys.mintAVault),
new PublicKey(zeroForOne ? poolKeys.mintAProgram : poolKeys.mintBProgram),
new PublicKey(zeroForOne ? poolKeys.mintBProgram : poolKeys.mintAProgram),
zeroForOne ? mintA : mintB,
zeroForOne ? mintB : mintA,
getPdaObservationId(new PublicKey(poolInfo.programId), new PublicKey(poolInfo.id)).publicKey,
swapResult.sourceAmountSwapped,
swapResult.destinationAmountSwapped,
params.dflowSegmenterOptions,
params.referralAccounts,
),
],
instructionTypes: [baseIn ? InstructionType.CpmmSwapBaseIn : InstructionType.CpmmSwapBaseOut],
});
txBuilder.addCustomComputeBudget(computeBudgetConfig);
return txBuilder.versionBuild({ txVersion }) as Promise<MakeTxData<T>>;
}
public async swapWithOracle<T extends TxVersion>(params: CpmmSwapParams<T>): Promise<MakeTxData<T>> {
const {
poolInfo,
poolKeys: propPoolKeys,
zeroForOne,
swapResult,
slippage = 0,
config,
computeBudgetConfig,
txVersion,
wrapSol,
} = params;
const { bypassAssociatedCheck, checkCreateATAOwner, associatedOnly } = {
// default
...{ bypassAssociatedCheck: false, checkCreateATAOwner: false, associatedOnly: true },
// custom
...config,
};
const txBuilder = this.createTxBuilder();
const [mintA, mintB] = [new PublicKey(poolInfo.mintA.address), new PublicKey(poolInfo.mintB.address)];
swapResult.destinationAmountSwapped = swapResult.destinationAmountSwapped
.mul(new BN((1 - slippage) * 10000))
.div(new BN(10000));
const poolKeys = propPoolKeys ?? (await this.getCpmmPoolKeys(poolInfo.id));
const mintAUseSOLBalance = poolInfo.mintA.address === WSOLMint.toBase58();
const mintBUseSOLBalance = poolInfo.mintB.address === WSOLMint.toBase58();
const { account: mintATokenAcc, instructionParams: mintATokenAccInstruction } =
await this.scope.account.getOrCreateTokenAccount({
mint: mintA,
tokenProgram: new PublicKey(poolKeys.mintAProgram),
owner: this.scope.ownerPubKey,
createInfo:
mintAUseSOLBalance || !zeroForOne
? {
payer: this.scope.ownerPubKey,
amount: zeroForOne ? swapResult.sourceAmountSwapped : 0,
}
: undefined,
notUseTokenAccount: mintAUseSOLBalance,
skipCloseAccount: !mintAUseSOLBalance,
associatedOnly: mintAUseSOLBalance ? false : associatedOnly,
checkCreateATAOwner,
});
mintATokenAccInstruction && txBuilder.addInstruction(mintATokenAccInstruction);
const { account: mintBTokenAcc, instructionParams: mintBTokenAccInstruction } =
await this.scope.account.getOrCreateTokenAccount({
mint: mintB,
tokenProgram: new PublicKey(poolKeys.mintBProgram),
owner: this.scope.ownerPubKey,
createInfo:
mintBUseSOLBalance || zeroForOne
? {
payer: this.scope.ownerPubKey,
amount: zeroForOne ? 0 : swapResult.sourceAmountSwapped,
}
: undefined,
notUseTokenAccount: mintBUseSOLBalance,
skipCloseAccount: !mintBUseSOLBalance,
associatedOnly: mintBUseSOLBalance ? false : associatedOnly,
checkCreateATAOwner,
});
mintBTokenAccInstruction && txBuilder.addInstruction(mintBTokenAccInstruction);
if (!mintATokenAcc || !mintBTokenAcc)
this.logAndCreateError("user do not have token account", {
mintA: poolInfo.mintA.symbol || poolInfo.mintA.address,
mintB: poolInfo.mintB.symbol || poolInfo.mintB.address,
mintATokenAcc,
mintBTokenAcc,
mintAUseSOLBalance,
mintBUseSOLBalance,
associatedOnly,
});
const inputMint = zeroForOne ? poolInfo.mintA.address : poolInfo.mintB.address;
if (wrapSol && inputMint === SOL_INFO.address) {
txBuilder.addInstruction({
instructions: [
SystemProgram.transfer({
fromPubkey: this.scope.ownerPubKey,
toPubkey: zeroForOne ? mintATokenAcc! : mintBTokenAcc!,
lamports: BigInt(swapResult.sourceAmountSwapped.toString()),
}),
createSyncNativeInstruction(new PublicKey(zeroForOne ? mintATokenAcc! : mintBTokenAcc!)),
],
});
}
txBuilder.addInstruction({
instructions: [
await makeSwapCpmmOracleBaseInInstruction(
this.program,
this.scope.ownerPubKey,
new PublicKey(poolKeys.config.id),
new PublicKey(poolInfo.id),
zeroForOne ? mintATokenAcc! : mintBTokenAcc!,
zeroForOne ? mintBTokenAcc! : mintATokenAcc!,
new PublicKey(zeroForOne ? poolKeys.mintAVault : poolKeys.mintBVault),
new PublicKey(zeroForOne ? poolKeys.mintBVault : poolKeys.mintAVault),
new PublicKey(zeroForOne ? poolKeys.mintAProgram : poolKeys.mintBProgram),
new PublicKey(zeroForOne ? poolKeys.mintBProgram : poolKeys.mintAProgram),
zeroForOne ? mintA : mintB,
zeroForOne ? mintB : mintA,
getPdaObservationId(new PublicKey(poolInfo.programId), new PublicKey(poolInfo.id)).publicKey,
swapResult.sourceAmountSwapped,
swapResult.destinationAmountSwapped,
params.dflowSegmenterOptions,
params.referralAccounts,
),
],
instructionTypes: [InstructionType.CpmmSwapBaseIn],
});
txBuilder.addCustomComputeBudget(computeBudgetConfig);
return txBuilder.versionBuild({ txVersion }) as Promise<MakeTxData<T>>;
}
public computeSwapAmount({
pool,
amount,
baseIn,
zeroForOne,
slippage,
observationState,
}: {
pool: CpmmComputeData;
amount: BN;
baseIn: boolean;
zeroForOne: boolean;
slippage: number;
observationState: CpmmObservationState;
}): {
allTrade: boolean;
amountIn: BN;
amountOut: BN;
minAmountOut: BN;
maxAmountIn: BN;
fee: BN;
executionPrice: Decimal;
priceImpact: Decimal;
} {
const [inputToken, outputToken, inputTokenReserves, outputTokenReserves] = zeroForOne
? [pool.mintA, pool.mintB, pool.baseReserve, pool.quoteReserve]
: [pool.mintB, pool.mintA, pool.quoteReserve, pool.baseReserve];
const inputDecimals = inputToken.decimals;
const outputDecimals = outputToken.decimals;
const swapResult = baseIn
? CurveCalculator.swapBaseIn(
amount,
new BN(inputTokenReserves),
new BN(outputTokenReserves),
new BN(pool.configInfo.tradeFeeRate),
observationState,
pool.volatilityFactor,
)
: CurveCalculator.swapBaseOut(
amount,
new BN(inputTokenReserves),
new BN(outputTokenReserves),
new BN(pool.configInfo.tradeFeeRate),
observationState,
pool.volatilityFactor,
);
const currentPrice = zeroForOne ? pool.poolPrice : new Decimal(1).div(pool.poolPrice);
const numerator = new Decimal(swapResult.destinationAmountSwapped.toString()).div(
new Decimal(10).pow(inputDecimals),
);
const denominator = new Decimal(swapResult.sourceAmountSwapped.toString()).div(new Decimal(10).pow(outputDecimals));
const executionPrice = numerator.div(denominator);
const otherAmountThreshold = baseIn
? swapResult.destinationAmountSwapped.mul(new BN((1 - slippage) * 10000)).div(new BN(10000))
: swapResult.sourceAmountSwapped.mul(new BN((1 + slippage) * 10000)).div(new BN(10000));
return {
allTrade: baseIn ? swapResult.sourceAmountSwapped.eq(amount) : swapResult.destinationAmountSwapped.eq(amount),
amountIn: swapResult.sourceAmountSwapped,
amountOut: swapResult.destinationAmountSwapped,
maxAmountIn: baseIn ? swapResult.sourceAmountSwapped : otherAmountThreshold,
minAmountOut: baseIn ? otherAmountThreshold : swapResult.destinationAmountSwapped,
executionPrice,
fee: swapResult.tradeFee,
priceImpact: currentPrice.sub(executionPrice).div(currentPrice),
};
}
public computePairAmount({
poolInfo,
baseReserve,
quoteReserve,
amount,
slippage,
epochInfo,
baseSpecified
}: ComputePairAmountParams): {
inputAmountFee: GetTransferAmountFee;
anotherAmount: GetTransferAmountFee;
maxAnotherAmount: GetTransferAmountFee;
liquidity: BN;
} {
const coefficient = 1 - Number(slippage.toSignificant()) / 100;
const inputAmount = new BN(
new Decimal(amount)
.mul(10 ** poolInfo[baseSpecified ? "mintA" : "mintB"].decimals)
.mul(coefficient)
.toFixed(0),
);
const inputAmountFee = getTransferAmountFeeV2(
inputAmount,
(baseSpecified ? poolInfo.mintA : poolInfo.mintB).extensions?.feeConfig,
epochInfo,
false,
);
const _inputAmountWithoutFee = inputAmount.sub(inputAmountFee.fee ?? new BN(0));
const lpAmount = new BN(poolInfo.lpSupply ?? "0");
this.logDebug("baseReserve:", baseReserve.toString(), "quoteReserve:", quoteReserve.toString());
this.logDebug(
"tokenIn:",
baseSpecified ? poolInfo.mintA.symbol : poolInfo.mintB.symbol,
"amountIn:",
inputAmount.toString(),
"amountInFee:",
inputAmountFee.fee?.toString() ?? 0,
"anotherToken:",
baseSpecified ? poolInfo.mintB.symbol : poolInfo.mintA.symbol,
"slippage:",
`${slippage.toSignificant()}%`,
);
// input is fixed
const input = baseSpecified ? "base" : "quote";
this.logDebug("input side:", input);
const liquidity = _inputAmountWithoutFee.mul(lpAmount).div(input === "base" ? baseReserve : quoteReserve);
let anotherAmountFee: GetTransferAmountFee = {
amount: BN_ZERO,
fee: undefined,
expirationTime: undefined,
};
if (!_inputAmountWithoutFee.isZero()) {
const lpAmountData = lpToAmount(liquidity, baseReserve, quoteReserve, lpAmount);
this.logDebug("lpAmountData:", {
amountA: lpAmountData.amountA.toString(),
amountB: lpAmountData.amountB.toString(),
});
anotherAmountFee = getTransferAmountFeeV2(
lpAmountData[baseSpecified ? "amountB" : "amountA"],
(baseSpecified ? poolInfo.mintB : poolInfo.mintA).extensions?.feeConfig,
epochInfo,
true,
);
}
const _slippage = new Percent(new BN(1)).add(slippage);
const slippageAdjustedAmount = getTransferAmountFeeV2(
_slippage.mul(anotherAmountFee.amount.sub(anotherAmountFee.fee ?? new BN(0))).quotient,
(baseSpecified ? poolInfo.mintB : poolInfo.mintA).extensions?.feeConfig,
epochInfo,
true,
);
this.logDebug(
"anotherAmount:",
anotherAmountFee.amount.toString(),
"anotherAmountFee:",
anotherAmountFee.fee?.toString() ?? 0,
"maxAnotherAmount:",
slippageAdjustedAmount.amount.toString(),
"maxAnotherAmountFee:",
slippageAdjustedAmount.fee?.toString() ?? 0,
);
return {
inputAmountFee,
anotherAmount: anotherAmountFee,
maxAnotherAmount: slippageAdjustedAmount,
liquidity,
};
}
public async initializePartner<T extends TxVersion>({
pool,
poolKeys,
partnerKey,
name,
token0TokenAccount,
token1TokenAccount,
computeBudgetConfig,
txVersion,
}: {
pool: string;
poolKeys: PoolKeys | undefined;
partnerKey: PublicKey;
name: string;
token0TokenAccount?: PublicKey;
token1TokenAccount?: PublicKey;
computeBudgetConfig?: ComputeBudgetConfig;
txVersion?: T;
}): Promise<MakeTxData<T>> {
const txBuilder = this.createTxBuilder();
const keys = poolKeys ?? (await this.getCpmmPoolKeys(pool));
const tokenAccount0 =
token0TokenAccount ??
this.scope.account.getAssociatedTokenAccount(new PublicKey(keys.mintA), new PublicKey(keys.mintAProgram));
const tokenAccount1 =
token1TokenAccount ??
this.scope.account.getAssociatedTokenAccount(new PublicKey(keys.mintB), new PublicKey(keys.mintBProgram));
txBuilder.addInstruction({
instructions: [
await makeInitializePartnerInstruction(
this.program,
this.scope.ownerPubKey,
this.scope.ownerPubKey,
new PublicKey(pool),
partnerKey,
name,
tokenAccount0,
tokenAccount1,
),
],
});
txBuilder.addCustomComputeBudget(computeBudgetConfig);
return txBuilder.versionBuild({ txVersion }) as Promise<MakeTxData<T>>;
}
public async addPartner<T extends TxVersion>({
pool,
poolKeys,
partnerKey,
computeBudgetConfig,
txVersion,
}: {
pool: string;
poolKeys: PoolKeys | undefined;
partnerKey: PublicKey;
computeBudgetConfig?: ComputeBudgetConfig;
txVersion?: T;
}): Promise<MakeTxData<T>> {
const txBuilder = this.createTxBuilder();
const keys = poolKeys ?? (await this.getCpmmPoolKeys(pool));
txBuilder.addInstruction({
instructions: [
await makeAddPartnerInstruction(
this.program,
new PublicKey(keys.config.id),
new PublicKey(pool),
partnerKey,
this.scope.ownerPubKey,
),
],
});
txBuilder.addCustomComputeBudget(computeBudgetConfig);
return txBuilder.versionBuild({ txVersion }) as Promise<MakeTxData<T>>;
}
public async getRpcUserLiquidityAccounts(addresses: PublicKey[]): Promise<(UserLiquidityAccount | null)[]> {
return await this.program.account.userPoolLiquidity.fetchMultiple(addresses);
}
public async getObservationStates(addresses: PublicKey[]): Promise<(CpmmObservationState | null)[]> {
return await this.program.account.observationState.fetchMultiple(addresses);
}
}
function lpToAmount(lp: BN, poolAmountA: BN, poolAmountB: BN, supply: BN): { amountA: BN; amountB: BN } {
let amountA = lp.mul(poolAmountA).div(supply);
if (!amountA.isZero() && !lp.mul(poolAmountA).mod(supply).isZero()) amountA = amountA.add(new BN(1));
let amountB = lp.mul(poolAmountB).div(supply);
if (!amountB.isZero() && !lp.mul(poolAmountB).mod(supply).isZero()) amountB = amountB.add(new BN(1));
return {
amountA,
amountB,
};
}