Appearance
Join two series
Sample the share rate and the pooled ether on the same grid, and put them side by side on each block.
cql
let stETH = ethereum:0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84;
from stETH at 25_800_000..26_000_000 every 7200 as a
| extend ethPerShare = format(a.getPooledEthByShares(1e18), 18)
| join (
from stETH at 25_800_000..26_000_000 every 7200 as b
| extend pooledEther = format(b.getTotalPooledEther(), 18)
) on $block
| project { block: $block, ethPerShare, pooledEther }
| order by block asc29 rows, oldest first; the first six:
| block | ethPerShare | pooledEther |
|---|---|---|
| 25804800 | 1.24219014278314575 | 9577513.744139325389714808 |
| 25812000 | 1.242271090726486053 | 9577726.064524543518701572 |
| 25819200 | 1.242346440645230173 | 9578787.206360787975638105 |
| 25826400 | 1.242424181709375761 | 9583377.950886032964272514 |
| 25833600 | 1.24250107943967682 | 9621710.894523357280615501 |
| 25840800 | 1.242575976401495391 | 9624902.757062597687619272 |
One row per sampled block, with the share rate and the pooled ether at that block. The ethPerShare column is Sample a long range's, block for block. The two columns move differently. The share rate steps up once a day, when stETH rebases: the last two rows, 25 999 200 and 26 000 000, are 800 blocks apart and share one rate. The pooled ether moves at every sample, and falls as well as rises, because deposits and withdrawals change it between rebases; across those same two rows it grows by about 784.
How it reads
The two sources use the same range and the same step, so they land on the same blocks; that is what makes $block a usable key. An inner join keeps a row only where both sides have that block, which here is every block: 29 on each side, 29 joined. A key written as one name appears once in the output, so $block is one column, not two. The other columns both sides have are kept from both, the right one renamed with a number — $timestamp1, $address1 — and the project leaves them out.
The two sides read the same contract, which keeps the example small. The same shape joins two contracts — stETH on one side and wstETH's stEthPerToken() on the other — as long as their grids agree, and that is when join earns its place: two calls on one contract at one block fit in one project with no join at all, as Read the share rate now does.
Variations
- Everything on either side.
join kind=fullouter, thenwhere isnull(pooledEther)orwhere isnull(ethPerShare)to see the blocks one side lacks. - A ratio. Read
totalShares = format(b.getTotalShares(), 18)on the right-hand side besidepooledEther, then| extend computed = pooledEther / totalSharesafter the join: the share rate, computed from its two parts. - Across chains.
$blockis no key between chains, andon $blockacross two is refused (CQL3007). Sample both sides bytime … every 1h, give each a columnt = bin($sampleAt, 1h)withextend, and joinon t, as Compare a contract across chains does. End both ranges on the hour: an end between two hours is sampled too, and repeats the last hour.