3

I have invested in UniSwap-V3 Liquidity Pool.

And how to track unclaimed fees programmatically?

I plan to have server-backend API fetch data to track unclaimed fees ($27.92 in picture)

enter image description here

I have tried to query position from EthScan Read Contract (in as second picture), but don't know how to get unclaimed fees from this data??

https://etherscan.io/address/0xc36442b4a4522e871399cd717abdd847ab11fe88#readContract

enter image description here

5 Answers 5

4

I solved this by going through powershell. I query the infura account that the uniswap interface queries to. Here is a powershell script for an DAI/USDC pool. You have to change some things for this to work for your pool.

The easiest way to do this is to go inspect the Uniswap webpage, find an "eth_call" query (not a "eth_blocknumber", and copy the data there. What you need to copy:

  • Your public key (green)
  • The data that is passed with the query (orange)

enter image description here

(You don't need to change this first part)

$r = Invoke-WebRequest -Uri "https://mainnet.infura.io/v3/099fc58e0de9451d80b18d7c74caa7c1" `
-Method "POST" `
-Headers @{
"method"="POST"
  "authority"="mainnet.infura.io"
  "scheme"="https"
  "path"="/v3/099fc58e0de9451d80b18d7c74caa7c1"
  "sec-ch-ua"="`" Not;A Brand`";v=`"99`", `"Google Chrome`";v=`"91`", `"Chromium`";v=`"91`""
  "accept"="application/json"
  "sec-ch-ua-mobile"="?0"
  "user-agent"="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.77 Safari/537.36"
  "origin"="https://app.uniswap.org"
  "sec-fetch-site"="cross-site"
  "sec-fetch-mode"="cors"
  "sec-fetch-dest"="empty"
  "referer"="https://app.uniswap.org/"
  "accept-encoding"="gzip, deflate, br"
  "accept-language"="nl-NL,nl;q=0.9,en-US;q=0.8,en;q=0.7,fr;q=0.6"
} `

update this part with your own data

-ContentType "application/json" `
-Body "[{`"jsonrpc`":`"2.0`",`"id`":224,`"method`":`"eth_call`",`
  "params`":[{`
    "from`":`"*PUBLIC-KEY-HERE*",` 
    "to`":`"0xc36442b4a4522e871399cd717abdd847ab11fe88`",` 
    "data`":`"*DATA-HERE*"
  },`"latest`"]}]"

Lastly, you need to make sense of the response. This code should parse the hex numbers to integers, but you need to scale them for your currencies. In the code below, it's DAI/USDC. You can just look at the value it returns, and change the powers so that you get the correct result. (You can take a look here if you want more info)

#convert the retrieved hex codes to your currency
$x = $r.Content | Out-String | ConvertFrom-Json 
$res = $x.result
$length = $res.length
$l1 = [Int64]$res.Substring(0,66)
$temp= '0x'+$res.Substring(66,$length-66)
$l2=[Int64]$temp

#change these powers to scale
$l1Scaled= $l1/[Math]::Pow(10,18)
$l2Scaled= $usdt/[Math]::Pow(10,6)
$out = 'Liquidity1:'+$l1Scaled+';Liquidity2:'+$l2Scaled
$out

Hope this works for you too.

EDIT to answer Richard's question: So take this random pool. Inspect the page and go to the network tab. enter image description here

Here you see a lot of outgoing queries. If you scroll down in the "Header" subtab, you can see the method. Most of these are probably "eth_blockNumber", but you need one that says "eth_call" as shown in the first picture. Once you find one of these, expand the first element of "Request Payload".

"0" -> "params" -> "0" -> "data"

Then you just copy whatever is in the data field (orange in the first picture).

2
  • Thank you for the answer. Are you able to supply a link to give a hint as to what should go in the data element? Jun 7, 2021 at 11:04
  • 1
    I edited the post to (hopefully) answer your question.
    – Gwen_vere
    Jun 9, 2021 at 17:18
3

By calling the "DeBank Open API".

Exmaple:

  • Random Wallet Address: 0x6741ad42f42564c3f21587d080880b2e79731eb1
  • Uniswap V3 Positions: DYDX + WETH
  • API call : https://openapi.debank.com/v1/user/protocol?id=0x6741ad42f42564c3f21587d080880b2e79731eb1&protocol_id=uniswap3

The DeBank Open API output is as below:

{
    "id":"uniswap3",
    "chain":"eth",
    "name":"Uniswap V3",
    "site_url":"https://app.uniswap.org",
    "logo_url":"https://static.debank.com/image/project/logo_url/uniswap3/87a541b3b83b041c8d12119e5a0d19f0.png",
    "has_supported_portfolio":true,
    "tvl":2419605554.113241,
    "portfolio_item_list":[
        {
            "stats":{
                "asset_usd_value":22484.275763599115,
                "debt_usd_value":0,
                "net_usd_value":22484.275763599115,
                "daily_yield_usd_value":0,
                "daily_cost_usd_value":0,
                "daily_net_yield_usd_value":0
            },
            "update_at":1632030470.1883495,
            "name":"Liquidity Pool",
            "detail_types":[
                "common"
            ],
            "detail":{
                "supply_token_list":[
                    {
                        "id":"0x92d6c1e31e14520e676a687f0a93788b716beff5",
                        "chain":"eth",
                        "name":"dYdX",
                        "symbol":"DYDX",
                        "display_symbol":null,
                        "optimized_symbol":"DYDX",
                        "decimals":18,
                        "logo_url":"https://static.debank.com/image/token/logo_url/0x92d6c1e31e14520e676a687f0a93788b716beff5/8fd712b1bd4622cb7ff3334cb61ce583.png",
                        "protocol_id":"",
                        "price":15.21,
                        "is_verified":true,
                        "is_core":true,
                        "is_wallet":true,
                        "time_at":1626053832,
                        "amount":887.3052320067715
                    },
                    {
                        "id":"0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2",
                        "chain":"eth",
                        "name":"Wrapped Ether",
                        "symbol":"WETH",
                        "display_symbol":null,
                        "optimized_symbol":"WETH",
                        "decimals":18,
                        "logo_url":"https://static.debank.com/image/token/logo_url/0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2/61844453e63cf81301f845d7864236f6.png",
                        "protocol_id":"",
                        "price":3427.29,
                        "is_verified":true,
                        "is_core":true,
                        "is_wallet":true,
                        "time_at":1513077455,
                        "amount":2.532022674845794
                    }
                ],
                "reward_token_list":[
                    {
                        "id":"0x92d6c1e31e14520e676a687f0a93788b716beff5",
                        "chain":"eth",
                        "name":"dYdX",
                        "symbol":"DYDX",
                        "display_symbol":null,
                        "optimized_symbol":"DYDX",
                        "decimals":18,
                        "logo_url":"https://static.debank.com/image/token/logo_url/0x92d6c1e31e14520e676a687f0a93788b716beff5/8fd712b1bd4622cb7ff3334cb61ce583.png",
                        "protocol_id":"",
                        "price":15.21,
                        "is_verified":true,
                        "is_core":true,
                        "is_wallet":true,
                        "time_at":1626053832,
                        "amount":10.044515602709696
                    },
                    {
                        "id":"0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2",
                        "chain":"eth",
                        "name":"Wrapped Ether",
                        "symbol":"WETH",
                        "display_symbol":null,
                        "optimized_symbol":"WETH",
                        "decimals":18,
                        "logo_url":"https://static.debank.com/image/token/logo_url/0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2/61844453e63cf81301f845d7864236f6.png",
                        "protocol_id":"",
                        "price":3427.29,
                        "is_verified":true,
                        "is_core":true,
                        "is_wallet":true,
                        "time_at":1513077455,
                        "amount":0.04598680274697121
                    }
                ]
            },
            "proxy_detail":{

            }
        }
    ]
}

Now you can track the unclaimed fees at the reward_token_list (a for loop would help):

response['portfolio_item_list'][0]['detail']['reward_token_list']

DYDX/WETH V3 Position

1
  • The debank API for this call is no longer free unfortunately.
    – someuser
    Sep 6, 2022 at 11:48
2

In order to get the right amount of uncollected fees for each token of your position, you should query the position and do a bunch of math. In the alternative, you can call the smart contract directly and let it compute for you.

In order to proceed, you will need the ABI of the smart contract available here. Or you can check it from Etherscan here.

Save it in a json file and the import where you need it.

Here is the code

import * as UNIv3 from "./univ3.abi.json"

async function collectUnclaimedFees(positionId: string) {
  const infuraProvider = new ethers.providers.InfuraProvider(
    Chain.MAINNET,
    process.env.INFURA_API_KEY || ""
  )

  const contract = new ethers.Contract(
    "0xC36442b4a4522E871399CD717aBDD847Ab11FE88",
    UNIv3,
    infuraProvider
  )

  // MAX_VALUE UINT128, to Collect All value
  const MAX = ethers.BigNumber.from(2).pow(128).sub(1).toString() // "340282366920938463463374607431768211455"

  // Your address here (owner of the position)
  const FROM = "0x*****"

  const encoded = {
    // The id of the minted NFT (you can find it on etherscan)
    tokenId: positionId,
    recipient: FROM,
    amount0Max: MAX,
    amount1Max: MAX,
  }

  const trx = await contract.callStatic.collect(encoded)

  const formatUnits = (amt: BigNumber, units?: number): string =>
    ethers.utils
      .formatUnits(ethers.BigNumber.from(amt).toString(), units || 18)
      .toString()

  // Get the units from the token, in this case the pool was a WBTC-ETH, so token0 has 8 units and token1 has 18, or the default value
  const amount0 = formatUnits(trx.amount0, 8)
  const amount1 = formatUnits(trx.amount1)

  console.log(`Token 0: ${amount0} | Token 1: ${amount1}`)
}

Results:

enter image description here

enter image description here

Further steps:

  • get the current market price of token0 and token1
  • get the $ of unclaimed fees
1

I have written this script to help users get their unclaimed fees of a position its in .mjs format js/typescript so easily convertible to either

import { JSBI } from "@uniswap/sdk";
import { ethers } from 'ethers'
import * as fs from 'fs';


    // ERC20 json abi file
let ERC20Abi = fs.readFileSync('<path>/ERC20.json');
const ERC20 = JSON.parse(ERC20Abi);

    // V3 pool abi json file
let pool = fs.readFileSync('<path>/V3PairAbi.json');
const IUniswapV3PoolABI = JSON.parse(pool);

    // V3 factory abi json
let facto = fs.readFileSync('<path>/V3factory.json');
const IUniswapV3FactoryABI = JSON.parse(facto);

    // V3 NFT manager abi
let NFT = fs.readFileSync('<path>/UniV3NFT.json');
const IUniswapV3NFTmanagerABI = JSON.parse(NFT);


const provider = new ethers.providers.JsonRpcProvider("https://eth-mainnet.alchemyapi.io/v2/<API_Key>")


    // V3 standard addresses (different for celo)
const factory = "0x1F98431c8aD98523631AE4a59f267346ea31F984";
const NFTmanager = "0xC36442b4a4522E871399CD717aBDD847Ab11FE88";



async function getData(tokenID){
    var FactoryContract = new ethers.Contract(factory, IUniswapV3FactoryABI, provider);

    var NFTContract =  new ethers.Contract(NFTmanager, IUniswapV3NFTmanagerABI, provider);
    var position = await NFTContract.positions(tokenID);
    
    var token0contract =  new ethers.Contract(position.token0, ERC20, provider);
    var token1contract =  new ethers.Contract(position.token1, ERC20, provider);
    var Decimal0 = await token0contract.decimals();
    var Decimal1 = await token1contract.decimals();
    
    var token0sym = await token0contract.symbol();
    var token1sym = await token1contract.symbol();
    
    var V3pool = await FactoryContract.getPool(position.token0, position.token1, position.fee);
    var poolContract = new ethers.Contract(V3pool, IUniswapV3PoolABI, provider);

    var slot0 = await poolContract.slot0();
    var tickLow = await poolContract.ticks(position.tickLower.toString());
    var tickHi = await poolContract.ticks(position.tickUpper.toString());
    
    var feeGrowthGlobal0 = await poolContract.feeGrowthGlobal0X128();
    var feeGrowthGlobal1 = await poolContract.feeGrowthGlobal1X128();
    
    var pairName = token0sym +"/"+ token1sym;
    
    var PoolInfo = {
    "Pair": pairName, 
    "Decimal0": Decimal0, 
    "Decimal1": Decimal1, 
    "tickCurrent": slot0.tick, 
    "tickLow": position.tickLower, 
    "tickHigh": position.tickUpper, 
    "liquidity": position.liquidity.toString(), 
    "feeGrowth0Low": tickLow.feeGrowthOutside0X128.toString(),  
    "feeGrowth0Hi": tickHi.feeGrowthOutside0X128.toString(), 
    "feeGrowth1Low": tickLow.feeGrowthOutside1X128.toString(),  
    "feeGrowth1Hi": tickHi.feeGrowthOutside1X128.toString(),
    "feeGrowthInside0LastX128": position.feeGrowthInside0LastX128.toString(),
    "feeGrowthInside1LastX128": position.feeGrowthInside1LastX128.toString(),
    "feeGrowthGlobal0X128": feeGrowthGlobal0.toString(), 
    "feeGrowthGlobal1X128": feeGrowthGlobal1.toString()}
    
    return PoolInfo
}


const ZERO = JSBI.BigInt(0);
const Q128 = JSBI.exponentiate(JSBI.BigInt(2), JSBI.BigInt(128));
const Q256 = JSBI.exponentiate(JSBI.BigInt(2), JSBI.BigInt(256));


function toBigNumber(numstr){
  let bi = numstr;
  if (typeof sqrtRatio !== 'bigint') {
    bi = JSBI.BigInt(numstr);
  }
  return bi;
};


function subIn256(x, y){
  const difference = JSBI.subtract(x, y)

  if (JSBI.lessThan(difference, ZERO)) {
    return JSBI.add(Q256, difference)
  } else {
    return difference
  }
}


async function getFees(feeGrowthGlobal0, feeGrowthGlobal1, feeGrowth0Low, feeGrowth0Hi, feeGrowthInside0, feeGrowth1Low, feeGrowth1Hi, feeGrowthInside1, liquidity, decimals0, decimals1, tickLower, tickUpper, tickCurrent){
    
    let feeGrowthGlobal_0 = toBigNumber(feeGrowthGlobal0);
    let feeGrowthGlobal_1 = toBigNumber(feeGrowthGlobal1);

    
    let tickLowerFeeGrowthOutside_0 = toBigNumber(feeGrowth0Low);
    let tickLowerFeeGrowthOutside_1 = toBigNumber(feeGrowth1Low);

    
    let tickUpperFeeGrowthOutside_0 = toBigNumber(feeGrowth0Hi);
    let tickUpperFeeGrowthOutside_1 = toBigNumber(feeGrowth1Hi);

    
    let tickLowerFeeGrowthBelow_0 = ZERO;
    let tickLowerFeeGrowthBelow_1 = ZERO;
    let tickUpperFeeGrowthAbove_0 = ZERO;
    let tickUpperFeeGrowthAbove_1 = ZERO;

    
    if (tickCurrent >= tickUpper){
        tickUpperFeeGrowthAbove_0 = subIn256(feeGrowthGlobal_0, tickUpperFeeGrowthOutside_0);
        tickUpperFeeGrowthAbove_1 = subIn256(feeGrowthGlobal_1, tickUpperFeeGrowthOutside_1);
    }else{
        tickUpperFeeGrowthAbove_0 = tickUpperFeeGrowthOutside_0
        tickUpperFeeGrowthAbove_1 = tickUpperFeeGrowthOutside_1
    }

    
    if (tickCurrent >= tickLower){
        tickLowerFeeGrowthBelow_0 = tickLowerFeeGrowthOutside_0
        tickLowerFeeGrowthBelow_1 = tickLowerFeeGrowthOutside_1
    }else{
        tickLowerFeeGrowthBelow_0 = subIn256(feeGrowthGlobal_0, tickLowerFeeGrowthOutside_0);
        tickLowerFeeGrowthBelow_1 = subIn256(feeGrowthGlobal_1, tickLowerFeeGrowthOutside_1);
    }

        
    let fr_t1_0 = subIn256(subIn256(feeGrowthGlobal_0, tickLowerFeeGrowthBelow_0), tickUpperFeeGrowthAbove_0);
    let fr_t1_1 = subIn256(subIn256(feeGrowthGlobal_1, tickLowerFeeGrowthBelow_1), tickUpperFeeGrowthAbove_1);

    
    let feeGrowthInsideLast_0 = toBigNumber(feeGrowthInside0);
    let feeGrowthInsideLast_1 = toBigNumber(feeGrowthInside1);

    
    let uncollectedFees_0 = (liquidity * subIn256(fr_t1_0, feeGrowthInsideLast_0)) / Q128;
    let uncollectedFees_1 = (liquidity * subIn256(fr_t1_1, feeGrowthInsideLast_1)) / Q128;
    console.log("Amount fees token 0 in lowest decimal: "+Math.floor(uncollectedFees_0));
    console.log("Amount fees token 1 in lowest decimal: "+Math.floor(uncollectedFees_1));
    
    
    let uncollectedFeesAdjusted_0 = (uncollectedFees_0 / toBigNumber(10**decimals0)).toFixed(decimals0);
    let uncollectedFeesAdjusted_1 = (uncollectedFees_1 / toBigNumber(10**decimals1)).toFixed(decimals1);
    console.log("Amount fees token 0 Human format: "+uncollectedFeesAdjusted_0);
    console.log("Amount fees token 1 Human format: "+uncollectedFeesAdjusted_1);
}


async function Start(positionID){
    var PoolInfo = await getData(positionID);

    
    var Fees = await getFees(PoolInfo.feeGrowthGlobal0X128, PoolInfo.feeGrowthGlobal1X128, PoolInfo.feeGrowth0Low, PoolInfo.feeGrowth0Hi, PoolInfo.feeGrowthInside0LastX128, PoolInfo.feeGrowth1Low, PoolInfo.feeGrowth1Hi, PoolInfo.feeGrowthInside1LastX128, PoolInfo.liquidity, PoolInfo.Decimal0, PoolInfo.Decimal1, PoolInfo.tickLow, PoolInfo.tickHigh, PoolInfo.tickCurrent);

}


Start(5);
0

The collect method return the amount0 and amount1 of claimed tokens. So you can call it as callStatic and get the result

    const positionManager = new Contract(
        mainnetAddresses.UNISWAP_V3.POSITION_MANAGER,
        nonFungiblePositionManagerAbi,
        provider
    );
    const res = await positionManager.callStatic.collect([positionId, MONITORED_ADDRESS, BigNumber.from(2).pow(128).sub(1), BigNumber.from(2).pow(128).sub(1)]);

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.