ETL Specs
Overview
An ETL Spec is a TypeScript file that exports a config
object, and a handlers
object. These two objects are used by the Data Reporter to know what data sources to listen to, and what to do with the data it receives.
The config object
See the annotated example below for an overview of the config
object.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
import { ethers } from 'ethers';
import { parseUnits } from 'ethers/lib/utils';
import { ETLSpecConfig, Handler, Handlers } from '../types';
// An ETL Spec must export a config object that adheres to the ETLSpecConfig type.
export const config: ETLSpecConfig = {
// Sources is an object that has the different data sources the
// Data Reporter will listen to. Sources can be 'contracts' and 'api'.
sources: {
// Contracts are EVM contracts that emit events.
// A Consumer may request that the Data Reporter listen to any number of
// events on any number of contracts.
contracts: [
{
// Each contract source must contain the following:
//
// The chainId of the chain that the contract is deployed on.
chainId: 1,
// The contract address. In this example, this is the
// address of the Uniswap USDC/WETH pool
address: '0x88e6a0c2ddd26feeb64f039a2c41296fcb3f5640',
// A list of events to listen to in that contract.
//
// Each event must contain the following:
events: [
{
// The event signature as a string with each argument in a new line.
// The Data Reporter will convert this into an event topic and paramenters
// in order to parse the event bytes data into individual arguments.
definition: `
event Swap(
address indexed sender,
address indexed recipient,
int256 amount0,
int256 amount1,
uint160 sqrtPriceX96,
uint128 liquidity,
int24 tick
)
`,
// The name of the handler function that will process the event data.
handler: 'handleSwap',
// The destination to send the event to.
//
// The destination must contain the following:
destination: {
// The chainId of the chain the destination is on.
chainId: 8453,
// The address of the contract that will receive the data.
address: '0x2FEdc8B2C27Dc2916ecE1dA5c16dC92C9858A767',
// The function signature of the function that will receive the data.
signature: 'handleReceive(bytes)',
},
},
],
},
],
// API sources are web2 API endpoints that return JSON.
//
// They must contain the following:
api: [
{
// The HTTP method to use to make the request. Currently, only GET is supported.
type: 'GET',
// The URL to make the request to.
url: 'https://api.coingecko.com/api/v3/simple/price?ids=ethereum&vs_currencies=usd',
// The name of the handler function that will process the response data.
handler: 'handleApi',
// How often to make the request, in milliseconds.
rate: 10 * 60 * 1000,
// The destination to send the response to.
destination: {
// The chainId of the chain the destination is on.
chainId: 8453,
// The address of the contract that will receive the data.
address: '0x2FEdc8B2C27Dc2916ecE1dA5c16dC92C9858A767',
// The function signature of the function that will receive the data.
signature: 'handleReceive(bytes)',
},
},
],
},
};
The handler functions
The handler functions are an object that contains the functions the data sources listed as their handler
value. They will receive the source event's data as it's processed.
Annotated example handler functions for the events in our config object above are shown below.
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
// This is the handler function for the Swap on-chain event.
// It will be called every time the Swap event is emitted.
//
// All handler functions will receive the same object with the following keys:
// - error: An error object if there was an error processing the data.
// - data: The raw data that was received from the source.
// - decodedData: The decoded data that was received from the source.
// - store: A key-value store that persists between handler function calls.
const handleSwap: Handler = ({ error, data, decodedData, store }) => {
if (error) {
// If a handler function does not return a payload, the Data Reporter
// will not send the data to the destination.
return;
}
// The store is a key-value store that persists between handler function calls.
// In this example, we are using it to count how many times the handler function
// has been called, but it can be used to store any data you want to persist between events.
const calledCount = store.get('count') || 1;
console.log(`I have been called ${calledCount} times!`);
store.set('count', calledCount + 1);
const blockNumber = data.blockNumber;
const amount0 = decodedData.amount0;
const amount1 = decodedData.amount1;
// The payload is the data that will be sent to the destination.
// Because the destination is a contract, we need to encode the data
// into bytes.
const payload = ethers.utils.defaultAbiCoder.encode(
['uint256', 'uint256', 'int256', 'int256'],
[calledCount, blockNumber, amount0, amount1]
);
// If we want the data to be sent to the destination, we must return
// an object with a payload key.
return {
payload,
};
};
// This is the handler function for the API source.
// It receives the same arguments as the other handler function,
// but in this case, the data is the response body as from the API
// as JSON, and the decodedData will be null so we can omit it.
const handleApi: Handler = ({ data }) => {
const ethInUsd = parseUnits(data.ethereum.usd.toString(), 18);
const payload = ethers.utils.defaultAbiCoder.encode(['uint256'], [ethInUsd]);
// Again, we must return an object with a payload key to send the data.
// Otherwise, the Data Reporter will not send the data to the destination.
return {
payload,
};
};
// Finally, we export the handler functions as an object.
// We must export the handlers as a named export called "handlers".
export const handlers: Handlers = {
handleSwap,
handleApi,
};
Wrapping up
With these two pieces, config
and handlers
, the Data Reporter is ready to run with the command:nx serve mole-cli --args="--spec=spec-name-here"
A work in progress by Dan Cortes & Mitch Anderson