Copy const {
intCV,
uintCV,
listCV,
tupleCV,
principalCV,
contractPrincipalCV,
createAssetInfo,
makeStandardFungiblePostCondition,
makeStandardNonFungiblePostCondition,
FungibleConditionCode,
NonFungibleConditionCode,
PostConditionMode,
AnchorMode,
makeContractCall,
broadcastTransaction
} = require('@stacks/transactions');
const { StacksMainnet } = require('@stacks/network');
const stacks_network = new StacksMainnet();
const STACKS_PRIVATE_KEY = '<your_stacks_private_key>';
const STACKS_TRANSACTION_FEE = 10000; // Transaction fee in uSTX (1 STX = 1000000)
const LIQUIDITY_ROUTER_CONTRACT = 'SP3ESW1QCNQPVXJDGQWT7E45RDCH38QBK9HEJSX4X.dlmm-liquidity-router-v-0-1';
const PRICE_SCALE_BPS = 1e8;
const FEE_SCALE_BPS = 1e4;
const get_signed_bin_id = (unsigned_bin_id) => {
return unsigned_bin_id - 500;
};
const get_token_asset_name = async (token_contract) => {
const BFF_API_URL = 'https://<api-endpoint>/quotes/v1/tokens';
const response = await fetch(BFF_API_URL, {
method: 'GET',
headers: { 'Content-Type': 'application/json' }
});
const data = await response.json();
const token = data.tokens.find(t => t.contract_address === token_contract);
if (!token) throw new Error(`Token not found: ${token_contract}`);
return token.asset_name;
};
const calculate_min_dlp_for_bin = (bin, pool_fees, slippage_tolerance = 1) => {
const {
is_active_bin,
bin_price,
reserve_x,
reserve_y,
bin_shares,
x_amount,
y_amount
} = bin;
const {
x_protocol_fee,
x_provider_fee,
x_variable_fee,
y_protocol_fee,
y_provider_fee,
y_variable_fee
} = pool_fees;
const minimum_bin_shares = 10000;
const minimum_burnt_shares = 1000;
const y_amount_scaled = y_amount * PRICE_SCALE_BPS;
const reserve_y_scaled = reserve_y * PRICE_SCALE_BPS;
const add_liquidity_value = bin_price * x_amount + y_amount_scaled;
const bin_liquidity_value = bin_price * reserve_x + reserve_y_scaled;
const dlp = bin_shares === 0 || bin_liquidity_value === 0
? Math.sqrt(add_liquidity_value)
: (add_liquidity_value * bin_shares) / bin_liquidity_value;
let x_amount_fees_liquidity = 0;
let y_amount_fees_liquidity = 0;
if (is_active_bin && dlp > 0) {
const x_liquidity_fee = x_protocol_fee + x_provider_fee + x_variable_fee;
const y_liquidity_fee = y_protocol_fee + y_provider_fee + y_variable_fee;
const x_amount_withdrawable = (dlp * (reserve_x + x_amount)) / (bin_shares + dlp);
const y_amount_withdrawable = (dlp * (reserve_y + y_amount)) / (bin_shares + dlp);
if (y_amount_withdrawable > y_amount && x_amount > x_amount_withdrawable) {
const max_x_amount_fees_liquidity = ((x_amount - x_amount_withdrawable) * x_liquidity_fee) / FEE_SCALE_BPS;
x_amount_fees_liquidity = x_amount > max_x_amount_fees_liquidity ? max_x_amount_fees_liquidity : x_amount;
};
if (x_amount_withdrawable > x_amount && y_amount > y_amount_withdrawable) {
const max_y_amount_fees_liquidity = ((y_amount - y_amount_withdrawable) * y_liquidity_fee) / FEE_SCALE_BPS;
y_amount_fees_liquidity = y_amount > max_y_amount_fees_liquidity ? max_y_amount_fees_liquidity : y_amount;
};
};
const x_amount_post_fees = x_amount - x_amount_fees_liquidity;
const y_amount_post_fees = y_amount - y_amount_fees_liquidity;
const y_amount_post_fees_scaled = y_amount_post_fees * PRICE_SCALE_BPS;
const reserve_x_post_fees = reserve_x + x_amount_fees_liquidity;
const reserve_y_post_fees_scaled = (reserve_y + y_amount_fees_liquidity) * PRICE_SCALE_BPS;
const add_liquidity_value_post_fees = bin_price * x_amount_post_fees + y_amount_post_fees_scaled;
const bin_liquidity_value_post_fees = bin_price * reserve_x_post_fees + reserve_y_post_fees_scaled;
let dlp_post_fees;
if (bin_shares === 0) {
const intended_dlp = Math.sqrt(add_liquidity_value_post_fees);
dlp_post_fees = intended_dlp >= minimum_bin_shares ? intended_dlp - minimum_burnt_shares : 0;
} else if (bin_liquidity_value_post_fees === 0) {
dlp_post_fees = Math.sqrt(add_liquidity_value_post_fees);
} else {
dlp_post_fees = (add_liquidity_value_post_fees * bin_shares) / bin_liquidity_value_post_fees;
};
const min_dlp = Math.floor(dlp_post_fees * (1 - slippage_tolerance / 1e2));
return {
min_dlp,
x_amount_fees_liquidity: Math.ceil(x_amount_fees_liquidity),
y_amount_fees_liquidity: Math.ceil(y_amount_fees_liquidity)
};
};
const add_liquidity = async (
pool_contract,
pool_data,
x_token_contract,
y_token_contract,
bins_to_add,
sender_address,
slippage_tolerance = 1
) => {
const router_contract_address = LIQUIDITY_ROUTER_CONTRACT.split('.')[0];
const router_contract_name = LIQUIDITY_ROUTER_CONTRACT.split('.')[1];
const pool_contract_address = pool_contract.split('.')[0];
const pool_contract_name = pool_contract.split('.')[1];
const x_token_contract_address = x_token_contract.split('.')[0];
const x_token_contract_name = x_token_contract.split('.')[1];
const token_x_asset_name = await get_token_asset_name(x_token_contract);
const token_x_asset_info = createAssetInfo(x_token_contract_address, x_token_contract_name, token_x_asset_name);
const y_token_contract_address = y_token_contract.split('.')[0];
const y_token_contract_name = y_token_contract.split('.')[1];
const token_y_asset_name = await get_token_asset_name(y_token_contract);
const token_y_asset_info = createAssetInfo(y_token_contract_address, y_token_contract_name, token_y_asset_name);
const pool_fees = {
x_protocol_fee: pool_data.x_protocol_fee || 0,
x_provider_fee: pool_data.x_provider_fee || 0,
x_variable_fee: pool_data.x_variable_fee || 0,
y_protocol_fee: pool_data.y_protocol_fee || 0,
y_provider_fee: pool_data.y_provider_fee || 0,
y_variable_fee: pool_data.y_variable_fee || 0
};
const bin_add_positions = bins_to_add.map(bin => {
const { min_dlp, x_amount_fees_liquidity, y_amount_fees_liquidity } = calculate_min_dlp_for_bin(bin, pool_fees, slippage_tolerance);
const max_x_liquidity_fee = Math.ceil(x_amount_fees_liquidity * (1 + slippage_tolerance / 1e2));
const max_y_liquidity_fee = Math.ceil(y_amount_fees_liquidity * (1 + slippage_tolerance / 1e2));
return tupleCV({
'pool-trait': contractPrincipalCV(pool_contract_address, pool_contract_name),
'x-token-trait': contractPrincipalCV(x_token_contract_address, x_token_contract_name),
'y-token-trait': contractPrincipalCV(y_token_contract_address, y_token_contract_name),
'bin-id': intCV(get_signed_bin_id(bin.bin_id)),
'x-amount': uintCV(bin.x_amount),
'y-amount': uintCV(bin.y_amount),
'min-dlp': uintCV(min_dlp),
'max-x-liquidity-fee': uintCV(max_x_liquidity_fee),
'max-y-liquidity-fee': uintCV(max_y_liquidity_fee)
});
});
const total_x_amount = bins_to_add.reduce((sum, bin) => sum + bin.x_amount, 0);
const total_y_amount = bins_to_add.reduce((sum, bin) => sum + bin.y_amount, 0);
const post_conditions = [];
post_conditions.push(
makeStandardFungiblePostCondition(
sender_address,
FungibleConditionCode.LessEqual,
total_x_amount.toString(),
token_x_asset_info
)
);
post_conditions.push(
makeStandardFungiblePostCondition(
sender_address,
FungibleConditionCode.LessEqual,
total_y_amount.toString(),
token_y_asset_info
)
);
bins_to_add.forEach(bin => {
if (bin.has_ever_added_to_bin) {
post_conditions.push(
makeStandardNonFungiblePostCondition(
sender_address,
NonFungibleConditionCode.Sends,
createAssetInfo(pool_contract_address, pool_contract_name, 'pool-token-id'),
tupleCV({
'token-id': uintCV(bin.bin_id),
'owner': principalCV(sender_address)
})
)
);
};
});
const tx_options = {
contractAddress: router_contract_address,
contractName: router_contract_name,
functionName: 'add-liquidity-multi',
functionArgs: [listCV(bin_add_positions)],
senderKey: STACKS_PRIVATE_KEY,
network: stacks_network,
fee: STACKS_TRANSACTION_FEE,
postConditions: post_conditions,
postConditionMode: PostConditionMode.Deny,
anchorMode: AnchorMode.Any
};
const transaction = await makeContractCall(tx_options);
const response = await broadcastTransaction(transaction, stacks_network);
return response;
};