Skip to content

Commit 3a49e5d

Browse files
committed
feat: add Keeta chain
1 parent 9b5077b commit 3a49e5d

File tree

6 files changed

+233
-2
lines changed

6 files changed

+233
-2
lines changed

projects/helper/chain/keeta.js

Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
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+
// If the fetching the reps fails initially and weightedReps is empty,
62+
// fall back to the default representative API endpoint.
63+
if (weightedReps.length === 0) {
64+
return getEnv('KEETA_RPC');
65+
}
66+
67+
const rep = weightedReps[0];
68+
return rep.api;
69+
}
70+
71+
/**
72+
* Fetch the account information for a given account including the current head block and supply.
73+
*
74+
* See https://static.network.keeta.com/docs/classes/KeetaNetSDK.Client.html#getaccountinfo
75+
*
76+
* @param {string} account - Address of the account to fetch the information for
77+
* @returns The account information
78+
*/
79+
async function getAccountInfo(account) {
80+
const api = await getRepresentativeEndpoint();
81+
82+
return await http.get(`${api}/node/ledger/account/${account}`);
83+
}
84+
85+
/**
86+
* Get the chain for a given account, which is the set of blocks the account has created.
87+
*
88+
* See https://static.network.keeta.com/docs/classes/KeetaNetSDK.Client.html#getchain
89+
*
90+
* @param {string} account - The account to get the chain for
91+
* @param {string} startBlock - The block hash to start from -- this is used to paginate the request
92+
* @returns The chain of blocks for the given account, in reverse order starting with the most recent block
93+
*/
94+
async function getChain(account, startBlock) {
95+
const api = await getRepresentativeEndpoint();
96+
97+
let url = `${api}/node/ledger/account/${account}/chain`;
98+
99+
if (startBlock) {
100+
url += '?start=' + startBlock;
101+
}
102+
103+
const chain = await http.get(url);
104+
105+
return chain;
106+
}
107+
108+
/**
109+
* Calculates the change of the token's supply after a given date.
110+
* Iterates over the blocks in the token's chain from newest to oldest and sums the amount of
111+
* TOKEN_ADMIN_SUPPLY operations until the targetDate is reached.
112+
*
113+
* @param {string} token - Address of the token
114+
* @param {Date} targetDate - Date after which the supply change should be calculated
115+
* @param {string} currentHeadBlock - Hash of the block at the head of the chain. Blocks that were added after this hash will be ignored.
116+
* @returns BigInt representing the change of the supply after the target date
117+
*/
118+
async function supplyChangeAfter(token, targetDate, currentHeadBlock) {
119+
let supplyChange = 0n;
120+
let start = null;
121+
let foundStart = false;
122+
let timestampReached = false;
123+
124+
while (!timestampReached) {
125+
const chain = await getChain(token, start);
126+
127+
for (const { block } of chain.blocks) {
128+
// Ignore any potentially newer blocks that were added after our call to get the account info
129+
if (!foundStart) {
130+
if (block.$hash === currentHeadBlock) {
131+
foundStart = true;
132+
} else {
133+
continue;
134+
}
135+
}
136+
137+
const blockDate = new Date(block.date);
138+
if (blockDate < targetDate) {
139+
timestampReached = true;
140+
break;
141+
}
142+
143+
for (const operation of block.operations) {
144+
// Only consider TOKEN_ADMIN_SUPPLY operations
145+
if (operation.type === 5) {
146+
// Method.ADD
147+
if (operation.method === 0) {
148+
supplyChange += BigInt(operation.amount);
149+
}
150+
151+
// Method.SUBTRACT
152+
if (operation.method === 1) {
153+
supplyChange -= BigInt(operation.amount);
154+
}
155+
}
156+
}
157+
}
158+
159+
if (!chain.nextKey) break;
160+
161+
start = chain.nextKey;
162+
}
163+
164+
return supplyChange;
165+
}
166+
167+
/**
168+
* Gets the supply of a token at the given timestamp on the Keeta mainnet.
169+
*
170+
* @param {string} token - Address of the token
171+
* @param {number} timestamp - Unix timestamp in seconds
172+
* @returns Supply of the token as a BigInt
173+
*/
174+
async function getSupply(token, timestamp) {
175+
// There's no API to get the supply of the token at a given point in time,
176+
// so instead we calculate that in two steps.
177+
178+
// 1. Get the current info for the token account.
179+
// This includes the token's current supply.
180+
const { currentHeadBlock, info } = await getAccountInfo(token);
181+
182+
// 2. Get the change of the supply between now and the given timestamp by iterating
183+
// over the chain backwards and summing all supply changes.
184+
// We pass the currentHeadBlock to ignore any blocks (with potential supply modifications)
185+
// that have been added to the chain between the two API calls.
186+
const supplyChange = await supplyChangeAfter(token, new Date(timestamp * 1000), currentHeadBlock);
187+
188+
// The supply of the token at the given timestamp is the difference between the current supply
189+
// and the supply change after the given timestamp.
190+
return BigInt(info.supply) - supplyChange;
191+
}
192+
193+
module.exports = {
194+
getSupply,
195+
};

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)