orca-clmm-agent
Version:
Orca Whirlpool clmm library for automated position management
557 lines (556 loc) • 28.4 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.joinTransactionChanges = exports.closePositionWithBaseToken = exports.closePositionGracefully = exports.convertDecimalToRaw = exports.convertRawToDecimal = exports.getDetailedPositions = exports.sleep = exports.getPreloadedTokens = exports.preloadTokens = exports.getTokens = void 0;
exports.getUSDPrice = getUSDPrice;
exports.fetchTokensWithPrices = fetchTokensWithPrices;
exports.computeNearestTotalRange = computeNearestTotalRange;
exports.openPositionWithBaseToken = openPositionWithBaseToken;
const orca_1 = require("./orca");
const pyth_1 = require("./pyth");
const analysis_1 = require("./analysis");
const solana_1 = require("./solana");
const sdk_1 = require("@lifi/sdk");
const whirlpools_core_1 = require("@orca-so/whirlpools-core");
const lifi_1 = require("./lifi");
const date_fns_1 = require("date-fns");
const kit_1 = require("@solana/kit");
const assert_1 = require("assert");
const lodash_1 = require("lodash");
const jupiter_1 = require("./jupiter");
const orca_types_1 = require("./orca.types");
const pythService = new pyth_1.PythPriceService();
const preloadedTokens = { tokens: [], time: new Date() };
const getTokens = async () => {
const { tokens: result } = await (0, sdk_1.getTokens)({ chains: [sdk_1.ChainId.SOL] });
const tokens = result[sdk_1.ChainId.SOL];
// update prices from jupiter, chunks of 50
const chunks = (0, lodash_1.chunk)(tokens.map((token) => token.address), 50);
for (const chunk of chunks) {
const result = await (0, jupiter_1.getJupiterUSDPrice)(chunk);
for (const [id, obj] of Object.entries(result)) {
const token = tokens.find((token) => token.address === id);
if (token) {
token.priceUSD = obj.usdPrice;
// @ts-ignore
token.time = new Date();
}
}
}
return tokens;
};
exports.getTokens = getTokens;
const preloadTokens = async () => {
const tokens = await (0, exports.getTokens)();
preloadedTokens.tokens = tokens;
preloadedTokens.time = new Date();
return tokens;
};
exports.preloadTokens = preloadTokens;
const getPreloadedTokens = async (maxAgeInSeconds = 300) => {
if (preloadedTokens.tokens.length === 0 || (0, date_fns_1.differenceInSeconds)(new Date(), preloadedTokens.time) > maxAgeInSeconds) {
await (0, exports.preloadTokens)();
}
return preloadedTokens.tokens;
};
exports.getPreloadedTokens = getPreloadedTokens;
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
exports.sleep = sleep;
const priceCacheThreshold = Number(process.env.PRICE_CACHE_THRESHOLD || 60);
async function getUSDPrice({ mintAddress }) {
let token = preloadedTokens.tokens.find((token) => token.address === mintAddress);
if (token) {
const age = (0, date_fns_1.differenceInSeconds)(new Date(), token.time);
if (age < priceCacheThreshold)
return +token.priceUSD;
}
// get token from LI.FI
token = {
...(await (0, sdk_1.getToken)(sdk_1.ChainId.SOL, mintAddress)),
time: new Date(),
};
if (token) {
const result = await (0, jupiter_1.getJupiterUSDPrice)([token.address]);
if (result[token.address])
token.priceUSD = result[token.address].usdPrice;
// update or push
if (preloadedTokens.tokens.find((token) => token.address === mintAddress)) {
preloadedTokens.tokens = preloadedTokens.tokens.map((token) => (token.address === mintAddress ? { ...token, priceUSD: token.priceUSD } : token));
}
else {
preloadedTokens.tokens.push(token);
}
return +token.priceUSD;
}
if (mintAddress === solana_1.PYUSD_MINT_ADDRESS)
return 1;
throw new Error(`[getUSDPrice] Failed to get price for ${mintAddress}`);
}
/**
* Fetches tokens with balances and adds USD price information
* @param walletAddress The wallet address to fetch tokens for
* @param rpcUrl Optional RPC URL
* @returns Promise resolving to an array of tokens with balances and USD prices
*/
async function fetchTokensWithPrices(walletAddress, rpcUrl) {
// Get tokens with non-zero balances
const balances = await (0, solana_1.fetchNonZeroTokenBalances)(walletAddress, rpcUrl);
//TODO: use getTokens from lifi sdk, includes usdPrice
// Process tokens in parallel to add price information
const tokensWithPrices = await Promise.all(balances.map(async (balance) => {
try {
// Get USD price from Pyth
const usdPrice = await getUSDPrice({ mintAddress: balance.address });
// Calculate USD value based on token balance
const usdValue = (0, exports.convertRawToDecimal)(BigInt(balance.balance.amount), balance.decimals) * usdPrice;
// Return token with added price information
return {
...balance,
usdPrice,
usdValue,
};
}
catch (error) {
console.warn(`Failed to get price for ${balance.symbol}:`, error);
// If price fetch fails, return token with zero price/value
return {
...balance,
usdPrice: 0,
usdValue: 0,
};
}
}));
// Sort tokens by USD value (highest first)
return tokensWithPrices.sort((a, b) => b.usdValue - a.usdValue);
}
/**
* Computes the nearest total range deviation and finds the closest match from a set of range choices.
* @param current Current pool price
* @param lowerPrice Lower range price
* @param upperPrice Upper range price
* @param ranges Array of range choices (e.g. [0.05, 0.1, 0.15])
* @returns The closest range value
*/
function computeNearestTotalRange(current, lowerPrice, upperPrice, ranges) {
// Calculate percent deltas
const downPct = ((current - lowerPrice) / current) * 100;
const upPct = ((upperPrice - current) / current) * 100;
// Total deviation
const total = (downPct + upPct) / 100;
// Find the closest range label
const result = ranges.reduce((closest, r) => (Math.abs(r - total) < Math.abs(closest - total) ? r : closest));
return result;
}
const getDetailedPositions = async (walletAddress, rpc, pools, rangeChoices = [0.05, 0.1, 0.15, 0.2], funder) => {
const positions = await (0, orca_1.getOrcaPositions)(walletAddress, rpc, pools, funder);
const detailedPositions = [];
for (const position of positions) {
const [tokenAPrice, tokenBPrice] = await Promise.all([
getUSDPrice({ mintAddress: position.tokenA.address }),
getUSDPrice({ mintAddress: position.tokenB.address }),
]);
const { relativePosition } = (0, analysis_1.analyzePositionBalance)(position);
const feeAValueUSD = position.fees.feeAmountA * tokenAPrice;
const feeBValueUSD = position.fees.feeAmountB * tokenBPrice;
const totalFeesUSD = feeAValueUSD + feeBValueUSD;
const { closeQuote } = position;
const mintSignatures = await rpc.getSignaturesForAddress((0, kit_1.address)(position.address)).send();
const blockTime = mintSignatures[mintSignatures.length - 1].blockTime;
const timestamp = (0, date_fns_1.fromUnixTime)(Number(blockTime));
//estimate
const tokenAValue = (0, exports.convertRawToDecimal)(BigInt(closeQuote.tokenEstA), position.tokenA.decimals) * tokenAPrice;
const tokenBValue = (0, exports.convertRawToDecimal)(BigInt(closeQuote.tokenEstB), position.tokenB.decimals) * tokenBPrice;
const positionValueUSD = tokenAValue + tokenBValue;
//min value
const minTokenAValue = (0, exports.convertRawToDecimal)(BigInt(closeQuote.tokenMinA), position.tokenA.decimals) * tokenAPrice;
const minTokenBValue = (0, exports.convertRawToDecimal)(BigInt(closeQuote.tokenMinB), position.tokenB.decimals) * tokenBPrice;
const minPositionValueUSD = minTokenAValue + minTokenBValue;
// Compute the range using the new utility
const range = computeNearestTotalRange(+position.currentMarketPrice, position.lowerPrice, position.upperPrice, rangeChoices);
detailedPositions.push({
...position,
tokenAPrice,
tokenAAmount: (0, exports.convertRawToDecimal)(BigInt(closeQuote.tokenEstA), position.tokenA.decimals),
tokenBPrice,
tokenBAmount: (0, exports.convertRawToDecimal)(BigInt(closeQuote.tokenEstB), position.tokenB.decimals),
relativePosition,
totalFeesUSD,
createdAt: timestamp,
positionValueUSD: {
est: positionValueUSD,
min: minPositionValueUSD,
},
range,
});
}
return detailedPositions;
};
exports.getDetailedPositions = getDetailedPositions;
/**
* Converts a raw token amount (in smallest units) to decimal format
* @param rawAmount The amount in smallest units (BigInt)
* @param decimals The number of decimal places for the token
* @returns The human-readable decimal amount
*/
const convertRawToDecimal = (rawAmount, decimals) => {
return Number(rawAmount) / Math.pow(10, decimals);
};
exports.convertRawToDecimal = convertRawToDecimal;
const convertDecimalToRaw = (decimalAmount, decimals) => {
return BigInt(Math.round(decimalAmount * Math.pow(10, decimals)));
};
exports.convertDecimalToRaw = convertDecimalToRaw;
/**
* Opens a position with a base token
* @returns A promise that resolves when the position is opened
* @throws Error if the initialization cost is too high
*/
async function openPositionWithBaseToken({ rpc, whirlpoolAddress, wallet, walletByteArray, baseTokenAddress, baseTokenAmount, lowerMultiple, upperMultiple, maxSwapAttempts = 30, maxGasUSD = 0.1, maxPriceImpact = 0.01, swapDustToAddress, }) {
const slippageToleranceBps = 100;
const whirlpool = await (0, orca_1.fetchOrcaPoolByAddress)(whirlpoolAddress);
const isTokenABase = whirlpool.tokenA.address === baseTokenAddress;
const baseToken = isTokenABase ? whirlpool.tokenA : whirlpool.tokenB;
const pairToken = isTokenABase ? whirlpool.tokenB : whirlpool.tokenA;
const pairTokenPriceUSD = await getUSDPrice({ mintAddress: pairToken.address });
const baseTokenPriceUSD = baseTokenAddress === solana_1.USDC_MINT_ADDRESS ? 1 : await getUSDPrice({ mintAddress: baseTokenAddress });
const { price } = await (0, orca_1.getOnChainPool)(whirlpool, rpc);
let portfolio = baseTokenAmount;
if (baseTokenAddress === solana_1.SOL_MINT_ADDRESS)
portfolio -= 0.05; //0.05 min balance in SOL to keep
if (portfolio < 0)
throw new Error(`[openPos] Negative portfolio: ${portfolio}`);
//relation between upper and lower multiple if both same difference to 1
const relation = (upperMultiple - lowerMultiple) / (1 - lowerMultiple);
const baseDeposit = portfolio / relation;
const lamports = (0, exports.convertDecimalToRaw)(baseDeposit, baseToken.decimals);
const increaseQuote = isTokenABase ? whirlpools_core_1.increaseLiquidityQuoteA : whirlpools_core_1.increaseLiquidityQuoteB;
const lowerTick = (0, whirlpools_core_1.priceToTickIndex)(price * lowerMultiple, whirlpool.tokenA.decimals, whirlpool.tokenB.decimals);
const upperTick = (0, whirlpools_core_1.priceToTickIndex)(price * upperMultiple, whirlpool.tokenA.decimals, whirlpool.tokenB.decimals);
const quote = increaseQuote(lamports, slippageToleranceBps, BigInt(whirlpool.sqrtPrice), (0, whirlpools_core_1.getInitializableTickIndex)(lowerTick, whirlpool.tickSpacing, false), (0, whirlpools_core_1.getInitializableTickIndex)(upperTick, whirlpool.tickSpacing, true));
const reqTokenAmountRaw = isTokenABase ? quote.tokenEstB : quote.tokenEstA;
const reqTokenAmount = (0, exports.convertRawToDecimal)(reqTokenAmountRaw, pairToken.decimals);
//console.log(`Required ${PairToken.symbol}: `, reqTokenAmount)
const pairAmountUSD = reqTokenAmount * pairTokenPriceUSD;
const baseAmountUSD = baseDeposit * baseTokenPriceUSD;
const portfolioUSD = portfolio * baseTokenPriceUSD;
const posValue = baseAmountUSD + pairAmountUSD;
const ratioUSDC = baseAmountUSD / posValue;
const ratioPair = pairAmountUSD / posValue;
// console.log(`Ratio: `, ratioUSDC.toFixed(3), ratioPair.toFixed(3));
// console.log(`Est ${baseToken.symbol}`, portfolio * ratioUSDC);
// console.log(`Est ${pairToken.symbol}`, (portfolio * ratioPair) / pairTokenPriceUSD);
//so we need ratioPair of portfolio in the pair token
const usdValueOfPairTokenNeeded = portfolioUSD * ratioPair;
// so much base token we need to swap to get the pair token
const amountOfBaseTokenNeeded = usdValueOfPairTokenNeeded / baseTokenPriceUSD;
const fromAmount = (0, exports.convertDecimalToRaw)(amountOfBaseTokenNeeded, baseToken.decimals).toString();
const details = await (0, lifi_1.swapAssets)({
rpc,
fromAmount,
fromTokenAddress: baseTokenAddress,
toTokenAddress: pairToken.address,
walletByteArray,
slippage: 0.005,
maxPriceImpact,
maxRetries: maxSwapAttempts,
confirmation: "finalized",
maxGasUSD,
});
//sometimes token change not found, not sure if LIFI waits for confirmation or finalization
await (0, exports.sleep)(500);
const tokenChange = details.changes.find((c) => c.owner === wallet.address && c.mint === pairToken.address);
if (!tokenChange) {
console.log(details);
throw new Error(`[openPos] Token change of ${pairToken.address} not found`);
}
//console.log(`[openPos] ${pairToken.symbol} change: ${tokenChange.amountDecimal}`);
const baseChange = details.changes.find((c) => c.owner === wallet.address && c.mint === baseTokenAddress);
if (!baseChange)
throw new Error(`[openPos] ${baseToken.symbol} change not found`);
let baseBalance = baseChange.amountDecimal;
//ensure 0.05 min balance in SOL
if (baseTokenAddress === solana_1.SOL_MINT_ADDRESS)
baseBalance -= 0.05;
const paramsKey = isTokenABase ? "tokenB" : "tokenA";
let createdPosition = false;
let multiplier = 1;
//refetch for latest price
while (!createdPosition) {
const slippageIncluded = tokenChange.amountDecimal * (1 - slippageToleranceBps / 10000);
const depositAmount = (0, exports.convertDecimalToRaw)(slippageIncluded * multiplier, pairToken.decimals);
//console.log(`[openPos] Deposit amount ${pairToken.symbol}: ${slippageIncluded * multiplier}`);
const params = { [paramsKey]: depositAmount };
const { price } = await (0, orca_1.getOnChainPool)(whirlpool, rpc).catch(async (error) => {
if (error.message.includes("Too Many Requests")) {
console.log("getOnChainPool error: Too many requests");
await (0, exports.sleep)(1000 * 20);
}
else
console.log("getOnChainPool error", error.message || error, typeof error);
return { price: null };
});
if (!price)
continue;
//console.log('whirlpool price now', price)
const lowerTick = (0, whirlpools_core_1.priceToTickIndex)(price * lowerMultiple, whirlpool.tokenA.decimals, whirlpool.tokenB.decimals);
const upperTick = (0, whirlpools_core_1.priceToTickIndex)(price * upperMultiple, whirlpool.tokenA.decimals, whirlpool.tokenB.decimals);
const inverseIncreaseQuote = isTokenABase ? whirlpools_core_1.increaseLiquidityQuoteB : whirlpools_core_1.increaseLiquidityQuoteA;
const oppositeQuote = inverseIncreaseQuote(depositAmount, slippageToleranceBps, BigInt(whirlpool.sqrtPrice), (0, whirlpools_core_1.getInitializableTickIndex)(lowerTick, whirlpool.tickSpacing, false), (0, whirlpools_core_1.getInitializableTickIndex)(upperTick, whirlpool.tickSpacing, true));
const baseKey = isTokenABase ? "tokenMaxA" : "tokenMaxB";
const maxBase = (0, exports.convertRawToDecimal)(oppositeQuote[baseKey], baseToken.decimals);
if (baseBalance < maxBase) {
//console.log(`Required max ${baseToken.symbol}: ${maxBase}, but only ${baseBalance} available -> Reducing multiplier`);
multiplier -= 0.001; //0.1%
await (0, exports.sleep)(250);
continue;
}
//if multiplier is at 1 meaning we deposit all of pair token we account less USDC utilization
if (maxBase < baseBalance * 0.95 && multiplier < 1) {
//console.log(`[openPos] Utilizing too little ${baseToken.symbol}, skipping`);
if (multiplier < 1)
multiplier += 0.01; //1%
await (0, exports.sleep)(250);
continue;
}
try {
const openResult = await (0, orca_1.openPosition)({
rpc,
whirlpoolAddress,
params,
price,
lowerMultiple,
upperMultiple,
slippageToleranceBps,
wallet,
swapDustToAddress,
walletByteArray,
maxGasUSD,
});
createdPosition = true;
return {
swapSignature: details.signature,
swapLoss: details.valueLoss + openResult.swapLoss,
swapPriceImpact: details.priceImpact,
positionSignature: openResult.signature,
positionMint: openResult.positionMint,
feeUSD: details.feeUSD + openResult.details.feeUSD,
tokenAPriceUSD: isTokenABase ? pairTokenPriceUSD : baseTokenPriceUSD,
tokenBPriceUSD: isTokenABase ? baseTokenPriceUSD : pairTokenPriceUSD,
};
}
catch (error) {
if (error instanceof orca_types_1.OrcaError) {
const { message, name } = error;
const code = error.code;
if (code === orca_1.TOKEN_MAX_EXCEEDED_ERROR || code === orca_1.TOKEN_MIN_SUBCEEDED_ERROR) {
//console.log(`[openPos] Token amount exceeded error: `, message);
multiplier -= 0.0025; //0.25%
await (0, exports.sleep)(1000);
continue;
}
if (code === orca_1.INVALID_START_TICK_ERROR) {
console.log(`[openPositionWithBaseToken] Invalid start tick error: `, message);
//hope on price change?
await (0, exports.sleep)(1000);
continue;
}
if (message.includes("Initialization cost too high")) {
console.warn(`[openPositionWithBaseToken] Initialization cost too high error: `, message);
// swap back into base token and throw error
const swapBackResult = await (0, lifi_1.swapAssets)({
rpc,
fromAmount: tokenChange.amount.toString(),
fromTokenAddress: tokenChange.mint,
toTokenAddress: baseTokenAddress,
walletByteArray,
slippage: 0.005,
maxPriceImpact: 0.01,
maxRetries: maxSwapAttempts,
confirmation: "finalized",
maxGasUSD,
});
return {
signatures: [details.signature, swapBackResult.signature],
swapLoss: details.valueLoss + swapBackResult.valueLoss,
feeUSD: details.feeUSD + swapBackResult.feeUSD,
error: error,
};
}
}
if (error instanceof kit_1.SolanaError) {
const reason = (error.cause ? error.cause : error);
if ((0, solana_1.checkForInstructionError)(reason)) {
const account = reason.InstructionError[0];
const code = reason.InstructionError[1].Custom;
if (code === solana_1.INSUFFICIENT_FUNDS_ERROR) {
console.log(`[openPositionWithBaseToken] Transaction failed due to insufficient funds error`, account);
break;
}
}
if (reason.message && reason.message.includes("Too Many Requests")) {
//console.log(`[openPositionWithBaseToken] Too many requests error: `, error.message);
await (0, exports.sleep)(1000 * 15);
continue;
}
console.error(`[openPositionWithBaseToken] SolanaError cause: `, reason);
}
if (error instanceof assert_1.AssertionError) {
if (error.message.includes("not have the required balance")) {
//console.log(`[openPos] Not enough ${baseToken.symbol} to open position error: `, error.message);
multiplier -= 0.0025; //0.25%
await (0, exports.sleep)(1000);
continue;
}
}
if (error.message.includes("Transaction timed out")) {
console.log(`[openPositionWithBaseToken] Transaction timed out error: `, error.message);
await (0, exports.sleep)(1000 * 15);
continue;
}
console.error(`[openPositionWithBaseToken] Error opening position: `, error);
throw error;
}
}
}
/**
* Closes a position and harvests yield, handling errors gracefully
* @param rpc The RPC client
* @param wallet The wallet signer
* @param position The position to close
* @param maxRetries The maximum number of retries
* @returns A promise that resolves when the position is closed
*/
const closePositionGracefully = async (rpc, wallet, position, maxRetries = 10) => {
let retries = 0;
while (retries < maxRetries) {
try {
const result = await (0, orca_1.closePositionAndHarvestYield)(rpc, wallet, position);
return result;
}
catch (error) {
retries++;
if (error instanceof kit_1.SolanaError) {
let { cause, message, context } = error;
//TODO: create handler for errors and if cause feed cause into it
if (message.includes("Transaction failed when it was simulated")) {
if ((0, solana_1.checkForInstructionError)(cause)) {
const account = cause.InstructionError[0];
const code = cause.InstructionError[1].Custom;
switch (code) {
case orca_1.TOKEN_MAX_EXCEEDED_ERROR:
console.log(`[closePositionGracefully] Transaction failed due to token max exceeded error`, account);
break;
case solana_1.INSUFFICIENT_FUNDS_ERROR:
console.log(`[closePositionGracefully] Transaction failed due to insufficient funds error`, account);
break;
case orca_1.TOKEN_MIN_SUBCEEDED_ERROR:
console.log(`[closePositionGracefully] Transaction failed due to token min exceeded error`, account);
break;
default:
console.error(`[closePositionGracefully] Transaction failed due error: `, code);
}
}
await (0, exports.sleep)(1000 * 3);
continue;
}
if (message.includes("Too Many Requests") || context?.statusCode === 429 || (cause instanceof kit_1.SolanaError && cause.context?.statusCode === 429)) {
console.log(`[closePositionGracefully] Too many requests error`);
await (0, exports.sleep)(1000 * 60);
continue;
}
if (message.includes("Failed to estimate the compute unit consumption")) {
console.log(`[closePositionGracefully] Failed to estimate the compute unit consumption for this transaction error`, error);
await (0, exports.sleep)(1000);
continue;
}
if (message.includes("Account not found at address")) {
console.log(`[closePositionGracefully] Seems like position was already closed`, error);
return;
}
console.error(`[closePositionGracefully] Unhandled SolanaError: `, error);
}
if (error instanceof orca_types_1.OrcaError) {
console.error(`[closePositionGracefully] OrcaError: `, error.message);
continue;
}
console.error(`[closePositionGracefully] Unhandled Error closing position: `, {
error,
message: error?.message,
cause: error?.cause,
name: error?.name,
});
throw error;
}
}
};
exports.closePositionGracefully = closePositionGracefully;
const closePositionWithBaseToken = async ({ rpc, wallet, position, walletByteArray, baseToken, maxPriceImpact = 0.02, maxGasUSD, maxRetries = 50, }) => {
const closeResult = await (0, exports.closePositionGracefully)(rpc, wallet, position);
if (!closeResult)
throw new Error("No close result found");
await (0, exports.sleep)(500);
const changesInWallet = closeResult.details.changes.filter((c) => c.owner === wallet.address);
const isTokenABase = position.tokenA.address === baseToken;
const pairToken = isTokenABase ? position.tokenB : position.tokenA;
const changeInPairToken = changesInWallet?.find((c) => c.mint === pairToken.address);
if (!changeInPairToken)
return {
closeSignature: closeResult.details.signature,
feeUSD: closeResult.details.feeUSD,
swapLoss: 0,
};
let swapResult;
while (!swapResult) {
try {
swapResult = await (0, lifi_1.swapAssets)({
rpc,
fromAmount: changeInPairToken.amount.toString(),
fromTokenAddress: changeInPairToken.mint,
toTokenAddress: baseToken,
walletByteArray,
slippage: 0.005,
maxPriceImpact,
maxRetries,
confirmation: "finalized",
maxGasUSD,
});
}
catch (error) {
if (error instanceof orca_types_1.OrcaError) {
if (error.message.includes("invalid start tick")) {
continue;
}
}
throw error;
}
}
const changes = swapResult.changes;
const changeInBaseToken = changes.find((c) => c.mint === baseToken);
if (!changeInBaseToken)
throw new Error("No change in base token found");
console.log("changeInBaseToken", changeInBaseToken);
const closeFees = closeResult.details.feeUSD + swapResult.feeUSD;
return {
closeSignature: closeResult.details.signature,
swapSignature: swapResult.signature,
swapLoss: swapResult.valueLoss,
feeUSD: closeFees,
};
};
exports.closePositionWithBaseToken = closePositionWithBaseToken;
const joinTransactionChanges = (earlierChanges, laterChanges) => {
const joined = [];
[...earlierChanges, ...laterChanges].forEach((change) => {
const existingChange = joined.find((c) => c.mint === change.mint && c.owner === change.owner);
if (existingChange) {
existingChange.amount = change.amount;
existingChange.amountDecimal = change.amountDecimal;
existingChange.change = BigInt(Number(existingChange.change) + Number(change.change));
existingChange.changeDecimal = existingChange.changeDecimal + change.changeDecimal;
}
else {
joined.push(change);
}
});
return joined;
};
exports.joinTransactionChanges = joinTransactionChanges;