|
| 1 | +--- |
| 2 | +sidebar_position: 3 |
| 3 | +layout: example |
| 4 | +title: Block Handler - Vault Snapshots |
| 5 | +short_title: Vault Snapshots |
| 6 | +date: 2023-04-02 00:00:00 |
| 7 | +lang: en |
| 8 | +index: 3 |
| 9 | +type: example |
| 10 | +description: In this example we will look at how to capture snapshots of Yearn Vaults periodically |
| 11 | +short_desc: A simple arkiver job to snapshot yearn vaults |
| 12 | +--- |
| 13 | + |
| 14 | +In this example we will look at how to capture snapshots of Yearn Vaults periodically using the [Arkiver](https://github.com/RoboVault/robo-arkiver)'s Block Handler Data Source. |
| 15 | + |
| 16 | +The Block Handler data source is usefull when there aren't suitable events to index. Share price and TVL is a good example, often you will want a periodic update snapshot. |
| 17 | + |
| 18 | +## Requirements |
| 19 | + |
| 20 | +- [Deno](https://deno.com/manual@v1.33.1/getting_started/installation) |
| 21 | +- [Arkiver CLI](https://robo-arkiver-docs.vercel.app/docs/getting-started/prerequisites#install-arkiver-cli) |
| 22 | +- [Docker Compose](https://docs.docker.com/get-docker/) to running the index job locally |
| 23 | + |
| 24 | +## Create the Arkive Job |
| 25 | + |
| 26 | +Let's start by creating the arkiver job with the Arkiver CLI. Run `arkiver init` and select the **block-handler-vaults** template. |
| 27 | + |
| 28 | +```bash |
| 29 | +$ arkiver init |
| 30 | + |
| 31 | + |
| 32 | + ▄████████ ▄████████ ▄█ ▄█▄ ▄█ ▄█ █▄ ▄████████ ▄████████ |
| 33 | + ███ ███ ███ ███ ███ ▄███▀ ███ ███ ███ ███ ███ ███ ███ |
| 34 | + ███ ███ ███ ███ ███▐██▀ ███▌ ███ ███ ███ █▀ ███ ███ |
| 35 | + ███ ███ ▄███▄▄▄▄██▀ ▄█████▀ ███▌ ███ ███ ▄███▄▄▄ ▄███▄▄▄▄██▀ |
| 36 | +▀███████████ ▀▀███▀▀▀▀▀ ▀▀█████▄ ███▌ ███ ███ ▀▀███▀▀▀ ▀▀███▀▀▀▀▀ |
| 37 | + ███ ███ ▀███████████ ███▐██▄ ███ ███ ███ ███ █▄ ▀███████████ |
| 38 | + ███ ███ ███ ███ ███ ▀███▄ ███ ███ ███ ███ ███ ███ ███ |
| 39 | + ███ █▀ ███ ███ ███ ▀█▀ █▀ ▀██████▀ ██████████ ███ ███ |
| 40 | + ███ ███ ▀ ███ ███ |
| 41 | + |
| 42 | + |
| 43 | + -----===== Arkiver v0.4.4 - https://arkiver.net =====----- |
| 44 | + |
| 45 | + ? Where should we create your arkive? (./cool-new-arkive) › ./vaults-arkive |
| 46 | + ? Which template would you like to use? (event-wildcard) › block-handler-vaults |
| 47 | + ? Are you using VSCode? (Yes) › Yes |
| 48 | + ✔ Initialized arkive |
| 49 | +``` |
| 50 | + |
| 51 | +This arkive exmaple is prepared with everything we need. |
| 52 | +:::tip Let's take a look inside |
| 53 | + |
| 54 | +Infomation overload? Skip to [Running the arkive](#run-indexing-locally) |
| 55 | + |
| 56 | +### Entities |
| 57 | + |
| 58 | +Entities specify how the indexed data is stored in the db and how it is accessed via the graphql interface. |
| 59 | + |
| 60 | +We need only one entity, the VaultSnapshot entity containing: |
| 61 | +- Infomation about the vault |
| 62 | +- Vault share price |
| 63 | +- Block and Timestamp |
| 64 | + |
| 65 | +```ts title="entities.ts" |
| 66 | +import { createEntity } from "../deps.ts"; |
| 67 | + |
| 68 | +export interface IVaultSnapshot { |
| 69 | + vault: string |
| 70 | + name: string |
| 71 | + symbol: string |
| 72 | + block: number |
| 73 | + timestamp: number |
| 74 | + sharePrice: number |
| 75 | +} |
| 76 | + |
| 77 | +export const VaultSnapshot = createEntity<IVaultSnapshot>("VaultSnapshot", { |
| 78 | + vault: String, |
| 79 | + name: String, |
| 80 | + symbol: String, |
| 81 | + block: { type: Number, index: true }, |
| 82 | + timestamp: { type: Number, index: true }, |
| 83 | + sharePrice: { type: Number, index: true }, |
| 84 | +}); |
| 85 | +``` |
| 86 | + |
| 87 | +Note the usse of the `IVaultSnapshot`, this is optional but it provides typing of the entity. |
| 88 | + |
| 89 | +### Manifest |
| 90 | + |
| 91 | +The manifest configures the datasources of the index job. We |
| 92 | + |
| 93 | +```ts title="manifest.ts" |
| 94 | +import { Manifest } from "https://deno.land/x/robo_arkiver@v0.4.4/mod.ts"; |
| 95 | +import { VaultSnapshot } from "./entities/vault.ts"; |
| 96 | +import { snapshotVault } from "./handlers/vault.ts"; |
| 97 | + |
| 98 | +const manifest = new Manifest("yearn-vaults"); |
| 99 | + |
| 100 | +manifest |
| 101 | + .addEntity(VaultSnapshot) |
| 102 | + .chain("mainnet") |
| 103 | + .addBlockHandler({ blockInterval: 1000, startBlockHeight: 12790000n, handler: snapshotVault }) |
| 104 | + |
| 105 | +export default manifest |
| 106 | + .build(); |
| 107 | + |
| 108 | +``` |
| 109 | + |
| 110 | +This manifest file configures the arkive job with one data source, a Block Handler. The block handler will periodically call the handler `snapshotVault` every 1000 blocks, startnig from block 12790000 on Ethereum. As per usual, the VaultSnapshot entity is added to the manifest to make it accessible via the GraphQL endpoint. |
| 111 | + |
| 112 | +### Handler |
| 113 | + |
| 114 | +`vault.ts` contains `snapshotVault()`, which is called every 1000 blocks. Let's break down what it's doing |
| 115 | + |
| 116 | +```ts title="vault.ts" |
| 117 | + // Get vault info from cache or onchain |
| 118 | + const vaults = await Promise.all(liveVaults.map(async vault => { |
| 119 | + const contract = getContract({ address: vault.address, abi: YearnV2Abi, publicClient: client }) |
| 120 | + return { |
| 121 | + address: vault.address, |
| 122 | + vault: { address: vault.address, abi: YearnV2Abi } as const, |
| 123 | + contract, |
| 124 | + name: await store.retrieve(`${vault.address}:name`, async () => await contract.read.name()), |
| 125 | + symbol: await store.retrieve(`${vault.address}:symbol`, async () => await contract.read.symbol()), |
| 126 | + decimals: await store.retrieve(`${vault.address}:decimals`, async () => await contract.read.decimals()) |
| 127 | + } |
| 128 | + })) |
| 129 | +``` |
| 130 | + |
| 131 | +To kick things off we grab the name, symbol and decimals for each of the vaults. We use `store` here to cache the results so they're only called on the first call. The name and symbols is stored with the snaphots and decimals is used to format the share prices. |
| 132 | + |
| 133 | +```ts title="vault.ts" |
| 134 | + |
| 135 | + // fetch share price for this block |
| 136 | + const sharePrices = (await Promise.all(vaults.map(e => { |
| 137 | + return client.readContract({ |
| 138 | + address: e.address, |
| 139 | + abi: YearnV2Abi, |
| 140 | + functionName: 'pricePerShare', |
| 141 | + blockNumber: block.number, |
| 142 | + }) |
| 143 | + }))) |
| 144 | +``` |
| 145 | + |
| 146 | +Next up is fetching the share price for each of the vaults with the `vault.pricePerShare()`. This returns a bigint and is formatted to a float further down. |
| 147 | + |
| 148 | +``` |
| 149 | + // Save the vault snapshots |
| 150 | + vaults.map((vault, i) => { |
| 151 | + return new VaultSnapshot({ |
| 152 | + id: `${vault.address}-${Number(block.number)}`, |
| 153 | + block: Number(block.number), |
| 154 | + timestamp: Number(block.timestamp), |
| 155 | + vault: vault.address, |
| 156 | + sharePrice: parseFloat(formatUnits(sharePrices[i], Number(vault.decimals))), |
| 157 | + name: vault.name, |
| 158 | + symbol: vault.symbol, |
| 159 | + }) |
| 160 | + }).map(e => e.save()) |
| 161 | +``` |
| 162 | + |
| 163 | +And here we finally create and save the snapshots. |
| 164 | +::: |
| 165 | + |
| 166 | +## Run Indexing Locally |
| 167 | + |
| 168 | +Run the index job locally. This will spin up a database and the graphql server with docker for a fully-feature local dev environment |
| 169 | +> Optional: run `arkiver start --help` to see the options |
| 170 | +
|
| 171 | +```bash |
| 172 | +$ arkiver start . |
| 173 | +``` |
| 174 | + |
| 175 | +Desired output: |
| 176 | +```bash |
| 177 | + -----===== Arkiver v0.4.4 - https://arkiver.net =====----- |
| 178 | + |
| 179 | +[0:yearn-vaults@v1.0] INFO Running Arkive - yearn-vaults |
| 180 | +🚀 Arkiver playground ready at http://0.0.0.0:4000/graphql |
| 181 | +[0:yearn-vaults@v1.0] INFO Running handlers for blocks 14399001-14402002 (3000 blocks - 3 items) |
| 182 | +[0:yearn-vaults@v1.0] INFO Processed blocks 14399001-14402002 in 3686.594ms (813.759 blocks/s - 0.814 items/s) |
| 183 | +[0:yearn-vaults@v1.0] INFO Running handlers for blocks 14402002-14405003 (3000 blocks - 3 items) |
| 184 | +[0:yearn-vaults@v1.0] INFO Processed blocks 14402002-14405003 in 3451.422ms (869.207 blocks/s - 0.869 items/s) |
| 185 | +... |
| 186 | +``` |
| 187 | + |
| 188 | +The index job is running, you can now navigate to http://0.0.0.0:4000/graphql to see the graphql explorer to experiment with the indexed data. |
| 189 | + |
| 190 | +Note: Make sure you click the explorer icon on the left menu to see what queery options are available. |
| 191 | + |
| 192 | + |
| 193 | + |
| 194 | +## Explore the data |
| 195 | + |
| 196 | +Now we can query the historic share price of the yearn vaults |
| 197 | + |
| 198 | +```graphql |
| 199 | +query MyQuery { |
| 200 | + VaultSnapshots(sort: TIMESTAMP_DESC, filter: {symbol: "yvDAI"}) { |
| 201 | + sharePrice |
| 202 | + name |
| 203 | + symbol |
| 204 | + timestamp |
| 205 | + } |
| 206 | +} |
| 207 | +``` |
| 208 | + |
| 209 | +```json title="response" |
| 210 | +{ |
| 211 | + "VaultSnapshots": [ |
| 212 | + { |
| 213 | + "sharePrice": 1.0271595721786284, |
| 214 | + "name": "DAI yVault", |
| 215 | + "symbol": "yvDAI", |
| 216 | + "timestamp": 1648837864 |
| 217 | + }, |
| 218 | + { |
| 219 | + "sharePrice": 1.0271595721786284, |
| 220 | + "name": "DAI yVault", |
| 221 | + "symbol": "yvDAI", |
| 222 | + "timestamp": 1648824336 |
| 223 | + }, |
| 224 | + ... |
| 225 | + ] |
| 226 | +} |
| 227 | +``` |
| 228 | + |
| 229 | +## Deploy to Production |
| 230 | + |
| 231 | +To deploy the arkive job to production you must sign in to your arkiver account. Run `arkiver help` for more infomation. |
| 232 | + |
| 233 | +To deploy, simply run: |
| 234 | + |
| 235 | +```bash |
| 236 | +arkiver deploy . |
| 237 | +``` |
| 238 | + |
| 239 | +This will package an deploy the arkive job. The name of the arkive job is specified in the manifiest file, in this example it's "yearn-vaults". |
| 240 | + |
| 241 | +Navigate to `https://data.arkiver.net/$USERNAME/yearn-vaults/graphql`, where $USERNAME is your username, to see your custom, production-ready graphql endpoint. |
| 242 | + |
| 243 | +Here is a deployment we prepared perviously: |
| 244 | +> https://data.arkiver.net/robolabs/yearn-vaults/graphql |
0 commit comments