Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
195 changes: 195 additions & 0 deletions projects/helper/chain/keeta.js
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
Copy link
Author

Choose a reason for hiding this comment

The 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);
}
}
}
}

if (!chain.nextKey) break;

start = chain.nextKey;
}

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,
};
1 change: 1 addition & 0 deletions projects/helper/chains.json
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,7 @@
"katana",
"kava",
"kcc",
"keeta",
"kekchain",
"kinto",
"kintsugi",
Expand Down
9 changes: 7 additions & 2 deletions projects/helper/coreAssets.json
Original file line number Diff line number Diff line change
Expand Up @@ -2737,13 +2737,18 @@
"WMON": "0x3bd359C1119dA7Da1D913D1C4D2B7c461115433A",
"WBTC": "0x0555E30da8f98308EdB960aa94C0Db47230d2B9c",
"WSOL": "0xea17E5a9efEBf1477dB45082d67010E2245217f1"
},
},
"stable": {
"USDT0": "0x779Ded0c9e1022225f8E0630b35a9b54bE713736",
"USDT0": "0x779Ded0c9e1022225f8E0630b35a9b54bE713736",
"wgUSDT": "0x89c3e9ba11414e1d2c6f3e751d6f687682928283"
},
"grx": {
"WGRX": "0x45C7287F897B3A79Cd3f6e4F14B4CE568f023bD5",
"USDT": "0x173462F5eb7CA0D1ab6aaea846fEFe85A28029E2"
},
"keeta": {
"KTA": "keeta_anqdilpazdekdu4acw65fj7smltcp26wbrildkqtszqvverljpwpezmd44ssg",
"USDC": "keeta_amnkge74xitii5dsobstldatv3irmyimujfjotftx7plaaaseam4bntb7wnna",
"EURC": "keeta_apblhar4ncp3ln62wrygsn73pt3houuvj7ic47aarnolpcu67oqn4xqcji3au"
}
}
1 change: 1 addition & 0 deletions projects/helper/env.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ const DEFAULTS = {
BLOCKFROST_PROJECT_ID: 'mai'+'nnetBfkdsCOvb4BS'+'VA6pb1D43ptQ7t3cLt06',
VIRBICOIN_RPC: "https://rpc.digitalregion.jp",
TATUM_PUBLIC_API_KEY: "t-6956724efd74cfe6b231bee6-cd40df69ad2d423588e36fc6",
KEETA_RPC: "https://rep1.main.network.api.keeta.com/api",
}

const ENV_KEYS = [
Expand Down
5 changes: 5 additions & 0 deletions projects/helper/tokenMapping.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link
Author

Choose a reason for hiding this comment

The 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 update-token-mappings script from the coins server in the defillama-server repo that it would add these tokens automatically, but with their address as the symbol (add not KTA / USDC / EURC), which is not desired I think.

}

ibcChains.forEach(chain => fixBalancesTokens[chain] = { ...ibcMappings, ...(fixBalancesTokens[chain] || {}) })
Expand Down
24 changes: 24 additions & 0 deletions projects/keeta/index.js
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);
Copy link
Author

Choose a reason for hiding this comment

The 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 api.timestamp from it being created and this function being called?

For a query for the most recent data we could skip the supplyChangeAfter call in the getSupply function to speed it up (no need to fetch the token's chain if we're interested in the latest supply which we get from the account info already).

api.add(token, supply);
}
}

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,
}
}
Loading