Skip to content

Commit 5bc7446

Browse files
committed
feat: add Keeta chain
1 parent 9b5077b commit 5bc7446

File tree

6 files changed

+227
-2
lines changed

6 files changed

+227
-2
lines changed

projects/helper/chain/keeta.js

Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
const http = require('../http')
2+
const { getEnv } = require('../env')
3+
4+
const UPDATE_REPS_INTERVAL = 5 * 60 * 1000;
5+
6+
let weightedReps = [];
7+
let updateRepsPromise = null;
8+
9+
/**
10+
* Updates the known representatives and stores them sorted by their weight.
11+
*/
12+
async function updateReps() {
13+
const api = weightedReps.length > 0 ? weightedReps[0].api : getEnv('KEETA_RPC');
14+
15+
const { representatives } = await http.get(`${api}/node/ledger/representatives`);
16+
17+
const reps = []
18+
for (const rep of representatives) {
19+
reps.push({
20+
weight: BigInt(rep.weight),
21+
api: rep.endpoints.api,
22+
});
23+
}
24+
25+
reps.sort((rep1, rep2) => {
26+
if (rep2.weight > rep1.weight) {
27+
return 1;
28+
}
29+
if (rep2.weight < rep1.weight) {
30+
return -1;
31+
}
32+
return 0;
33+
});
34+
35+
weightedReps = reps;
36+
}
37+
38+
/**
39+
* Gets the API endpoint of the currently heighest weighted representative.
40+
* If the representatives are unknown, it initializes them and schedules a period refresh.
41+
*
42+
* @returns URL of the representative's API endpoint
43+
*/
44+
async function getRepresentativeEndpoint() {
45+
if (weightedReps.length === 0) {
46+
updateRepsPromise = updateReps().catch(function () {
47+
// Ignore any errors
48+
});
49+
50+
// Update reps regularly
51+
setInterval(() => {
52+
updateRepsPromise = updateReps().catch(function () {
53+
// Ignore any errors
54+
});
55+
}, UPDATE_REPS_INTERVAL);
56+
}
57+
58+
// If an update is running, wait for it
59+
await updateRepsPromise;
60+
61+
const rep = weightedReps[0];
62+
return rep.api;
63+
}
64+
65+
/**
66+
* Fetch the account information for a given account including the current head block and supply.
67+
*
68+
* See https://static.network.keeta.com/docs/classes/KeetaNetSDK.Client.html#getaccountinfo
69+
*
70+
* @param {string} account - Address of the account to fetch the information for
71+
* @returns The account information
72+
*/
73+
async function getAccountInfo(account) {
74+
const api = await getRepresentativeEndpoint();
75+
76+
return await http.get(`${api}/node/ledger/account/${account}`);
77+
}
78+
79+
/**
80+
* Get the chain for a given account, which is the set of blocks the account has created.
81+
*
82+
* See https://static.network.keeta.com/docs/classes/KeetaNetSDK.Client.html#getchain
83+
*
84+
* @param {string} account - The account to get the chain for
85+
* @param {string} startBlock - The block hash to start from -- this is used to paginate the request
86+
* @returns The chain of blocks for the given account, in reverse order starting with the most recent block
87+
*/
88+
async function getChain(account, startBlock) {
89+
const api = await getRepresentativeEndpoint();
90+
91+
let url = `${api}/node/ledger/account/${account}/chain`;
92+
93+
if (startBlock) {
94+
url += '?start=' + startBlock;
95+
}
96+
97+
const chain = await http.get(url);
98+
99+
return chain;
100+
}
101+
102+
/**
103+
* Calculates the change of the token's supply after a given date.
104+
* Iterates over the blocks in the token's chain from newest to oldest and sums the amount of
105+
* TOKEN_ADMIN_SUPPLY operations until the targetDate is reached.
106+
*
107+
* @param {string} token - Address of the token
108+
* @param {Date} targetDate - Date after which the supply change should be calculated
109+
* @param {string} currentHeadBlock - Hash of the block at the head of the chain. Blocks that were added after this hash will be ignored.
110+
* @returns BigInt representing the change of the supply after the target date
111+
*/
112+
async function supplyChangeAfter(token, targetDate, currentHeadBlock) {
113+
let supplyChange = 0n;
114+
let start = null;
115+
let foundStart = false;
116+
let timestampReached = false;
117+
118+
while (!timestampReached) {
119+
const chain = await getChain(token, start);
120+
121+
for (const { block } of chain.blocks) {
122+
// Ignore any potentially newer blocks that were added after our call to get the account info
123+
if (!foundStart) {
124+
if (block.$hash === currentHeadBlock) {
125+
foundStart = true;
126+
} else {
127+
continue;
128+
}
129+
}
130+
131+
blockDate = new Date(block.date);
132+
if (blockDate < targetDate) {
133+
timestampReached = true;
134+
break;
135+
}
136+
137+
for (const operation of block.operations) {
138+
// Only consider TOKEN_ADMIN_SUPPLY operations
139+
if (operation.type === 5) {
140+
// Method.ADD
141+
if (operation.method === 0) {
142+
supplyChange += BigInt(operation.amount);
143+
}
144+
145+
// Method.SUBTRACT
146+
if (operation.method === 1) {
147+
supplyChange -= BigInt(operation.amount);
148+
}
149+
}
150+
}
151+
}
152+
153+
if (!chain.nextKey) break;
154+
155+
start = chain.nextKey;
156+
}
157+
158+
return supplyChange;
159+
}
160+
161+
/**
162+
* Gets the supply of a token at the given timestamp on the Keeta mainnet.
163+
*
164+
* @param {string} token - Address of the token
165+
* @param {number} timestamp - Unix timestamp in seconds
166+
* @returns Supply of the token as a BigInt
167+
*/
168+
async function getSupply(token, timestamp) {
169+
// There's no API to get the supply of the token at a given point in time,
170+
// so instead we calculate that in two steps.
171+
172+
// 1. Get the current info for the token account.
173+
// This includes the token's current supply.
174+
const { currentHeadBlock, info } = await getAccountInfo(token);
175+
176+
// 2. Get the change of the supply between now and the given timestamp by iterating
177+
// over the chain backwards and summing all supply changes.
178+
// We pass the currentHeadBlock to ignore any blocks (with potential supply modifications)
179+
// that have been added to the chain between the two API calls.
180+
const supplyChange = await supplyChangeAfter(token, new Date(timestamp * 1000), currentHeadBlock);
181+
182+
// The supply of the token at the given timestamp is the difference between the current supply
183+
// and the supply change after the given timestamp.
184+
return BigInt(info.supply) - supplyChange;
185+
}
186+
187+
module.exports = {
188+
getSupply,
189+
};

projects/helper/chains.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,7 @@
226226
"katana",
227227
"kava",
228228
"kcc",
229+
"keeta",
229230
"kekchain",
230231
"kinto",
231232
"kintsugi",

projects/helper/coreAssets.json

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2737,13 +2737,18 @@
27372737
"WMON": "0x3bd359C1119dA7Da1D913D1C4D2B7c461115433A",
27382738
"WBTC": "0x0555E30da8f98308EdB960aa94C0Db47230d2B9c",
27392739
"WSOL": "0xea17E5a9efEBf1477dB45082d67010E2245217f1"
2740-
},
2740+
},
27412741
"stable": {
2742-
"USDT0": "0x779Ded0c9e1022225f8E0630b35a9b54bE713736",
2742+
"USDT0": "0x779Ded0c9e1022225f8E0630b35a9b54bE713736",
27432743
"wgUSDT": "0x89c3e9ba11414e1d2c6f3e751d6f687682928283"
27442744
},
27452745
"grx": {
27462746
"WGRX": "0x45C7287F897B3A79Cd3f6e4F14B4CE568f023bD5",
27472747
"USDT": "0x173462F5eb7CA0D1ab6aaea846fEFe85A28029E2"
2748+
},
2749+
"keeta": {
2750+
"KTA": "keeta_anqdilpazdekdu4acw65fj7smltcp26wbrildkqtszqvverljpwpezmd44ssg",
2751+
"USDC": "keeta_amnkge74xitii5dsobstldatv3irmyimujfjotftx7plaaaseam4bntb7wnna",
2752+
"EURC": "keeta_apblhar4ncp3ln62wrygsn73pt3houuvj7ic47aarnolpcu67oqn4xqcji3au"
27482753
}
27492754
}

projects/helper/env.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ const DEFAULTS = {
4545
BLOCKFROST_PROJECT_ID: 'mai'+'nnetBfkdsCOvb4BS'+'VA6pb1D43ptQ7t3cLt06',
4646
VIRBICOIN_RPC: "https://rpc.digitalregion.jp",
4747
TATUM_PUBLIC_API_KEY: "t-6956724efd74cfe6b231bee6-cd40df69ad2d423588e36fc6",
48+
KEETA_RPC: "https://rep1.main.network.api.keeta.com/api",
4849
}
4950

5051
const ENV_KEYS = [

projects/helper/tokenMapping.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,11 @@ const fixBalancesTokens = {
107107
'0x2a25fbD67b3aE485e461fe55d9DbeF302B7D3989': { coingeckoId: 'usd-coin', decimals: 6 },
108108
'0x83A15000b753AC0EeE06D2Cb41a69e76D0D5c7F7': { coingeckoId: 'ethereum', decimals: 18 },
109109
},
110+
keeta: {
111+
[ADDRESSES.keeta.KTA]: { coingeckoId: 'keeta', decimals: 18 },
112+
[ADDRESSES.keeta.USDC]: { coingeckoId: 'usd-coin', decimals: 6 },
113+
[ADDRESSES.keeta.EURC]: { coingeckoId: 'euro-coin', decimals: 6 },
114+
}
110115
}
111116

112117
ibcChains.forEach(chain => fixBalancesTokens[chain] = { ...ibcMappings, ...(fixBalancesTokens[chain] || {}) })

projects/keeta/index.js

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
const ADDRESSES = require('../helper/coreAssets.json')
2+
const { getSupply } = require('../helper/chain/keeta');
3+
4+
const SUPPORTED_TOKENS = [
5+
ADDRESSES.keeta.KTA,
6+
ADDRESSES.keeta.USDC,
7+
ADDRESSES.keeta.EURC,
8+
]
9+
10+
async function tvl(api) {
11+
for (const token of SUPPORTED_TOKENS) {
12+
const supply = await getSupply(token, api.timestamp);
13+
api.add(token, supply);
14+
}
15+
}
16+
17+
module.exports = {
18+
methodology: 'TVL is calculated as the supply of a token on the Keeta network.',
19+
// Date of mainnet release
20+
start: '2025-09-23',
21+
keeta: {
22+
tvl,
23+
}
24+
}

0 commit comments

Comments
 (0)