Service returns circulating supply for Lido tokens.
API service is deployed in the mainnet and testnet chains. A list of available endpoints can be found in the Swagger:
- Mainnet: https://circ-supply.lido.fi/api
- Goerli: https://circ-supply.testnet.fi/api
Circ supply is calculated by the formula:
Circ supply = Total LDO supply - LDO tokens in Lido treasury - Non vested tokens
const totalSupply = await ldoContract.totalSupply();const treasuryAddress = await lidoContract.getTreasury();
const treasuryBalance = await ldoContract.balanceOf(treasuryAddress);To calculate the number of non vested tokens, we first get all the members of the vesting program using events emitted on the token manager contract:
const newFilter = tmContract.filters.NewVesting();
const revokeFilter = tmContract.filters.RevokeVesting();
/**
* Since we get updates only for new blocks while the server is running,
* we also look at RevokeVesting events to make sure that vestings will be
* updated in the case of a vesting revoke
*/
const [newEvents, revokeEvents] = await Promise.all([
tmContract.queryFilter(newFilter),
tmContract.queryFilter(revokeFilter),
]);
const vestingMembers = new Set<string>();
newEvents.forEach((event) => vestingMembers.add(event.args.receiver));
revokeEvents.forEach((event) => vestingMembers.add(event.args.receiver));Then for each updated member we get the number of their vestings:
const vestingsLength = await tmContract.vestingsLengths(member);Then we get information on each vesting of the member:
const vestingInfo = await tmContract.getVesting(member, vestingId);At this step we have all the information to calculate the number of non vested tokens. But there may be more tokens in the vesting than on the member's balance, in case the tokens have been burned. That's why we collect all token burning events:
const filter = this.ldoContract.filters.Transfer(null, AddressZero);
const events = await this.ldoContract.queryFilter(filter);
const burnsAddresses = new Set<string>();
events.forEach((event) => burnsAddresses.add(event.args._from));For all addresses where tokens were burned, we update the member's balance:
const memberBalance = await ldoContract.balanceOf(holderAddress);Then we calculate the amount of non vested tokens at a specific time using the same code as in the TokenManager.sol contract. For each member we check the calculated number of non vested tokens from all of his vestings against his LDO balance and use the minimum of these values:
const lockedTokens = balance.lt(nonVested) ? balance : nonVested;Finally we sum up all locked tokens.
Circ supply for stETH equals stETH total supply
const totalSupply = await lidoContract.totalSupply();
const circSupply = totalSupply;Circ supply for wstETH equals wstETH total supply
const totalSupply = await wstethContract.totalSupply();
const circSupply = totalSupply;TODO: add other chains support
The data for all endpoints is updated by the cron job. The update interval is set via env variable TOKEN_UPDATE_CRON, which defaults */1 * * * *.
On mainnet the LDO burn and vesting scans start from src/tokens/ldo/snapshots/mainnet.json: the addresses derived from LDO events from the deploy block up to toBlock. Only blocks after toBlock are scanned on start. Both commands need an archive RPC that serves eth_getLogs:
# fails if the addresses differ from chain events up to toBlock
# CI passes MAINNET_RPC_URL here
EL_API_URLS=<rpc url> yarn ldo-snapshot:check
# move the snapshot forward to a finalized block (default: the current finalized block)
EL_API_URLS=<rpc url> yarn ldo-snapshot:update [toBlock]Never edit the file by hand. A stale snapshot only makes the first scan longer.
Step 1. Copy the contents of sample.env to .env:
$ cp sample.env .envStep 2. Install dependencies:
$ yarn installStep 3. Start the development server
$ yarn start:dev# development
$ yarn start
# watch mode
$ yarn start:dev
# production mode
$ yarn start:prod# unit tests
$ yarn test
# e2e tests
$ yarn test:e2e
# test coverage
$ yarn test:cov| Variable | Required | Default | Description |
|---|---|---|---|
| EL_API_URLS | Yes | Execution layer RPC urls, separated by comma | |
| CHAIN_ID | Yes | Execution layer chain id | |
| PORT | No | 3000 |
App port |
| CORS_WHITELIST_REGEXP | No | Regexp that checks which domains have access | |
| GLOBAL_THROTTLE_TTL | No | 5 |
The number of seconds that each request will last in storage |
| GLOBAL_THROTTLE_LIMIT | No | 100 |
The maximum number of requests within the TTL limit |
| GLOBAL_CACHE_TTL | No | 1 |
Cache expiration time in seconds |
| SENTRY_DSN | No | Sentry DSN | |
| LOG_LEVEL | No | json |
Log level: debug, info, notice, warning or error |
| LOG_FORMAT | No | debug |
Log format: simple or json |
| TOKEN_UPDATE_CRON | No | */1 * * * * |
Token update cron job interval |
The sample.env file contains examples of variable values.
Lido Circ Supply is MIT licensed.
To create new release:
- Merge all changes to the
mainbranch - Navigate to Repo => Actions
- Run action "Prepare release" action against
mainbranch - When action execution is finished, navigate to Repo => Pull requests
- Find pull request named "chore(release): X.X.X" review and merge it with "Rebase and merge" (or "Squash and merge")
- After merge release action will be triggered automatically
- Navigate to Repo => Actions and see last actions logs for further details
The release also builds the k8s prod image ("CI K8s Prod") and pushes it to the prod registry as
<tag>. To rebuild an existing tag, run "CI K8s Prod" manually and pick the tag in "Use workflow from".