cre

CRE

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

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