DEMO 1
Bring Your Financial Data Onchain
See how you can bring your financial data onchain in a variety of different ways by creating a Net Asset Value feed, Proof of Reserve feed, or other custom data feed
Net Asset Value
Fund name
Block Partners
NAV
How It Works
Under the Hood
1//go:build wasip1
2
3package main
4
5import (
6 "encoding/hex"
7 "encoding/json"
8 "errors"
9 "fmt"
10 "log/slog"
11 "math/big"
12 "time"
13
14 "{{projectName}}/contracts/evm/src/generated/balance_reader"
15 "{{projectName}}/contracts/evm/src/generated/ierc20"
16 "{{projectName}}/contracts/evm/src/generated/message_emitter"
17 "{{projectName}}/contracts/evm/src/generated/reserve_manager"
18
19 "github.com/ethereum/go-ethereum/common"
20 "github.com/shopspring/decimal"
21
22 pb "github.com/smartcontractkit/chainlink-protos/cre/go/sdk"
23 "github.com/smartcontractkit/cre-sdk-go/capabilities/blockchain/evm"
24 "github.com/smartcontractkit/cre-sdk-go/capabilities/networking/http"
25 "github.com/smartcontractkit/cre-sdk-go/capabilities/scheduler/cron"
26 "github.com/smartcontractkit/cre-sdk-go/cre"
27 "github.com/smartcontractkit/cre-sdk-go/cre/wasm"
28)
29
30const (
31 SecretName = "SECRET_ADDRESS"
32)
33
34// EVMConfig holds per-chain configuration.
35type EVMConfig struct {
36 TokenAddress string `json:"tokenAddress"`
37 PORAddress string `json:"porAddress"`
38 ProxyAddress string `json:"proxyAddress"`
39 BalanceReaderAddress string `json:"balanceReaderAddress"`
40 MessageEmitterAddress string `json:"messageEmitterAddress"`
41 ChainSelector uint64 `json:"chainSelector"`
42 GasLimit uint64 `json:"gasLimit"`
43}
44
45type Config struct {
46 Schedule string `json:"schedule"`
47 URL string `json:"url"`
48 EVMs []EVMConfig `json:"evms"`
49}
50
51type HTTPTriggerPayload struct {
52 ExecutionTime time.Time `json:"executionTime"`
53}
54
55type ReserveInfo struct {
56 LastUpdated time.Time `consensus_aggregation:"median" json:"lastUpdated"`
57 TotalReserve decimal.Decimal `consensus_aggregation:"median" json:"totalReserve"`
58}
59
60type PORResponse struct {
61 AccountName string `json:"accountName"`
62 TotalTrust float64 `json:"totalTrust"`
63 TotalToken float64 `json:"totalToken"`
64 Ripcord bool `json:"ripcord"`
65 UpdatedAt time.Time `json:"updatedAt"`
66}
67
68func InitWorkflow(config *Config, logger *slog.Logger, secretsProvider cre.SecretsProvider) (cre.Workflow[*Config], error) {
69 cronTriggerCfg := &cron.Config{
70 Schedule: config.Schedule,
71 }
72
73 logTriggerCfg := &evm.FilterLogTriggerRequest{
74 Addresses: make([][]byte, len(config.EVMs)),
75 }
76
77 for i, evmCfg := range config.EVMs {
78 address, err := hex.DecodeString(evmCfg.MessageEmitterAddress[2:])
79 if err != nil {
80 return nil, fmt.Errorf("failed to decode MessageEmitter address %s: %w", evmCfg.MessageEmitterAddress, err)
81 }
82 logTriggerCfg.Addresses[i] = address
83 }
84
85 httpTriggerCfg := &http.Config{}
86
87 return cre.Workflow[*Config]{
88 cre.Handler(
89 cron.Trigger(cronTriggerCfg),
90 onPORCronTrigger,
91 ),
92 cre.Handler(
93 evm.LogTrigger(config.EVMs[0].ChainSelector, logTriggerCfg),
94 onLogTrigger,
95 ),
96 cre.Handler(
97 http.Trigger(httpTriggerCfg),
98 onHTTPTrigger,
99 ),
100 }, nil
101}
102
103func onPORCronTrigger(config *Config, runtime cre.Runtime, outputs *cron.Payload) (string, error) {
104 return doPOR(config, runtime, outputs.ScheduledExecutionTime.AsTime())
105}
106
107func onLogTrigger(config *Config, runtime cre.Runtime, payload *evm.Log) (string, error) {
108 logger := runtime.Logger()
109 messageEmitter, err := prepareMessageEmitter(logger, config.EVMs[0])
110 if err != nil {
111 return "", fmt.Errorf("failed to prepare message emitter: %w", err)
112 }
113
114 topics := payload.GetTopics()
115 if len(topics) < 3 {
116 logger.Error("Log payload does not contain enough topics", "topics", topics)
117 return "", fmt.Errorf("log payload does not contain enough topics: %d", len(topics))
118 }
119
120 // topics[1] is a 32-byte topic, but the address is the last 20 bytes
121 emitter := topics[1][12:]
122 lastMessageInput := message_emitter.GetLastMessageInput{
123 Emitter: common.Address(emitter),
124 }
125
126 message, err := messageEmitter.GetLastMessage(runtime, lastMessageInput, big.NewInt(8771643)).Await()
127 if err != nil {
128 logger.Error("Could not read from contract", "contract_chain", config.EVMs[0].ChainSelector, "err", err.Error())
129 return "", err
130 }
131
132 logger.Info("Message retrieved from the contract", "message", message)
133
134 return message, nil
135}
136
137func onHTTPTrigger(config *Config, runtime cre.Runtime, payload *http.Payload) (string, error) {
138 logger := runtime.Logger()
139 logger.Info("Raw HTTP trigger received")
140
141 // If there’s no input, fall back to “now”.
142 if len(payload.Input) == 0 {
143 logger.Warn("HTTP trigger payload is empty; defaulting execution time to now")
144 return doPOR(config, runtime, time.Now().UTC())
145 }
146
147 // Log the raw JSON for debugging (human-readable).
148 logger.Info("Payload bytes", "payloadBytes", string(payload.Input))
149
150 // Unmarshal raw JSON bytes directly into your struct.
151 var req HTTPTriggerPayload
152 if err := json.Unmarshal(payload.Input, &req); err != nil {
153 logger.Error("failed to unmarshal http trigger payload", "err", err)
154 return "", err
155 }
156
157 // Provide a sensible default if the field is missing/zero.
158 if req.ExecutionTime.IsZero() {
159 req.ExecutionTime = time.Now().UTC()
160 }
161
162 logger.Info("Parsed HTTP trigger received", "payload", req)
163 return doPOR(config, runtime, req.ExecutionTime)
164}
165
166func doPOR(config *Config, runtime cre.Runtime, runTime time.Time) (string, error) {
167 logger := runtime.Logger()
168 // Fetch PoR
169 logger.Info("fetching por", "url", config.URL, "evms", config.EVMs)
170 client := &http.Client{}
171 reserveInfo, err := http.SendRequest(config, runtime, client, fetchPOR, cre.ConsensusAggregationFromTags[*ReserveInfo]()).Await()
172 if err != nil {
173 logger.Error("error fetching por", "err", err)
174 return "", err
175 }
176
177 logger.Info("ReserveInfo", "reserveInfo", reserveInfo)
178
179 totalSupply, err := getTotalSupply(config, runtime)
180 if err != nil {
181 return "", err
182 }
183
184 logger.Info("TotalSupply", "totalSupply", totalSupply)
185 totalReserveScaled := reserveInfo.TotalReserve.Mul(decimal.NewFromUint64(1e18)).BigInt()
186 logger.Info("TotalReserveScaled", "totalReserveScaled", totalReserveScaled)
187
188 nativeTokenBalance, err := fetchNativeTokenBalance(runtime, config.EVMs[0], config.EVMs[0].TokenAddress)
189 if err != nil {
190 return "", fmt.Errorf("failed to fetch native token balance: %w", err)
191 }
192 logger.Info("Native token balance", "token", config.EVMs[0].TokenAddress, "balance", nativeTokenBalance)
193
194 secretReq := &pb.SecretRequest{
195 Id: SecretName,
196 }
197
198 secretAddress, err := runtime.GetSecret(secretReq).Await()
199 if err != nil {
200 logger.Error(fmt.Sprintf("failed to get secret address: %v", err))
201 return "", err
202 }
203 secretAddressBalance, err := fetchNativeTokenBalance(runtime, config.EVMs[0], secretAddress.Value)
204 if err != nil {
205 return "", fmt.Errorf("failed to fetch secret address balance: %w", err)
206 }
207 logger.Info("Secret address balance", "balance", secretAddressBalance)
208
209 // Update reserves
210 if err := updateReserves(config, runtime, totalSupply, totalReserveScaled); err != nil {
211 return "", fmt.Errorf("failed to update reserves: %w", err)
212 }
213
214 return reserveInfo.TotalReserve.String(), nil
215}
216
217func prepareMessageEmitter(logger *slog.Logger, evmCfg EVMConfig) (*message_emitter.MessageEmitter, error) {
218 evmClient := &evm.Client{
219 ChainSelector: evmCfg.ChainSelector,
220 }
221
222 address := common.HexToAddress(evmCfg.MessageEmitterAddress)
223
224 messageEmitter, err := message_emitter.NewMessageEmitter(evmClient, address, nil)
225 if err != nil {
226 logger.Error("failed to create message emitter", "address", evmCfg.MessageEmitterAddress, "err", err)
227 return nil, fmt.Errorf("failed to create message emitter for address %s: %w", evmCfg.MessageEmitterAddress, err)
228 }
229
230 return messageEmitter, nil
231}
232
233func fetchNativeTokenBalance(runtime cre.Runtime, evmCfg EVMConfig, tokenHolderAddress string) (*big.Int, error) {
234 logger := runtime.Logger()
235 evmClient := &evm.Client{
236 ChainSelector: evmCfg.ChainSelector,
237 }
238
239 balanceReaderAddress := common.HexToAddress(evmCfg.BalanceReaderAddress)
240 balanceReader, err := balance_reader.NewBalanceReader(evmClient, balanceReaderAddress, nil)
241 if err != nil {
242 logger.Error("failed to create balance reader", "address", evmCfg.BalanceReaderAddress, "err", err)
243 return nil, fmt.Errorf("failed to create balance reader for address %s: %w", evmCfg.BalanceReaderAddress, err)
244 }
245 tokenAddress, err := hexToBytes(tokenHolderAddress)
246 if err != nil {
247 logger.Error("failed to decode token address", "address", tokenHolderAddress, "err", err)
248 return nil, fmt.Errorf("failed to decode token address %s: %w", tokenHolderAddress, err)
249 }
250
251 logger.Info("Getting native balances", "address", evmCfg.BalanceReaderAddress, "tokenAddress", tokenHolderAddress)
252 balances, err := balanceReader.GetNativeBalances(runtime, balance_reader.GetNativeBalancesInput{
253 Addresses: []common.Address{common.Address(tokenAddress)},
254 }, big.NewInt(8771643)).Await()
255
256 if err != nil {
257 logger.Error("Could not read from contract", "contract_chain", evmCfg.ChainSelector, "err", err.Error())
258 return nil, err
259 }
260
261 if len(balances) < 1 {
262 logger.Error("No balances returned from contract", "contract_chain", evmCfg.ChainSelector)
263 return nil, fmt.Errorf("no balances returned from contract for chain %d", evmCfg.ChainSelector)
264 }
265
266 return balances[0], nil
267}
268
269func getTotalSupply(config *Config, runtime cre.Runtime) (*big.Int, error) {
270 evms := config.EVMs
271 logger := runtime.Logger()
272 // Fetch supply from all EVMs in parallel
273 supplyPromises := make([]cre.Promise[*big.Int], len(evms))
274 for i, evmCfg := range evms {
275 evmClient := &evm.Client{
276 ChainSelector: evmCfg.ChainSelector,
277 }
278
279 address := common.HexToAddress(evmCfg.TokenAddress)
280 token, err := ierc20.NewIERC20(evmClient, address, nil)
281 if err != nil {
282 logger.Error("failed to create token", "address", evmCfg.TokenAddress, "err", err)
283 return nil, fmt.Errorf("failed to create token for address %s: %w", evmCfg.TokenAddress, err)
284 }
285 evmTotalSupplyPromise := token.TotalSupply(runtime, big.NewInt(8771643))
286 supplyPromises[i] = evmTotalSupplyPromise
287 }
288
289 // We can add cre.AwaitAll that takes []cre.Promise[T] and returns ([]T, error)
290 totalSupply := big.NewInt(0)
291 for i, promise := range supplyPromises {
292 supply, err := promise.Await()
293 if err != nil {
294 selector := evms[i].ChainSelector
295 logger.Error("Could not read from contract", "contract_chain", selector, "err", err.Error())
296 return nil, err
297 }
298
299 totalSupply = totalSupply.Add(totalSupply, supply)
300 }
301
302 return totalSupply, nil
303}
304
305func updateReserves(config *Config, runtime cre.Runtime, totalSupply *big.Int, totalReserveScaled *big.Int) error {
306 evmCfg := config.EVMs[0]
307 logger := runtime.Logger()
308 logger.Info("Updating reserves", "totalSupply", totalSupply, "totalReserveScaled", totalReserveScaled)
309 evmClient := &evm.Client{
310 ChainSelector: evmCfg.ChainSelector,
311 }
312 reserveManager, err := reserve_manager.NewReserveManager(evmClient, common.HexToAddress(evmCfg.ProxyAddress), nil)
313 if err != nil {
314 return fmt.Errorf("failed to create reserve manager: %w", err)
315 }
316
317 logger.Info("Writing report", "totalSupply", totalSupply, "totalReserveScaled", totalReserveScaled)
318 resp, err := reserveManager.WriteReportFromUpdateReserves(runtime, reserve_manager.UpdateReserves{
319 TotalMinted: totalSupply,
320 TotalReserve: totalReserveScaled,
321 }, nil).Await()
322
323 if err != nil {
324 logger.Error("WriteReport await failed", "error", err, "errorType", fmt.Sprintf("%T", err))
325 return fmt.Errorf("failed to write report: %w", err)
326 }
327 logger.Info("Write report succeeded", "response", resp)
328 logger.Info("Write report transaction succeeded at", "txHash", common.BytesToHash(resp.TxHash).Hex())
329 return nil
330}
331
332func fetchPOR(config *Config, logger *slog.Logger, sendRequester *http.SendRequester) (*ReserveInfo, error) {
333 httpActionOut, err := sendRequester.SendRequest(&http.Request{
334 Method: "GET",
335 Url: config.URL,
336 }).Await()
337 if err != nil {
338 return nil, err
339 }
340
341 porResp := &PORResponse{}
342 if err = json.Unmarshal(httpActionOut.Body, porResp); err != nil {
343 return nil, err
344 }
345
346 if porResp.Ripcord {
347 return nil, errors.New("ripcord is true")
348 }
349
350 res := &ReserveInfo{
351 LastUpdated: porResp.UpdatedAt.UTC(),
352 TotalReserve: decimal.NewFromFloat(porResp.TotalToken),
353 }
354 return res, nil
355}
356
357func hexToBytes(hexStr string) ([]byte, error) {
358 if len(hexStr) < 2 || hexStr[:2] != "0x" {
359 return nil, fmt.Errorf("invalid hex string: %s", hexStr)
360 }
361 return hex.DecodeString(hexStr[2:])
362}
363
364func main() {
365 wasm.NewRunner(cre.ParseJSON[Config]).Run(InitWorkflow)
366}
367