UNPKG

@stabilis/c9-shape-liquidity-getter

Version:

A library for calculating redemption values of concentrated liquidity positions for C9 shape liquidity.

299 lines (298 loc) 13.4 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.getRedemptionValue = getRedemptionValue; exports.getRedemptionValues = getRedemptionValues; const c9Data_1 = require("../utils/c9Data"); const gateway_1 = require("./gateway"); const errors_1 = require("../types/errors"); const tickCalculator_1 = require("../utils/tickCalculator"); const i192_1 = require("../utils/i192"); /** * Calculates redemption value for a single NFT using component data */ function calculateSingleRedemption(nftData, c9Data, priceBounds, middlePrice) { try { const liquidityClaimsField = nftData.fields.find((f) => f.field_name === "liquidity_claims"); if (!liquidityClaimsField?.entries) { throw errors_1.NFTError.invalidClaims(nftData.id || "unknown"); } // Initialize amounts using I192 let amount_x = i192_1.I192.zero(); let amount_y = i192_1.I192.zero(); let isActive = false; // Calculate price bounds ticks if provided let lowerBoundTick; let upperBoundTick; if (priceBounds) { // Calculate the middle price let currentPrice; if (middlePrice !== undefined) { currentPrice = middlePrice; } else { // Calculate current price from current tick + half bin span const middleTick = c9Data.currentTick + Math.floor(c9Data.binSpan / 2); currentPrice = parseFloat((0, tickCalculator_1.calculatePrice)(middleTick)); } // Calculate actual price bounds using multipliers const lowerPrice = currentPrice * priceBounds[0]; const upperPrice = currentPrice * priceBounds[1]; // Convert to ticks - this is a tick calculation, so we don't use I192 lowerBoundTick = (0, tickCalculator_1.calculateTick)(lowerPrice); upperBoundTick = (0, tickCalculator_1.calculateTick)(upperPrice); } // Calculate redemption values for (const entry of liquidityClaimsField.entries) { const tick = parseInt(entry.key.value); const claimAmount = entry.value.value; // Skip if outside price bounds if (priceBounds && (tick < lowerBoundTick || tick > upperBoundTick)) { continue; } if (tick < c9Data.currentTick) { // Bin below current tick - only Y tokens const bin = c9Data.binMapData[tick]; if (bin) { let share = new i192_1.I192(claimAmount).divide(bin.total_claim); // Apply bin fraction if price bounds are provided if (priceBounds) { const binStartTick = (0, tickCalculator_1.calculateBinStartTick)(tick, c9Data.binSpan); const binFraction = (0, tickCalculator_1.calculateBinFraction)(binStartTick, c9Data.binSpan, lowerBoundTick, upperBoundTick); share = share.multiply(binFraction); } amount_y = amount_y.add(share.multiply(bin.amount)); } } else if (tick > c9Data.currentTick) { // Bin above current tick - only X tokens const bin = c9Data.binMapData[tick]; if (bin) { let share = new i192_1.I192(claimAmount).divide(bin.total_claim); // Apply bin fraction if price bounds are provided if (priceBounds) { const binStartTick = (0, tickCalculator_1.calculateBinStartTick)(tick, c9Data.binSpan); const binFraction = (0, tickCalculator_1.calculateBinFraction)(binStartTick, c9Data.binSpan, lowerBoundTick, upperBoundTick); share = share.multiply(binFraction); } amount_x = amount_x.add(share.multiply(bin.amount)); } } else { // Active bin - both X and Y tokens isActive = true; let liquidityShare = new i192_1.I192(claimAmount).divide(c9Data.active_total_claim); // Apply bin fraction if price bounds are provided if (priceBounds) { const binStartTick = (0, tickCalculator_1.calculateBinStartTick)(tick, c9Data.binSpan); const binFraction = (0, tickCalculator_1.calculateBinFraction)(binStartTick, c9Data.binSpan, lowerBoundTick, upperBoundTick); liquidityShare = liquidityShare.multiply(binFraction); } amount_x = amount_x.add(new i192_1.I192(c9Data.active_x).multiply(liquidityShare)); amount_y = amount_y.add(new i192_1.I192(c9Data.active_y).multiply(liquidityShare)); } } return { xToken: amount_x.toString(), yToken: amount_y.toString(), isActive, }; } catch (error) { if (error instanceof errors_1.NFTError) { throw error; } console.error("Error calculating single redemption value:", error); return null; } } /** * Calculates the redemption value for a single NFT position * @param input The input parameters containing componentAddress, nftId, and optional stateVersion and priceBounds * @returns A promise that resolves to the redemption values or null if calculation fails */ async function getRedemptionValue(input) { try { const { componentAddress, nftId, stateVersion, priceBounds, middlePrice } = input; // Type validation if (typeof componentAddress !== "string") { throw errors_1.ValidationError.invalidComponentAddress(componentAddress); } if (typeof nftId !== "string") { throw errors_1.ValidationError.invalidNftId(nftId); } if (typeof stateVersion !== "number") { throw errors_1.ValidationError.invalidStateVersion(stateVersion); } if (priceBounds !== undefined) { if (!Array.isArray(priceBounds) || priceBounds.length !== 2) { throw errors_1.ValidationError.invalidPriceBounds(); } if (typeof priceBounds[0] !== "number" || typeof priceBounds[1] !== "number") { throw errors_1.ValidationError.invalidPriceBounds(); } if (priceBounds[0] >= priceBounds[1]) { throw errors_1.ValidationError.invalidPriceBounds(); } if (priceBounds[0] <= 0) { throw errors_1.ValidationError.invalidPriceBounds(); } } if (middlePrice !== undefined && typeof middlePrice !== "number") { throw errors_1.ValidationError.invalidMiddlePrice(); } if (middlePrice !== undefined && middlePrice <= 0) { throw errors_1.ValidationError.invalidMiddlePrice(); } // Initialize API once const api = (0, gateway_1.getGatewayApi)(); // 1. Get all C9 data let c9Data; try { c9Data = await (0, c9Data_1.getC9BinData)(componentAddress, stateVersion, api); } catch (error) { if (error instanceof errors_1.DataError) { throw error; } if (error?.details?.validation_errors?.[0]?.errors?.[0]?.includes("beyond the end")) { throw errors_1.DataError.stateVersionTooHigh(stateVersion); } if (error?.code === 400) { throw errors_1.NetworkError.requestFailed(error.message, error.code); } throw errors_1.ComponentError.notC9Component(componentAddress); } if (!c9Data) { throw errors_1.ComponentError.notC9Component(componentAddress); } if (!c9Data.currentTick) { throw errors_1.ComponentError.missingField(componentAddress, "currentTick"); } // 2. Get NFT data const nftDataMap = await (0, gateway_1.getNFTDataInChunks)([nftId], c9Data.nftAddress, api, stateVersion).catch((error) => { if (error?.code === 400) { throw errors_1.NetworkError.requestFailed(error.message, error.code); } return {}; }); const nftData = nftDataMap[nftId]; if (!nftData) { throw errors_1.NFTError.notFound(nftId); } // 3. Calculate redemption value const result = calculateSingleRedemption(nftData, c9Data, priceBounds, middlePrice); if (!result) { throw errors_1.DataError.invalidFormat("redemption calculation"); } return result; } catch (error) { if (error instanceof errors_1.BaseError) { throw error; } console.error("Error calculating redemption value:", error); throw errors_1.ComponentError.notC9Component(input.componentAddress); } } /** * Calculates redemption values for multiple NFT positions * @param input The input parameters containing componentAddress, array of nftIds, and optional stateVersion and priceBounds * @returns A promise that resolves to an object mapping nftIds to their redemption values */ async function getRedemptionValues(input) { try { const { componentAddress, nftIds, stateVersion, priceBounds, middlePrice } = input; // Type validation if (typeof componentAddress !== "string") { throw errors_1.ValidationError.invalidComponentAddress(componentAddress); } if (!Array.isArray(nftIds)) { throw errors_1.ValidationError.invalidNftIds(); } if (!nftIds.every((id) => typeof id === "string")) { throw errors_1.ValidationError.invalidNftIds(); } if (typeof stateVersion !== "number") { throw errors_1.ValidationError.invalidStateVersion(stateVersion); } if (priceBounds !== undefined) { if (!Array.isArray(priceBounds) || priceBounds.length !== 2) { throw errors_1.ValidationError.invalidPriceBounds(); } if (typeof priceBounds[0] !== "number" || typeof priceBounds[1] !== "number") { throw errors_1.ValidationError.invalidPriceBounds(); } if (priceBounds[0] >= priceBounds[1]) { throw errors_1.ValidationError.invalidPriceBounds(); } if (priceBounds[0] <= 0) { throw errors_1.ValidationError.invalidPriceBounds(); } } if (middlePrice !== undefined && typeof middlePrice !== "number") { throw errors_1.ValidationError.invalidMiddlePrice(); } if (middlePrice !== undefined && middlePrice <= 0) { throw errors_1.ValidationError.invalidMiddlePrice(); } const results = {}; // Initialize API once const api = (0, gateway_1.getGatewayApi)(); // 1. Get component data (only once for all NFTs) let c9Data; try { c9Data = await (0, c9Data_1.getC9BinData)(componentAddress, stateVersion, api); } catch (error) { if (error instanceof errors_1.DataError) { throw error; } if (error?.details?.validation_errors?.[0]?.errors?.[0]?.includes("beyond the end")) { throw errors_1.DataError.stateVersionTooHigh(stateVersion); } if (error?.code === 400) { throw errors_1.NetworkError.requestFailed(error.message, error.code); } throw errors_1.ComponentError.notC9Component(componentAddress); } if (!c9Data) { throw errors_1.ComponentError.notC9Component(componentAddress); } if (!c9Data.currentTick) { throw errors_1.ComponentError.missingField(componentAddress, "currentTick"); } // 2. Get NFT data in chunks const nftDataMap = await (0, gateway_1.getNFTDataInChunks)(nftIds, c9Data.nftAddress, api, stateVersion).catch((error) => { if (error?.code === 400) { throw errors_1.NetworkError.requestFailed(error.message, error.code); } return {}; }); // 3. Calculate redemption values for each NFT for (const nftId of nftIds) { const nftData = nftDataMap[nftId]; if (nftData) { try { const redemptionValue = calculateSingleRedemption(nftData, c9Data, priceBounds, middlePrice); if (redemptionValue) { results[nftId] = redemptionValue; } } catch (error) { console.error(`Error calculating redemption for NFT ${nftId}:`, error); // Continue processing other NFTs even if one fails } } } return results; } catch (error) { if (error instanceof errors_1.BaseError) { throw error; } console.error("Error calculating redemption values:", error); throw errors_1.ComponentError.notC9Component(input.componentAddress); } }