-
Notifications
You must be signed in to change notification settings - Fork 6.9k
feat: add Keeta chain #17941
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: add Keeta chain #17941
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,195 @@ | ||
| const http = require('../http') | ||
| const { getEnv } = require('../env') | ||
|
|
||
| const UPDATE_REPS_INTERVAL = 5 * 60 * 1000; | ||
|
|
||
| let weightedReps = []; | ||
| let updateRepsPromise = null; | ||
|
|
||
| /** | ||
| * Updates the known representatives and stores them sorted by their weight. | ||
| */ | ||
| async function updateReps() { | ||
| const api = weightedReps.length > 0 ? weightedReps[0].api : getEnv('KEETA_RPC'); | ||
|
|
||
| const { representatives } = await http.get(`${api}/node/ledger/representatives`); | ||
|
|
||
| const reps = [] | ||
| for (const rep of representatives) { | ||
| reps.push({ | ||
| weight: BigInt(rep.weight), | ||
| api: rep.endpoints.api, | ||
| }); | ||
| } | ||
|
|
||
| reps.sort((rep1, rep2) => { | ||
| if (rep2.weight > rep1.weight) { | ||
| return 1; | ||
| } | ||
| if (rep2.weight < rep1.weight) { | ||
| return -1; | ||
| } | ||
| return 0; | ||
| }); | ||
|
|
||
| weightedReps = reps; | ||
| } | ||
|
|
||
| /** | ||
| * Gets the API endpoint of the currently heighest weighted representative. | ||
| * If the representatives are unknown, it initializes them and schedules a period refresh. | ||
| * | ||
| * @returns URL of the representative's API endpoint | ||
| */ | ||
| async function getRepresentativeEndpoint() { | ||
| if (weightedReps.length === 0 && !updateRepsPromise) { | ||
| updateRepsPromise = updateReps().catch(function () { | ||
| // Ignore any errors | ||
| }); | ||
|
|
||
| // Update reps regularly | ||
| setInterval(() => { | ||
| updateRepsPromise = updateReps().catch(function () { | ||
| // Ignore any errors | ||
| }); | ||
| }, UPDATE_REPS_INTERVAL); | ||
| } | ||
|
|
||
| // If an update is running, wait for it | ||
| if (updateRepsPromise) await updateRepsPromise; | ||
|
|
||
| // If fetching the reps fails initially and weightedReps is empty, | ||
| // fall back to the default representative API endpoint. | ||
| if (weightedReps.length === 0) { | ||
| return getEnv('KEETA_RPC'); | ||
| } | ||
|
|
||
| // Return the API endpoint of the representative with the most weight | ||
| return weightedReps[0].api; | ||
| } | ||
|
|
||
| /** | ||
| * Fetch the account information for a given account including the current head block and supply. | ||
| * | ||
| * See https://static.network.keeta.com/docs/classes/KeetaNetSDK.Client.html#getaccountinfo | ||
| * | ||
| * @param {string} account - Address of the account to fetch the information for | ||
| * @returns The account information | ||
| */ | ||
| async function getAccountInfo(account) { | ||
| const api = await getRepresentativeEndpoint(); | ||
|
|
||
| return await http.get(`${api}/node/ledger/account/${account}`); | ||
| } | ||
|
|
||
| /** | ||
| * Get the chain for a given account, which is the set of blocks the account has created. | ||
| * | ||
| * See https://static.network.keeta.com/docs/classes/KeetaNetSDK.Client.html#getchain | ||
| * | ||
| * @param {string} account - The account to get the chain for | ||
| * @param {string} startBlock - The block hash to start from -- this is used to paginate the request | ||
| * @returns The chain of blocks for the given account, in reverse order starting with the most recent block | ||
| */ | ||
| async function getChain(account, startBlock) { | ||
| const api = await getRepresentativeEndpoint(); | ||
|
|
||
| let url = `${api}/node/ledger/account/${account}/chain`; | ||
|
|
||
| if (startBlock) { | ||
| url += '?start=' + startBlock; | ||
| } | ||
|
|
||
| const chain = await http.get(url); | ||
|
|
||
| return chain; | ||
| } | ||
|
Comment on lines
+4
to
+106
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. All of this code would be covered by the chain's official SDK https://www.npmjs.com/package/@keetanetwork/keetanet-client including automatic retries, API response validation etc and improve the maintainability of this adapter, essentially replacing it with: const KeetaNet = require('@keetanetwork/keetanet-client')
// No need to specify a representative, the SDK takes care of that.
const client = KeetaNet.Client.fromNetwork('main')
// Get account information
const { currentHeadBlock, info } = await client.getAccountInfo(token);
// Get chain of token account
const chain = await client.getChain(token, { startBlock: start });If you agree on adding the SDK as a dependency, I'll adjust the code accordingly. |
||
|
|
||
| /** | ||
| * Calculates the change of the token's supply after a given date. | ||
| * Iterates over the blocks in the token's chain from newest to oldest and sums the amount of | ||
| * TOKEN_ADMIN_SUPPLY operations until the targetDate is reached. | ||
| * | ||
| * @param {string} token - Address of the token | ||
| * @param {Date} targetDate - Date after which the supply change should be calculated | ||
| * @param {string} currentHeadBlock - Hash of the block at the head of the chain. Blocks that were added after this hash will be ignored. | ||
| * @returns BigInt representing the change of the supply after the target date | ||
| */ | ||
| async function supplyChangeAfter(token, targetDate, currentHeadBlock) { | ||
| let supplyChange = 0n; | ||
| let start = null; | ||
| let foundStart = false; | ||
| let timestampReached = false; | ||
|
|
||
| while (!timestampReached) { | ||
| const chain = await getChain(token, start); | ||
|
|
||
| for (const { block } of chain.blocks) { | ||
| // Ignore any potentially newer blocks that were added after our call to get the account info | ||
| if (!foundStart) { | ||
| if (block.$hash === currentHeadBlock) { | ||
| foundStart = true; | ||
| } else { | ||
| continue; | ||
| } | ||
| } | ||
|
|
||
| const blockDate = new Date(block.date); | ||
| if (blockDate < targetDate) { | ||
| timestampReached = true; | ||
| break; | ||
| } | ||
|
|
||
| for (const operation of block.operations) { | ||
| // Only consider TOKEN_ADMIN_SUPPLY operations | ||
| if (operation.type === 5) { | ||
| // Method.ADD | ||
| if (operation.method === 0) { | ||
| supplyChange += BigInt(operation.amount); | ||
| } | ||
|
|
||
| // Method.SUBTRACT | ||
| if (operation.method === 1) { | ||
| supplyChange -= BigInt(operation.amount); | ||
| } | ||
| } | ||
coderabbitai[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| } | ||
| } | ||
|
|
||
| if (!chain.nextKey) break; | ||
|
|
||
| start = chain.nextKey; | ||
| } | ||
sc4l3r marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| return supplyChange; | ||
| } | ||
|
|
||
| /** | ||
| * Gets the supply of a token at the given timestamp on the Keeta mainnet. | ||
| * | ||
| * @param {string} token - Address of the token | ||
| * @param {number} timestamp - Unix timestamp in seconds | ||
| * @returns Supply of the token as a BigInt | ||
| */ | ||
| async function getSupply(token, timestamp) { | ||
| // There's no API to get the supply of the token at a given point in time, | ||
| // so instead we calculate that in two steps. | ||
|
|
||
| // 1. Get the current info for the token account. | ||
| // This includes the token's current supply. | ||
| const { currentHeadBlock, info } = await getAccountInfo(token); | ||
|
|
||
| // 2. Get the change of the supply between now and the given timestamp by iterating | ||
| // over the chain backwards and summing all supply changes. | ||
| // We pass the currentHeadBlock to ignore any blocks (with potential supply modifications) | ||
| // that have been added to the chain between the two API calls. | ||
| const supplyChange = await supplyChangeAfter(token, new Date(timestamp * 1000), currentHeadBlock); | ||
|
|
||
| // The supply of the token at the given timestamp is the difference between the current supply | ||
| // and the supply change after the given timestamp. | ||
| return BigInt(info.supply) - supplyChange; | ||
| } | ||
|
|
||
| module.exports = { | ||
| getSupply, | ||
| }; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -226,6 +226,7 @@ | |
| "katana", | ||
| "kava", | ||
| "kcc", | ||
| "keeta", | ||
| "kekchain", | ||
| "kinto", | ||
| "kintsugi", | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -107,6 +107,11 @@ const fixBalancesTokens = { | |
| '0x2a25fbD67b3aE485e461fe55d9DbeF302B7D3989': { coingeckoId: 'usd-coin', decimals: 6 }, | ||
| '0x83A15000b753AC0EeE06D2Cb41a69e76D0D5c7F7': { coingeckoId: 'ethereum', decimals: 18 }, | ||
| }, | ||
| keeta: { | ||
| [ADDRESSES.keeta.KTA]: { coingeckoId: 'keeta', decimals: 18 }, | ||
| [ADDRESSES.keeta.USDC]: { coingeckoId: 'usd-coin', decimals: 6 }, | ||
| [ADDRESSES.keeta.EURC]: { coingeckoId: 'euro-coin', decimals: 6 }, | ||
| } | ||
|
Comment on lines
+110
to
+114
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I've added these mappings since it was mentioned in the docs at https://docs.llama.fi/list-your-project/how-to-add-a-new-blockchain#id-3.-add-token-mappings-in-tokenmapping.js . Is this actually necessary? I noticed that when running the |
||
| } | ||
|
|
||
| ibcChains.forEach(chain => fixBalancesTokens[chain] = { ...ibcMappings, ...(fixBalancesTokens[chain] || {}) }) | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,24 @@ | ||
| const ADDRESSES = require('../helper/coreAssets.json') | ||
| const { getSupply } = require('../helper/chain/keeta'); | ||
|
|
||
| const SUPPORTED_TOKENS = [ | ||
| ADDRESSES.keeta.KTA, | ||
| ADDRESSES.keeta.USDC, | ||
| ADDRESSES.keeta.EURC, | ||
| ] | ||
|
|
||
| async function tvl(api) { | ||
| for (const token of SUPPORTED_TOKENS) { | ||
| const supply = await getSupply(token, api.timestamp); | ||
|
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. To determine whether this is a request for historical data or for the latest data, what is the typical difference of the For a query for the most recent data we could skip the |
||
| api.add(token, supply); | ||
| } | ||
| } | ||
sc4l3r marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| module.exports = { | ||
| methodology: 'TVL is calculated as the supply of a token on the Keeta network.', | ||
| // Date of mainnet release | ||
| start: '2025-09-23', | ||
| keeta: { | ||
| tvl, | ||
| } | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.