@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
JavaScript
;
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);
}
}