After trying to answer the question at stackexchange I've decided to write this short article, as the topic is indeed quite interesting.
1. Archive Node
The most simple is the last part of the question: at a specific block (onchain)
. As you may know, ethereum's geth
node can be launched with various --gcmode
values: full
or archive
. The archive
mode doesn't remove any historical state data, it means - we can perform read queries specifying the blockNumber
and getting the result back, as it was at that block.
Node-as-a-Service
All popular node providers, like infura.io, quicknode.com, alchemy.com can give you access to the archive node, but it won't be free of charge.
Self Hosting
As already mentioned, if you use geth
include additionally --syncmode full --gcmode archive
flags. But you have to be patient - it takes time to full sync the node, and it will occupy >10.2TB
storage: etherscan/chainarchive
I use erigon
as it allows us to specify the number of blocks we want to keep with the historical state data
./erigon.exe --datadir D:/Erigon --chain mainnet --private.api.addr=127.0.0.1:9090 --prune=hrtc --prune.h.older=400000 --prune.r.older=400000 --prune.t.older=400000 --prune.c.older=400000
This will keep 400000
blocks, which with the average blocktime of 13s
will keep the data for 2 months, and it takes 680GB
of storage.
Intermediate summary
When you perform
READ
actions at blockchain you always get the result back which refers to someblockNumber
. Even if you don't specify the block number, you get the result just for the latest block, but you can set any previous block your node supports.
2. Token Price
To retrieve the price onchain, there are 2 options - a)
oracles: 3rd party contracts, which accumulate the prices in the correct way, and b)
direct DEX sources - like Uniswap.
2.1 Oracles
The most convenient way, as it provides direct TOKEN/USD
pair access, and most correct way, as it provides the most accurate price information at a given time point (blockNumber
).
I suggest the ⬡ Chainlink price feed contracts: data.chain.link/crypto-usd. For an example, lets get the ETH
price from the feed:
data.chain.link/eth-usd
(0x5f4ec3df9cbd43714fe2740f5e3616155c5b8419
)
Here I will use
0xweb
📦 package manager to create contract classes and to read data from the blockchain.
# install 0xweb as global util
$ npm i 0xweb -g
# initialize dependencies in the current folder, for API usage. For CLI not required
$ 0xweb init
# download and generate classes for the contract
$ 0xweb install 0x5f4ec3df9cbd43714fe2740f5e3616155c5b8419 --name chainlink/oracle-eth
After the contract classes are generated, you can access onchain data via CLI or API
❗❣️❗ There are default KEYs for etherscan/co and infura. They are rate-limited. Please, create and insert your keys:
0xweb config -e
, replace Node URLs foreth
.
cli
$ 0xweb contract read chainlink/oracle-eth latestAnswer
api
import { ChainlinkOracleEth } from './0xweb/eth/chainlink/oracle-eth/oracle-eth';
import { Config } from '@dequanto/Config';
import { $bigint } from '@dequanto/utils/$bigint';
async function example () {
await Config.fetch();
const oracle = new ChainlinkOracleEth();
const decimals = await oracle.decimals();
const price: bigint = await oracle.latestAnswer();
console.log(`ETH Price: ${ $bigint.toEther(price, decimals) } USD`);
}
example();
These examples return current ETH
price. How to get the price at a specific block? Easy, by defining the block
number.
$ 0xweb contract read chainlink/oracle-eth latestAnswer --block 14450000
const price: bigint = await oracle.forBlock(14450000).latestAnswer();
⚠️ Ethereum 🗄️node URLs in default configuration are not archive nodes, so it won't work, you should replace them with archive node URLs.
Intermediate summary
Though ⬡ Chainlink is the most accurate and easy way to get the prices, but it doesn't contain all those thousands of tokens out there. That's why let's have a look at DEXes.
2.2 DEX
If a token is traded, that means we can get the price from there. Looks simple, but has its caveats. First of all, I'll show how to get the prices from UniswapV2
using 0xweb
cli and dequanto
libraries, and afterward, I briefly explain how it works under the hood.
cli
0xweb has already the built-in token command, which can retrieve prices from UniswapV2
$ 0xweb token price LINK
# or with block number (archive node required)
$ 0xweb token price LINK --block 14450000
# or by address
$ 0xweb token price 0x514910771af9ca656af840dff83e8264ecf986ca
# example of the command return
Symbol LINK
Address 0x514910771af9ca656af840dff83e8264ecf986ca
Decimals 18
Price 15.715912
api
import di from 'a-di';
import { Config } from '@dequanto/Config';
import { PlatformFactory } from '@dequanto/chains/PlatformFactory';
import { TokenPriceService } from '@dequanto/tokens/TokenPriceService';
async function example () {
// we need this once: to find and read all configurations
await Config.fetch();
let token = 'LINK';
// any unknown tokens are also supported
let token = { address: "0x12345abcd", decimals: 18 };
let chain = await di.resolve(PlatformFactory).get('eth')
let service = new TokenPriceService(chain.client, chain.explorer);
let { price } = await service.getPrice(token, {
block: 14450000
});
console.log(price);
}
example();
A couple of words about how to run the scripts from api examples. I use atma global package to run the scripts. It supports file middlewares, in particular the atma-loader-ts, it will compile, cache and execute the TS files on the fly: atma run ./example.ts
. All required packages and loader configuration will be created on 0xweb init
. But you can use any other loader/builder you already use for TypeScript.
⚙️ How it works
2.2.1 UniswapV2
interfaces
interface IUniswapV2Factory {
function getPair(address tokenA, address tokenB) external view returns (address pair);
}
interface IUniswapV2Pair {
function token0() external view returns (address);
function token1() external view returns (address);
function getReserves() external view returns (uint112 reserve0, uint112 reserve1, uint32 blockTimestampLast);
}
1. How to get the price of a token e.g.WETH
from the pair WETH/USDC
?
This step is trivial, the pair, has reserves of each token - amount of tokens in the pool. And the price is just a ratio of these amounts. price(WETH) = totalEther(USDC)/totalEther(WETH)
.
- ⚠️ Be aware, that each
ERC20
token can have different values fordecimals
, so just convert values toether
✨ By having two tokens, e.g.
0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48
(USDC
)/0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2
(WETH
) which one isreserve0
andreserve1
? Uniswap stores the tokens in a sorted way:token0 < token1
const USDC = '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48'; const WETH = '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2'; const lpReserves = await poolPair .forBlock(opts?.block ?? opts?.date) .getReserves(); let reserves = [lpReserves.reserve0, lpReserves.reserve1]; let sorted = BigInt(USDC) < BigInt(WETH); if (sorted === false) { reserves.reverse(); } let [ usdc, weth ] = reserves;
- ✨ From the previous example, you have noticed the
.fromBlock(opts?.block ?? opts?.date)
method - so yes, you can specify theBlockNumber
orDate
(which will be resolved to theBlockNumber
).
2. Okay, but how do I find out the address of the pair contract?
The UniswapV2Factory
contract has the method getPair
, which accepts any two tokens, and returns the pair contract (if exists ❗).
// tokens order doesn't matter here
factoryContract.getPair(USDC, WETH)
3. What if I want to get the price for a token, but there is no TOKEN/USDC
pair?
If we stick to on-chain solution only, you could index all pairs in the Factory
, then you have the list of all pairs for a token. But this is unnecessary, as it is enough to check all major stable coins and the wrapped eth with that token: USDC, USDT, DAI, WETH
.
- ⚠️ Often only
TOKEN/WETH
pair is present, or stable pairs have small liquidity, but as we already know - we can get the price forWETH
from Chainlink or any ofWETH/STABLE
pairs
Following points you have to think about:
- ✨ to get the price from the pool with the most liquidity
- ✨ to get the price for one or two nearby blocks and take the average
- ✨ this gives the price in stable coins, those are not always equal to
1$
🏁
Did you find this article valuable?
Support Alex Kit by becoming a sponsor. Any amount is appreciated!