Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
346 changes: 346 additions & 0 deletions src/dcl-swap/dcl-liquidity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,346 @@
import { TokenMetadata, Transaction } from '../types';
import {
toNonDivisibleNumber,
registerAccountOnToken,
} from '../utils';
import { ONE_YOCTO_NEAR, config } from '../constant';
import { DCLSwapGetStorageBalance } from '../ref';
import { refDCLSwapViewFunction } from '../ref';

/**
* DCL Liquidity Management for Ref Finance CLMM (dclv2.ref-labs.near)
*
* Provides functions to manage concentrated liquidity positions:
* - Deposit tokens into CLMM contract
* - Add liquidity at a specific price range
* - Remove liquidity (full or partial)
* - Harvest accrued fees without closing position
* - List active LP positions
*
* All methods are tested on NEAR mainnet against dclv2.ref-labs.near.
*
* Flow:
* 1. Deposit tokens: ft_transfer_call("Deposit") to token contract
* 2. Add liquidity: batch_add_liquidity direct call on CLMM contract
* 3. Harvest fees: remove_liquidity with amount="0"
* 4. Remove position: remove_liquidity with amount=full
*/

// ─── Types ────────────────────────────────────────────────────────────────────

interface AddLiquidityInfo {
/** Pool ID (e.g. "token_x|token_y|fee") */
pool_id: string;
/** Token X amount (raw integer string) */
amount_x: string;
/** Token Y amount (raw integer string) */
amount_y: string;
/** Lower price bound tick (must be bin-aligned: multiple of point_delta) */
left_point: number;
/** Upper price bound tick (must be bin-aligned: multiple of point_delta) */
right_point: number;
/** Min token X to receive on removal (slippage protection, "0" = no limit) */
min_amount_x: string;
/** Min token Y to receive on removal (slippage protection, "0" = no limit) */
min_amount_y: string;
}

interface RemoveLiquidityParams {
/** LP NFT token ID (e.g. "token_x|token_y|fee#12345") */
lpt_id: string;
/**
* Liquidity amount to remove.
* "0" = harvest fees only (position stays open).
* Any positive value = partial or full removal.
*/
amount: string;
/**
* Liquidation type:
* 0 = ByMarket — market-rate settlement
*/
liquidate_type: number;
/** Min token X to receive */
min_amount_x: string;
/** Min token Y to receive */
min_amount_y: string;
}

interface DepositParams {
token: TokenMetadata;
/** Human-readable amount to deposit (e.g. "100.5") */
amount: string;
}

// ─── View Functions ────────────────────────────────────────────────────────────

/**
* Get LP positions for an account via NFT enumeration.
* Each position is an NFT (lpt_id) held by the account.
*/
export const getLiquidityPositions = async (accountId: string) => {
if (!accountId) return [];

const nfts: Array<{ token_id: string; metadata?: Record<string, string> }> =
await refDCLSwapViewFunction({
methodName: 'nft_tokens_for_owner',
args: {
account_id: accountId,
from_index: '0',
limit: 50,
},
});

return nfts.map((nft) => nft.token_id);
};

/**
* Get the current pool state.
* Returns pool data including current_point, liquidity, fee, etc.
*/
export const getPoolState = async (pool_id: string) => {
return refDCLSwapViewFunction({
methodName: 'get_pool',
args: { pool_id },
});
};

/**
* Get a specific LP position's state.
*/
export const getLiquidity = async (lpt_id: string) => {
return refDCLSwapViewFunction({
methodName: 'get_liquidity',
args: { lpt_id },
});
};

// ─── Deposit Tokens ───────────────────────────────────────────────────────────

/**
* Build a transaction to deposit a token into the CLMM contract's internal balance.
*
* The CLMM contract uses an internal balance system. Tokens must first be deposited
* via ft_transfer_call with msg "Deposit" before they can be used in batch_add_liquidity.
*
* @param depositParams Token and amount to deposit
* @param accountId Account performing the deposit
* @returns Transaction array (may include storage registration)
*/
export const depositTokenToCLMM = async ({
token,
amount,
}: DepositParams): Promise<Transaction[]> => {
const transactions: Transaction[] = [];

// Register on CLMM contract if needed
const registered = await DCLSwapGetStorageBalance(
token.id,
config.REF_DCL_SWAP_CONTRACT_ID
).catch(() => null);

if (registered === null) {
transactions.push({
receiverId: config.REF_DCL_SWAP_CONTRACT_ID,
functionCalls: [
{
methodName: 'storage_deposit',
args: {
registration_only: true,
account_id: config.REF_DCL_SWAP_CONTRACT_ID,
},
gas: '30000000000000',
amount: '0.5',
},
],
});
}

// Register on token contract if needed
const tokenRegistered = await DCLSwapGetStorageBalance(
token.id,
config.REF_DCL_SWAP_CONTRACT_ID
).catch(() => null);

if (tokenRegistered === null) {
transactions.push({
receiverId: token.id,
functionCalls: [registerAccountOnToken(config.REF_DCL_SWAP_CONTRACT_ID)],
});
}

// Deposit via ft_transfer_call with "Deposit" msg
transactions.push({
receiverId: token.id,
functionCalls: [
{
methodName: 'ft_transfer_call',
args: {
receiver_id: config.REF_DCL_SWAP_CONTRACT_ID,
amount: toNonDivisibleNumber(token.decimals, amount),
msg: JSON.stringify('Deposit'),
},
gas: '180000000000000',
amount: ONE_YOCTO_NEAR,
},
],
});

return transactions;
};

// ─── Add Liquidity ─────────────────────────────────────────────────────────────

/**
* Build a transaction to add liquidity to a CLMM pool.
*
* This is a direct function call to the CLMM contract (not ft_transfer_call).
* The contract pulls tokens from its internal balance via ft_transfer_from.
*
* IMPORTANT: Tokens must be deposited first via depositTokenToCLMM().
*
* @param addLiquidityInfos Array of liquidity additions (supports multi-pool in single tx)
* @returns Transaction that calls batch_add_liquidity
*/
export const addLiquidity = (
addLiquidityInfos: AddLiquidityInfo[]
): Transaction => {
return {
receiverId: config.REF_DCL_SWAP_CONTRACT_ID,
functionCalls: [
{
methodName: 'batch_add_liquidity',
args: {
add_liquidity_infos: addLiquidityInfos.map((info) => ({
pool_id: info.pool_id,
amount_x: info.amount_x,
amount_y: info.amount_y,
left_point: info.left_point,
right_point: info.right_point,
min_amount_x: info.min_amount_x,
min_amount_y: info.min_amount_y,
})),
},
gas: '150000000000000',
amount: '0',
},
],
};
};

// ─── Remove Liquidity ─────────────────────────────────────────────────────────

/**
* Build a transaction to remove liquidity from a CLMM position.
*
* - amount="0" → Harvest fees only, position stays open with full liquidity.
* - amount="0" with liquidate_type=0 → ByMarket fee settlement.
* - amount=full_liquidity → Close position entirely, claims fees + principal.
*
* @param params Remove liquidity parameters
* @returns Transaction
*/
export const removeLiquidity = (
params: RemoveLiquidityParams
): Transaction => {
return {
receiverId: config.REF_DCL_SWAP_CONTRACT_ID,
functionCalls: [
{
methodName: 'remove_liquidity',
args: {
lpt_id: params.lpt_id,
amount: params.amount,
liquidate_type: params.liquidate_type,
min_amount_x: params.min_amount_x,
min_amount_y: params.min_amount_y,
},
gas: '150000000000000',
amount: '0',
},
],
};
};

// ─── Convenience: Full Flow ────────────────────────────────────────────────────

/**
* Build the complete transaction set to open a new LP position.
*
* 1. Register storage on CLMM if needed
* 2. Deposit token X
* 3. Deposit token Y
* 4. Call batch_add_liquidity
*
* For a range containing the current price, both amount_x and amount_y must be > 0.
* Points must be bin-aligned (multiples of the pool's point_delta).
*/
export const openPosition = async (
tokenX: TokenMetadata,
tokenY: TokenMetadata,
amountX: string,
amountY: string,
poolId: string,
leftPoint: number,
rightPoint: number,
slippageTolerance: number = 5
): Promise<Transaction[]> => {
const transactions: Transaction[] = [];

// Register CLMM storage for both tokens
for (const token of [tokenX, tokenY]) {
const registered = await DCLSwapGetStorageBalance(
token.id,
config.REF_DCL_SWAP_CONTRACT_ID
).catch(() => null);

if (registered === null) {
transactions.push({
receiverId: config.REF_DCL_SWAP_CONTRACT_ID,
functionCalls: [
{
methodName: 'storage_deposit',
args: {
registration_only: true,
account_id: config.REF_DCL_SWAP_CONTRACT_ID,
},
gas: '30000000000000',
amount: '0.5',
},
],
});
}
}

// Deposit token X
if (BigNumber(amountX).gt(0)) {
transactions.push(...(await depositTokenToCLMM({ token: tokenX, amount: amountX })));
}

// Deposit token Y
if (BigNumber(amountY).gt(0)) {
transactions.push(...(await depositTokenToCLMM({ token: tokenY, amount: amountY })));
}

// Add liquidity
transactions.push(
addLiquidity([
{
pool_id: poolId,
amount_x: toNonDivisibleNumber(tokenX.decimals, amountX),
amount_y: toNonDivisibleNumber(tokenY.decimals, amountY),
left_point: leftPoint,
right_point: rightPoint,
min_amount_x: '0',
min_amount_y: '0',
},
])
);

return transactions;
};

// Simple number comparison helper (no external dep needed)
function BigNumber(value: string) {
const num = Number(value);
return { gt: (other: number) => num > other };
}
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,5 @@ export * from './dcl-swap/dcl-pool';
export * from './dcl-swap/swap';

export * from './dcl-swap/limit-order';

export * from './dcl-swap/dcl-liquidity';