πŸ› οΈHODLMM API Documentation

This page provides endpoints and sample code for integrating with HODLMM.

Adding Liquidity

1

Getting Pool Bins

Before adding liquidity, you need to get the pool's bins. You can do this via the following endpoint:

  • /quotes/bins/{pool_id}: Returns all bins for a pool

This endpoint is a GET request.

const BFF_API_URL = 'https://<api-endpoint>/quotes/bins';

const get_pool_bins = async (pool_id) => {
  const response = await fetch(BFF_API_URL + `/${pool_id}`, {
    method: 'GET',
    headers: { 'Content-Type': 'application/json' }
  });

  const data = await response.json();
  return data;
};
2

Getting User Position Bins

After getting pool bins, you need to get the user's position bins. You can do this via the following endpoint:

  • /bff/users/{user_address}/positions/{pool_id}/bins: Returns position bins for a user for a pool

This endpoint is a GET request.

const BFF_API_URL = 'https://<api-endpoint>/bff/users';

const get_user_position_bins = async (user_address, pool_id) => {
  const response = await fetch(BFF_API_URL + `/${user_address}/positions/${pool_id}/bins`, {
    method: 'GET',
    headers: { 'Content-Type': 'application/json' }
  });

  const data = await response.json();
  return data;
};
3

Preparing Bins for Add Liquidity

After getting pool bins and user positions, you need to prepare the bins with the amounts you want to add to each bin.

const bins_to_add = [
  {
    bin_id: 500,
    x_amount: 10000000000,
    y_amount: 1750000000
  }
];

const prepare_bins_for_add = (pool_bins, user_positions, active_bin_id, bins_to_add) => {
  const user_position_bins_map = new Map(
    (Array.isArray(user_positions?.bins)
      ? user_positions.bins
      : []).map(bin => [bin.bin_id, bin]));
  
  return bins_to_add.map(add_bin => {
    const pool_bin = pool_bins.bins.find(b => b.bin_id === add_bin.bin_id);
    
    if (!pool_bin) throw new Error(`Bin ${add_bin.bin_id} not found in pool`);
    
    if (add_bin.bin_id < active_bin_id && add_bin.x_amount > 0) {
      throw new Error('Only y_token can be added to bins below the active bin');
    };
    
    if (add_bin.bin_id > active_bin_id && add_bin.y_amount > 0) {
      throw new Error('Only x_token can be added to bins above the active bin');
    };
    
    if (add_bin.bin_id === active_bin_id && add_bin.x_amount === 0 && add_bin.y_amount === 0) {
      throw new Error('Active bin requires at least one token amount to be greater than 0');
    };
    
    return {
      is_active_bin: add_bin.bin_id === active_bin_id,
      bin_id: add_bin.bin_id,
      x_amount: add_bin.x_amount,
      y_amount: add_bin.y_amount,
      bin_price: Number(pool_bin.price),
      reserve_x: Number(pool_bin.reserve_x),
      reserve_y: Number(pool_bin.reserve_y),
      bin_shares: Number(pool_bin.liquidity ?? 0),
      user_liquidity: user_position_bins_map.get(add_bin.bin_id)?.user_liquidity || 0,
      has_ever_added_to_bin: user_position_bins_map.has(add_bin.bin_id)
    };
  });
};
4

Executing Add Liquidity

After preparing the bins to add, you can execute the add by calling the liquidity router contract with the necessary parameters.

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-0';

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/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;
  };
  
  return Math.floor(dlp_post_fees * (1 - slippage_tolerance / 1e2));
};

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 = calculate_min_dlp_for_bin(bin, pool_fees, slippage_tolerance);
    const max_x_liquidity_fee = Math.floor((bin.x_amount * slippage_tolerance) / 1e2);
    const max_y_liquidity_fee = Math.floor((bin.y_amount * 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;
};

Withdrawing Liquidity

1

Getting User Position Bins

Before withdrawing liquidity, you need to get the user's position bins. You can do this via the following endpoint:

  • /bff/users/{user_address}/positions/{pool_id}/bins: Returns position bins for a user for a pool

This endpoint is a GET request.

const BFF_API_URL = 'https://<api-endpoint>/bff/users';

const get_user_position_bins = async (user_address, pool_id) => {
  const response = await fetch(BFF_API_URL + `/${user_address}/positions/${pool_id}/bins`, {
    method: 'GET',
    headers: { 'Content-Type': 'application/json' }
  });

  const data = await response.json();
  return data;
};
2

Preparing Bins for Withdraw Liquidity

After getting user positions, you need to prepare the bins with the percentage you want to withdraw from each bin.

const prepare_bins_for_withdraw = (user_positions, withdrawal_percentage) => {
  const user_position_bins_map = new Map(
    (Array.isArray(user_positions?.bins)
      ? user_positions.bins
      : []).map(bin => [bin.bin_id, bin]));

  return user_positions.bins
    .filter(bin => bin.user_liquidity > 0)
    .map(bin => ({
      bin_id: bin.bin_id,
      user_liquidity: bin.user_liquidity,
      liquidity: bin.liquidity,
      reserve_x: bin.reserve_x,
      reserve_y: bin.reserve_y,
      withdrawal_percentage,
      has_ever_added_to_bin: user_position_bins_map.has(bin.bin_id)
    }));
};
3

Executing Withdraw Liquidity

After preparing the bins to withdraw, you can execute the withdrawal by calling the liquidity router contract with the necessary parameters.

const {
  intCV,
  uintCV,
  listCV,
  tupleCV,
  principalCV,
  contractPrincipalCV,
  createAssetInfo,
  makeStandardFungiblePostCondition,
  makeContractFungiblePostCondition,
  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-0';

const get_signed_bin_id = (unsigned_bin_id) => {
  return unsigned_bin_id - 500;
};

const calculate_bin_withdrawal_amounts = (bin, withdrawal_percentage, slippage_tolerance = 1) => {
  if (withdrawal_percentage < 0 || withdrawal_percentage > 100) throw new Error('withdrawal_percentage must be between 0 and 100');
  if (bin.user_liquidity === 0 || bin.liquidity === 0) return { liquidity_to_remove: 0, min_x_amount: 0, min_y_amount: 0 };

  const percentage_decimal = withdrawal_percentage / 1e2;
  const liquidity_to_remove = Math.floor(bin.user_liquidity * percentage_decimal);
  const percentage_of_bin = liquidity_to_remove / bin.liquidity;

  const slippage_multiplier = 1 - (slippage_tolerance / 1e2);
  const min_x_amount = Math.floor(bin.reserve_x * percentage_of_bin * slippage_multiplier);
  const min_y_amount = Math.floor(bin.reserve_y * percentage_of_bin * slippage_multiplier);

  return {
    liquidity_to_remove,
    min_x_amount,
    min_y_amount
  };
};

const get_token_asset_name = async (token_contract) => {
  const BFF_API_URL = 'https://<api-endpoint>/quotes/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 withdraw_liquidity = async (
  pool_contract,
  x_token_contract,
  y_token_contract,
  bins_to_withdraw,
  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 pool_contract_asset_name = 'pool-token';
  const pool_contract_asset_info = createAssetInfo(pool_contract_address, pool_contract_name, pool_contract_asset_name);
  
  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 { bin_withdrawal_positions, total_liquidity_removed, total_min_x_amount, total_min_y_amount } = bins_to_withdraw.reduce(
    (acc, bin) => {
      const amounts = calculate_bin_withdrawal_amounts(bin, bin.withdrawal_percentage, slippage_tolerance);
      
      return {
        bin_withdrawal_positions: [
          ...acc.bin_withdrawal_positions,
          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)),
            'amount': uintCV(amounts.liquidity_to_remove),
            'min-x-amount': uintCV(amounts.min_x_amount),
            'min-y-amount': uintCV(amounts.min_y_amount)
          })
        ],
        total_liquidity_removed: acc.total_liquidity_removed + amounts.liquidity_to_remove,
        total_min_x_amount: acc.total_min_x_amount + amounts.min_x_amount,
        total_min_y_amount: acc.total_min_y_amount + amounts.min_y_amount
      };
    },
    { bin_withdrawal_positions: [], total_liquidity_removed: 0, total_min_x_amount: 0, total_min_y_amount: 0 }
  );

  const post_conditions = [];

  post_conditions.push(
    makeStandardFungiblePostCondition(
      sender_address,
      FungibleConditionCode.Equal,
      total_liquidity_removed.toString(),
      pool_contract_asset_info
    )
  );

  post_conditions.push(
    makeContractFungiblePostCondition(
      pool_contract_address,
      pool_contract_name,
      FungibleConditionCode.GreaterEqual,
      total_min_x_amount.toString(),
      token_x_asset_info
    )
  );

  post_conditions.push(
    makeContractFungiblePostCondition(
      pool_contract_address,
      pool_contract_name,
      FungibleConditionCode.GreaterEqual,
      total_min_y_amount.toString(),
      token_y_asset_info
    )
  );

  bins_to_withdraw.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: 'withdraw-liquidity-multi',
    functionArgs: [listCV(bin_withdrawal_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;
};

Moving Liquidity

1

Getting Pool Bins

Before moving liquidity, you need to get the pool's bins. You can do this via the following endpoint:

  • /quotes/bins/{pool_id}: Returns all bins for a pool

This endpoint is a GET request.

const BFF_API_URL = 'https://<api-endpoint>/quotes/bins';

const get_pool_bins = async (pool_id) => {
  const response = await fetch(BFF_API_URL + `/${pool_id}`, {
    method: 'GET',
    headers: { 'Content-Type': 'application/json' }
  });

  const data = await response.json();
  return data;
};
2

Getting User Position Bins

After getting pool bins, you need to get the user's position bins. You can do this via the following endpoint:

  • /bff/users/{user_address}/positions/{pool_id}/bins: Returns position bins for a user for a pool

This endpoint is a GET request.

const BFF_API_URL = 'https://<api-endpoint>/bff/users';

const get_user_position_bins = async (user_address, pool_id) => {
  const response = await fetch(BFF_API_URL + `/${user_address}/positions/${pool_id}/bins`, {
    method: 'GET',
    headers: { 'Content-Type': 'application/json' }
  });

  const data = await response.json();
  return data;
};
3

Preparing Bins for Move Liquidity

After getting pool bins and user positions, you need to prepare the bins you want to move liquidity from, to, and the amount of liquidity to move.

const bins_to_move = [
  {
    from_bin_id: 480,
    to_bin_id: 498,
    amount: 12848399
  }
];

const prepare_bins_for_move = (pool_bins, user_positions, active_bin_id, bins_to_move) => {
  const user_position_bins_map = new Map(
    (Array.isArray(user_positions?.bins)
      ? user_positions.bins
      : []).map(bin => [bin.bin_id, bin]));
  
  return bins_to_move.map(move => {
    const { from_bin_id, to_bin_id, amount } = move;
    
    const from_bin = user_positions.bins.find(b => b.bin_id === from_bin_id);
    if (!from_bin) throw new Error('Unable to find from_bin in user positions');
    if (from_bin.user_liquidity === 0) throw new Error('User has no liquidity in from_bin');
    if (amount > from_bin.user_liquidity) throw new Error('Amount exceeds user liquidity in from_bin');
  
    const to_bin = pool_bins.bins.find(b => b.bin_id === to_bin_id);
    if (!to_bin) throw new Error('Unable to find to_bin in pool bins');

    if (from_bin_id === to_bin_id) throw new Error('Cannot move liquidity to the same bin');
    
    const x_amount = Math.floor((amount * from_bin.reserve_x) / from_bin.liquidity);
    const y_amount = Math.floor((amount * from_bin.reserve_y) / from_bin.liquidity);

    if (to_bin_id < active_bin_id && x_amount > 0) {
      throw new Error('Only y_token can be added to bins below the active bin');
    };
    
    if (to_bin_id > active_bin_id && y_amount > 0) {
      throw new Error('Only x_token can be added to bins above the active bin');
    };
    
    if (to_bin_id === active_bin_id && x_amount === 0 && y_amount === 0) {
      throw new Error('Active bin requires at least one token amount to be greater than 0');
    };
    
    return {
      from_bin_id,
      to_bin_id,
      amount,
      x_amount,
      y_amount,
      bin_price: Number(to_bin.price),
      reserve_x: Number(to_bin.reserve_x),
      reserve_y: Number(to_bin.reserve_y),
      bin_shares: Number(to_bin.liquidity ?? 0),
      is_active_bin: to_bin_id === active_bin_id,
      has_ever_added_to_from_bin: user_position_bins_map.has(from_bin_id),
      has_ever_added_to_to_bin: user_position_bins_map.has(to_bin_id)
    };
  });
};
4

Executing Move Liquidity

After preparing the bins to move, you can execute the move by calling the liquidity router contract with the necessary parameters.

const {
  intCV,
  uintCV,
  tupleCV,
  listCV,
  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-0';

const PRICE_SCALE_BPS = 1e8;
const FEE_SCALE_BPS = 1e4;

const get_signed_bin_id = (unsigned_bin_id) => {
  return unsigned_bin_id - 500;
};

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;
  };
  
  return Math.floor(dlp_post_fees * (1 - slippage_tolerance / 1e2));
};

const move_liquidity = async (
  pool_contract,
  pool_data,
  x_token_contract,
  y_token_contract,
  bins_to_move,
  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 pool_contract_asset_name = 'pool-token';
  const pool_contract_asset_info = createAssetInfo(pool_contract_address, pool_contract_name, pool_contract_asset_name);
  
  const x_token_contract_address = x_token_contract.split('.')[0];
  const x_token_contract_name = x_token_contract.split('.')[1];

  const y_token_contract_address = y_token_contract.split('.')[0];
  const y_token_contract_name = y_token_contract.split('.')[1];

  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_move_positions = bins_to_move.map(bin => {
    const min_dlp = calculate_min_dlp_for_bin(bin, pool_fees, slippage_tolerance);
    const max_x_liquidity_fee = Math.floor((bin.x_amount * slippage_tolerance) / 1e2);
    const max_y_liquidity_fee = Math.floor((bin.y_amount * 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),
      'from-bin-id': intCV(get_signed_bin_id(bin.from_bin_id)),
      'to-bin-id': intCV(get_signed_bin_id(bin.to_bin_id)),
      'amount': uintCV(bin.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_liquidity_removed = bins_to_move.reduce((sum, bin) => sum + bin.amount, 0);

  const post_conditions = [];

  post_conditions.push(
    makeStandardFungiblePostCondition(
      sender_address,
      FungibleConditionCode.Equal,
      total_liquidity_removed.toString(),
      pool_contract_asset_info
    )
  );

  bins_to_move.forEach(bin => {
    if (bin.has_ever_added_to_from_bin) {
      post_conditions.push(
        makeStandardNonFungiblePostCondition(
          sender_address,
          NonFungibleConditionCode.Sends,
          createAssetInfo(pool_contract_address, pool_contract_name, 'pool-token-id'),
          tupleCV({
            'token-id': uintCV(bin.from_bin_id),
            'owner': principalCV(sender_address)
          })
        )
      );
    };
    
    if (bin.has_ever_added_to_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.to_bin_id),
            'owner': principalCV(sender_address)
          })
        )
      );
    };
  });

  const tx_options = {
    contractAddress: router_contract_address,
    contractName: router_contract_name,
    functionName: 'move-liquidity-multi',
    functionArgs: [listCV(bin_move_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;
};

Swapping Tokens

1

Getting a Quote

Before executing a swap, you need to get a quote. You can do this via the following endpoints:

  • /quotes/quote: Returns the best route

  • /quotes/quote/multi: Returns all routes

These endpoints are POST requests and the request body is the same for both.

const BFF_API_URL = 'https://<api-endpoint>/quotes/quote/multi';

const get_quote = async (input_token, output_token, amount_in, amm_strategy = 'best', slippage_tolerance = 1) => {
  const response = await fetch(BFF_API_URL, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      input_token, // Input token contract address
      output_token, // Output token contract address
      amount_in, // Input token amount (in uints)
      amm_strategy, // AMM strategy to use (optional, default: best)
      slippage_tolerance // Slippage tolerance (optional, 1 = 1%)
    })
  });

  const data = await response.json();
  return data;
};
2

Getting Swap Parameters

After getting a quote, you need to generate the swap parameters. You can do this via the following endpoint:

  • /quotes/swap: Returns the swap parameters for a route

This endpoint is a POST request.

const BFF_API_URL = 'https://<api-endpoint>/quotes/swap';

const get_swap_params = async (amount_in, input_token, output_token, route) => {
  const response = await fetch(BFF_API_URL, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      execution_path: route.execution_path,
      amount_in,
      amount_out: route.amount_out,
      input_token,
      output_token,
      input_token_decimals: route.input_token_decimals,
      output_token_decimals: route.output_token_decimals,
      slippage_tolerance: route.slippage_tolerance
    })
  });

  const data = await response.json();
  return data;
};
3

Executing a Swap

After getting the swap parameters, you can execute the swap by calling the swap router contract with the necessary parameters.

const {
  intCV,
  uintCV,
  trueCV,
  falseCV,
  listCV,
  tupleCV,
  contractPrincipalCV,
  createAssetInfo,
  makeStandardFungiblePostCondition,
  makeContractFungiblePostCondition,
  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 execute_swap = async (params, sender_address, bin_slippage_tolerance = 10) => {
  const swap_contract_address = params.swap_contract.split('.')[0];
  const swap_contract_name = params.swap_contract.split('.')[1];

  let converted_post_conditions = [];
  for (let i = 0; i < params.post_conditions?.length; i++) {
    const pc = params.post_conditions[i];
    
    const token_contract_address = pc.token_contract.split('.')[0];
    const token_contract_name = pc.token_contract.split('.')[1];
    const token_asset_info = createAssetInfo(token_contract_address, token_contract_name, pc.token_asset_name);

    let amount = pc.amount;
    const is_last_post_condition = i === params.post_conditions?.length - 1;
    if (is_last_post_condition && params.swap_parameters_typed?.length) {
      const total_min_received = params.swap_parameters_typed.reduce(
        (sum, param) => sum + BigInt(param.value['min-received'].value),
        BigInt(0)
      );
      amount = total_min_received.toString()
    };

    const condition_code = pc.condition_code === 'less_than_or_equal_to'
      ? FungibleConditionCode.LessEqual
      : FungibleConditionCode.GreaterEqual;

    if (pc.sender_address === 'tx-sender') {
      converted_post_conditions.push(
        makeStandardFungiblePostCondition(
          sender_address,
          condition_code,
          amount,
          token_asset_info
        )
      );
    } else {
      converted_post_conditions.push(
        makeContractFungiblePostCondition(
          pc.sender_address.split('.')[0],
          pc.sender_address.split('.')[1],
          condition_code,
          amount,
          token_asset_info
        )
      );
    };
  };

  let converted_function_args = [];
  if (params.swap_parameters_typed && params.swap_parameters_typed.length > 0) {
    const converted_swap_parameters = params.swap_parameters_typed.map(param => {
      const value = param.value;
      return tupleCV({
        'pool-trait': contractPrincipalCV(
          value['pool-trait'].value.split('.')[0],
          value['pool-trait'].value.split('.')[1]
        ),
        'x-token-trait': contractPrincipalCV(
          value['x-token-trait'].value.split('.')[0],
          value['x-token-trait'].value.split('.')[1]
        ),
        'y-token-trait': contractPrincipalCV(
          value['y-token-trait'].value.split('.')[0],
          value['y-token-trait'].value.split('.')[1]
        ),
        'expected-bin-id': intCV(value['expected-bin-id'].value),
        'amount': uintCV(value['amount'].value),
        'min-received': uintCV(value['min-received'].value),
        'x-for-y': value['x-for-y'].type === 'true' ? trueCV() : falseCV(),
      });
    });

    converted_function_args = [
      listCV(converted_swap_parameters),
      uintCV(bin_slippage_tolerance)
    ];
  };

  const tx_options = {
    contractAddress: swap_contract_address,
    contractName: swap_contract_name,
    functionName: params.function_name,
    functionArgs: converted_function_args,
    senderKey: STACKS_PRIVATE_KEY,
    network: stacks_network,
    fee: STACKS_TRANSACTION_FEE,
    postConditions: converted_post_conditions,
    postConditionMode: PostConditionMode.Deny,
    anchorMode: AnchorMode.Any
  };

  const transaction = await makeContractCall(tx_options);
  const response = await broadcastTransaction(transaction, stacks_network);
  
  return response;
};

Getting Data

Getting Available Tokens

You can get all available tokens via the following endpoint:

  • /quotes/tokens: Returns all available tokens

This endpoint is a GET request.

const BFF_API_URL = 'https://<api-endpoint>/quotes/tokens';

const get_available_tokens = async () => {
  const response = await fetch(BFF_API_URL, {
    method: 'GET',
    headers: { 'Content-Type': 'application/json' }
  });

  const data = await response.json();
  return data;
};

Getting Available Pools

You can get all available pools via the following endpoint:

  • /quotes/pools: Returns all available pools

This endpoint is a GET request.

const BFF_API_URL = 'https://<api-endpoint>/quotes/pools';

const get_available_pools = async () => {
  const response = await fetch(BFF_API_URL, {
    method: 'GET',
    headers: { 'Content-Type': 'application/json' }
  });

  const data = await response.json();
  return data;
};

Getting Available Pairs

You can get all available trading pairs via the following endpoint:

  • /quotes/pairs: Returns all available trading pairs

This endpoint is a GET request.

const BFF_API_URL = 'https://<api-endpoint>/quotes/pairs';

const get_available_pairs = async () => {
  const response = await fetch(BFF_API_URL, {
    method: 'GET',
    headers: { 'Content-Type': 'application/json' }
  });

  const data = await response.json();
  return data;
};

Getting Pool Data

You can get all data for a pool via the following endpoint:

  • /pools/{pool_id}: Returns all data for a pool

This endpoint is a GET request.

const BFF_API_URL = 'https://<api-endpoint>/pools';

const get_pool_data = async (pool_id) => {
  const response = await fetch(BFF_API_URL + `/${pool_id}`, {
    method: 'GET',
    headers: { 'Content-Type': 'application/json' }
  });

  const data = await response.json();
  return data;
};

Getting Pool Bins

You can get all bins for a pool via the following endpoint:

  • /quotes/bins/{pool_id}: Returns all bins for a pool

This endpoint is a GET request.

const BFF_API_URL = 'https://<api-endpoint>/quotes/bins';

const get_pool_bins = async (pool_id) => {
  const response = await fetch(BFF_API_URL + `/${pool_id}`, {
    method: 'GET',
    headers: { 'Content-Type': 'application/json' }
  });

  const data = await response.json();
  return data;
};

Getting Pool Bin Price History

You can get the bin price history for a pool via the following endpoint:

  • /bff/pools/{pool_id}/bin-price-history: Returns bin price history for a pool

This endpoint is a GET request.

const BFF_API_URL = 'https://<api-endpoint>/bff/pools';

const get_pool_bin_price_history = async (pool_id) => {
  const response = await fetch(BFF_API_URL + `/${pool_id}/bin-price-history`, {
    method: 'GET',
    headers: { 'Content-Type': 'application/json' }
  });

  const data = await response.json();
  return data;
};

Getting User Position for a Pool

You can get the position for a user for a pool via the following endpoint:

  • /bff/users/{user_address}/positions/{pool_id}: Returns position for a user for a pool

This endpoint is a GET request.

const BFF_API_URL = 'https://<api-endpoint>/bff/users';

const get_user_position = async (user_address, pool_id) => {
  const response = await fetch(BFF_API_URL + `/${user_address}/positions/${pool_id}`, {
    method: 'GET',
    headers: { 'Content-Type': 'application/json' }
  });

  const data = await response.json();
  return data;
};

Getting User Position Bins for a Pool

You can get the position bins for a user for a pool via the following endpoint:

  • /bff/users/{user_address}/positions/{pool_id}/bins: Returns position bins for a user for a pool

This endpoint is a GET request.

const BFF_API_URL = 'https://<api-endpoint>/bff/users';

const get_user_position_bins = async (user_address, pool_id) => {
  const response = await fetch(BFF_API_URL + `/${user_address}/positions/${pool_id}/bins`, {
    method: 'GET',
    headers: { 'Content-Type': 'application/json' }
  });

  const data = await response.json();
  return data;
};

Last updated