Skip to content

Conversation

@Unique-Divine
Copy link
Contributor

@Unique-Divine Unique-Divine commented Sep 17, 2025

Purpose

  1. Several LayerZero OFTs have launched on Nibiru that connect to Ethereum, Base, and Arbitrum.
  2. nibiru: add new tokens to tokenMapping.json; sUSDa, cbBTC, uBTC, USDa
  3. Reference: Continues from DeFiLlama-server#9893
  4. Related: usd-coin: add nibiru USDC.e from LayerZero Stargate bridge; add Avalon USDa peggedassets-server#594

Token Info

Testing and Validation

I wrote a script using the logic in this repo (fetchCgPriceData) to make sure all of the tokens in the map correspond to actual IDs on CoinGecko using a free API key. Sharing that below in case it's useful for someone else.

dfl_server.ts - Logic from DeFiLlama-server repo

[Expand dfl_server.ts]
export default function sleep(ms: number) {
  return new Promise((resolve) => setTimeout(resolve, ms));
}
const locks = [] as ((value: unknown) => void)[];
export function getCoingeckoLock() {
  return new Promise((resolve) => {
    locks.push(resolve);
  });
}
export function releaseCoingeckoLock() {
  const firstLock = locks.shift();
  if (firstLock !== undefined) {
    firstLock(null);
  }
}
// Rate limit is 500 calls/min for coingecko's API
// So we'll release one every 0.2 seconds to match it
export function setTimer(timeBetweenTicks: number = 200) {
  const timer = setInterval(() => {
    releaseCoingeckoLock();
  }, timeBetweenTicks);
  return timer;
}

export type CoingeckoResponse = {
  [cgId: string]: {
    usd: number;
    usd_market_cap: number;
    last_updated_at: number;
    usd_24h_vol: number;
  };
};

const isProApiKey = false;

const coingeckDotCom: string = `https://${isProApiKey ? "pro-api" : "api"
  }.coingecko.com`;

const x_cf_api_key: string = isProApiKey
  ? "x_cg_pro_api_key"
  : "x_cg_demo_api_key";

export async function retryCoingeckoRequest(
  query: string,
  retries: number,
  log: boolean = false
): Promise<CoingeckoResponse> {
  for (let i = 0; i < retries; i++) {
    await getCoingeckoLock();
    try {
      const fetched = await fetch(
        `${coingeckDotCom}/api/v3/${query}&${x_cf_api_key}=${process.env.CG_KEY}`
      );
      if (log) console.log(fetched);
      const res = (await fetched.json()) as CoingeckoResponse;
      if (log) console.log(res);
      if (Object.keys(res).length == 1 && Object.keys(res)[0] == "status")
        throw new Error(`cg call failed`);
      return res;
    } catch (e) {
      if (log) console.log(e);
      if ((i + 1) % 3 === 0 && retries > 3) {
        await sleep(10e3); // 10s
      }
      continue;
    }
  }
  return {};
}

export async function fetchCgPriceData(
  coinIds: string[],
  log: boolean = false
) {
  const url = `${coingeckDotCom}/api/v3/simple/price?ids=${coinIds.join(
    ","
  )}&vs_currencies=usd&include_market_cap=true&include_last_updated_at=true&include_24hr_vol=true&${x_cf_api_key}=${process.env.CG_KEY
    }`;

  const res = await fetch(url);
  if (log) console.log("HTTP status:", res.status);

  let json: any;
  try {
    json = await res.json();
  } catch (err) {
    if (log) {
      console.error("Failed to parse JSON:", err);
      const text = await res.text().catch(() => "");
      console.error("Raw response text:", text);
    }
    return {};
  }

  // Handle CG error payloads like { status: {...} }
  if (
    !res.ok ||
    (json &&
      typeof json === "object" &&
      ("status" in json || "error_code" in json))
  ) {
    if (log) console.error("CoinGecko error:", json);
    return {};
  }

  if (log) console.log("Parsed data:", json);
  return json as CoingeckoResponse;
}

export async function fetchCgPriceDataRetry(
  coinIds: string[],
  log: boolean = false
) {
  return await retryCoingeckoRequest(
    `simple/price?ids=${coinIds.join(
      ","
    )}&vs_currencies=usd&include_market_cap=true&include_last_updated_at=true&include_24hr_vol=true`,
    10,
    log
  );
}

export type CG_TOKEN_VAL = {
  decimals: string;
  symbol: string;
  to: `coingecko#${string}`;
};

/**
 * CG_TOKEN_MAPPING: This is the value at the "nibiru" field in
 * `tokenMapping.json` in the DeFiLlama-server repo.
 *
 * @see [CoinGecko Token API List](https://docs.google.com/spreadsheets/d/1wTTuxXt8n9q7C4NDXqQpI3wpKu1_5bGVmP9Xz0XGSyU/edit?gid=0#gid=0)
 * */
export const CG_TOKEN_MAPPING: {
  [key: string]: CG_TOKEN_VAL;
} = {
  "0x0000000000000000000000000000000000000000": {
    decimals: "18",
    symbol: "NIBI",
    to: "coingecko#nibiru",
  },
  unibi: {
    decimals: "6",
    symbol: "NIBI",
    to: "coingecko#nibiru",
  },
  "tf:nibi1vetfuua65frvf6f458xgtjerf0ra7wwjykrdpuyn0jur5x07awxsfka0ga:axv": {
    decimals: "6",
    symbol: "AXV",
    to: "coingecko#astrovault",
  },
  nibi1cehpv50vl90g9qkwwny8mw7txw79zs6f7wsfe8ey7dgp238gpy4qhdqjhm: {
    decimals: "6",
    symbol: "xNIBI",
    to: "coingecko#nibiru",
  },
  "0x0CaCF669f8446BeCA826913a3c6B96aCD4b02a97": {
    decimals: "18",
    symbol: "WNIBI",
    to: "coingecko#nibiru",
  },
  "0xcA0a9Fb5FBF692fa12fD13c0A900EC56Bb3f0a7b": {
    decimals: "6",
    symbol: "stNIBI",
    to: "coingecko#nibiru",
  },
  "tf:nibi1udqqx30cw8nwjxtl4l28ym9hhrp933zlq8dqxfjzcdhvl8y24zcqpzmh8m:ampNIBI":
  {
    decimals: "6",
    symbol: "stNIBI.nibi",
    to: "coingecko#nibiru",
  },
  "0x0829F361A05D993d5CEb035cA6DF3446b060970b": {
    decimals: "6",
    symbol: "USDC",
    to: "coingecko#usd-coin",
  },
  "0xf4e097E36d2064E2bDCA96e60439f3A369522003": {
    decimals: "18",
    symbol: "USDa",
    to: "coingecko#tether",
  },
  "0x84f682626302EA7BCA2A7c338b84863292131319": {
    decimals: "18",
    symbol: "sUSDa",
    to: "coingecko#susda",
  },
  "0xfCfc58685101e2914cBCf7551B432500db84eAa8": {
    decimals: "18",
    symbol: "MIM",
    to: "coingecko#magic-internet-money",
  },
  "0x08EBA8ff53c6ee5d37A90eD4b5239f2F85e7B291": {
    decimals: "18",
    symbol: "USDC.arb",
    to: "coingecko#usd-coin",
  },
  "0x43F2376D5D03553aE72F4A8093bbe9de4336EB08": {
    decimals: "6",
    symbol: "USDT",
    to: "coingecko#tether",
  },
  "0xcdA5b77E2E2268D9E09c874c1b9A4c3F07b37555": {
    decimals: "18",
    symbol: "WETH",
    to: "coingecko#ethereum",
  },
  "0x1d1715ecE22A6Ab6349196e7054E61de3Ac2Ea3D": {
    decimals: "18",
    symbol: "ynETHx",
    to: "coingecko#yneth-max",
  },
  "0x9568F2AFd09D845D48Cf999244e18B1a9467eAdB": {
    decimals: "8",
    symbol: "cbBTC",
    to: "coingecko#coinbase-wrapped-btc",
  },
  "0xd59be1Da2e9B30b6f7aB27b2D08f841B39c349fa": {
    decimals: "18",
    symbol: "uBTC",
    to: "coingecko#ubtc",
  },
  "0x7168634Dd1ee48b1C5cC32b27fD8Fc84E12D00E6": {
    decimals: "6",
    symbol: "AXV",
    to: "coingecko#astrovault",
  },
  "0x1429B38e58b97de646ACd65fdb8a4502c2131484": {
    decimals: "18",
    symbol: "WNIBI.omni",
    to: "coingecko#nibiru",
  },
};

src/dfl_server.test.ts

[Expand `dfl_server.test.ts`]
import { describe, test, expect } from "bun:test";
import {
  CG_TOKEN_MAPPING,
  fetchCgPriceData,
  type CG_TOKEN_VAL,
} from "./dfl_server";

import { config } from "dotenv";
import { join } from "path";

describe("coingecko ids", async () => {
  config();

  let tokens: string[] = [];
  let tokenInfos: CG_TOKEN_VAL[] = [];

  const cgIdPrefix = "coingecko#";
  const getCgId = (info: CG_TOKEN_VAL): string =>
    info.to.startsWith(cgIdPrefix) ? info.to.slice(cgIdPrefix.length) : info.to;
  for (const [token, info] of Object.entries(CG_TOKEN_MAPPING)) {
    tokens.push(token);
    tokenInfos.push(info);
  }

  const cgCoinIds: string[] = tokenInfos.map((info) => getCgId(info));

  const doLogging: boolean = true;
  const out = await fetchCgPriceData(cgCoinIds, doLogging);
  const outBad = await fetchCgPriceData(["foobar_fake_token"], doLogging);

  for (const [idx, token] of tokens.entries()) {
    const cgCoinId = cgCoinIds[idx];
    test(`token "${token}", cgCoinId="${cgCoinId}" has valid coingecko id"`, () => {
      const isOnCg: boolean = out[cgCoinId] != null;
      expect(isOnCg).toBeTruthy();
    });
  }

  const fPath = join(__dirname, "nibiru_tokenMapping.json");
  const f = Bun.file(fPath);
  if (await f.exists()) {
    const currContents = await f.json();

    for (const [k, v] of Object.entries(CG_TOKEN_MAPPING)) {
      expect(currContents[k].to).toBe(v.to);
    }
    const fPathNew = join(__dirname, "nibiru_tokenMapping-new.json");
    const fNew = Bun.file(fPathNew);
    await Bun.write(fNew, JSON.stringify(CG_TOKEN_MAPPING, null, 2));
  } else {
    await Bun.write(f, JSON.stringify(CG_TOKEN_MAPPING, null, 2));
  }

  console.debug("DEBUG %o: ", { out, outBad });
});

Comment on lines 357 to 360
"decimals": "6",
"symbol": "xNIBI",
"to": "coingecko#astrovault-xnibi"
"to": "coingecko#nibiru"
},
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

xNIBI is a staking derivative of NIBI (worth >= 1 NIBI), but it's not on CoinGecko. It's a CW20 token. Fixing this to map to NIBI instead.

More Info: https://astrovault.io/pool

image

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants