11package datafeeds
22
33import (
4+ "crypto/sha256"
45 "encoding/binary"
56 "encoding/json"
67 "errors"
78 "fmt"
89 "math/big"
910 "strconv"
1011
12+ chainselectors "github.com/smartcontractkit/chain-selectors"
1113 ocrcommon "github.com/smartcontractkit/libocr/commontypes"
1214 ocr2types "github.com/smartcontractkit/libocr/offchainreporting2/types"
1315 ocr3types "github.com/smartcontractkit/libocr/offchainreporting2plus/ocr3types"
1416
1517 "github.com/smartcontractkit/chainlink-common/pkg/capabilities"
1618 "github.com/smartcontractkit/chainlink-common/pkg/capabilities/consensus/ocr3/types"
1719 "github.com/smartcontractkit/chainlink-common/pkg/logger"
20+ "github.com/smartcontractkit/chainlink-common/pkg/types/chains/solana"
1821 "github.com/smartcontractkit/chainlink-protos/cre/go/values"
1922)
2023
2124var (
2225 ErrNoMatchingChainSelector = errors .New ("no matching chain selector found" )
2326)
2427
28+ type SolanaEncoderKey = string
29+
30+ const (
31+ /*
32+ OutputFormat for solana:
33+ "account_context_hash": <"hash">,
34+ "payload": []reports{timestamp uint32, answer *big.Int, dataId [16]byte }
35+ Solana encoder compatible idl config:
36+ encoderConfig := map[string]any{
37+ report_schema": `{
38+ "kind": "struct",
39+ "fields": [
40+ { "name": "payload", "type": { "vec": { "defined": "DecimalReport" } } }
41+ ]
42+ }`,
43+ "defined_types": `[
44+ {
45+ "name":"DecimalReport",
46+ "type":{
47+ "kind":"struct",
48+ "fields":[
49+ { "name":"timestamp", "type":"u32" },
50+ { "name":"answer", "type":"u128" },
51+ { "name": "dataId", "type": {"array": ["u8",16]}}
52+ ]
53+ }
54+ }
55+ ]`,
56+ }
57+
58+ */
59+ TopLevelPayloadListFieldName = SolanaEncoderKey ("payload" )
60+ TopLevelAccountCtxHashFieldName = SolanaEncoderKey ("account_context_hash" )
61+ SolTimestampOutputFieldName = SolanaEncoderKey ("timestamp" )
62+ SolAnswerOutputFieldName = SolanaEncoderKey ("answer" )
63+ SolDataIDOutputFieldName = SolanaEncoderKey ("dataId" )
64+ )
65+
2566// secureMintReport represents the inner report structure, mimics the Report type in the SM plugin repo
2667type secureMintReport struct {
2768 ConfigDigest ocr2types.ConfigDigest `json:"configDigest"`
@@ -33,11 +74,17 @@ type secureMintReport struct {
3374// chainSelector represents the chain selector type, mimics the ChainSelector type in the SM plugin repo
3475type chainSelector uint64
3576
77+ type SolanaConfig struct {
78+ // Add Solana-specific configuration fields here
79+ AccountContext solana.AccountMetaSlice `mapstructure:"remaining_accounts"`
80+ }
81+
3682// SecureMintAggregatorConfig is the config for the SecureMint aggregator.
3783// This aggregator is designed to pick out reports for a specific chain selector.
3884type SecureMintAggregatorConfig struct {
3985 // TargetChainSelector is the chain selector to look for
4086 TargetChainSelector chainSelector `mapstructure:"targetChainSelector"`
87+ Solana SolanaConfig `mapstructure:"solana"`
4188}
4289
4390// ToMap converts the SecureMintAggregatorConfig to a values.Map, which is suitable for the
@@ -54,7 +101,152 @@ func (c SecureMintAggregatorConfig) ToMap() (*values.Map, error) {
54101var _ types.Aggregator = (* SecureMintAggregator )(nil )
55102
56103type SecureMintAggregator struct {
57- config SecureMintAggregatorConfig
104+ config SecureMintAggregatorConfig
105+ registry FormatterFactory
106+ }
107+
108+ type ChainReportFormatter interface {
109+ PackReport (lggr logger.Logger , report * secureMintReport ) (* values.Map , error )
110+ }
111+
112+ type EVMReportFormatter struct {
113+ TargetChainSelector uint64
114+ }
115+
116+ func (f * EVMReportFormatter ) PackReport (lggr logger.Logger , report * secureMintReport ) (* values.Map , error ) {
117+ // Convert chain selector to bytes for data ID
118+ // Secure Mint dataID: 0x04 + chain selector as bytes + right padded with 0s
119+ var chainSelectorAsDataID [16 ]byte
120+ chainSelectorAsDataID [0 ] = 0x04
121+ binary .BigEndian .PutUint64 (chainSelectorAsDataID [1 :], uint64 (f .TargetChainSelector ))
122+
123+ smReportAsAnswer , err := packSecureMintReportIntoUint224ForEVM (report .Mintable , report .Block )
124+ if err != nil {
125+ return nil , fmt .Errorf ("failed to pack secure mint report for evm into uint224: %w" , err )
126+ }
127+
128+ lggr .Debugw ("packed report into answer" , "smReportAsAnswer" , smReportAsAnswer )
129+
130+ // This is what the DF Cache contract expects:
131+ // abi: "(bytes16 dataId, uint32 timestamp, uint224 answer)[] Reports"
132+ toWrap := []any {
133+ map [EVMEncoderKey ]any {
134+ DataIDOutputFieldName : chainSelectorAsDataID ,
135+ AnswerOutputFieldName : smReportAsAnswer ,
136+ TimestampOutputFieldName : int64 (report .Block ),
137+ },
138+ }
139+
140+ wrappedReport , err := values .NewMap (map [string ]any {
141+ TopLevelListOutputFieldName : toWrap ,
142+ })
143+ if err != nil {
144+ return nil , fmt .Errorf ("failed to wrap report: %w" , err )
145+ }
146+
147+ return wrappedReport , nil
148+ }
149+
150+ func NewEVMReportFormatter (chainSelector uint64 , config SecureMintAggregatorConfig ) (ChainReportFormatter , error ) {
151+ return & EVMReportFormatter {TargetChainSelector : chainSelector }, nil
152+ }
153+
154+ type SolanaReportFormatter struct {
155+ TargetChainSelector uint64
156+ OnReportAccounts solana.AccountMetaSlice
157+ }
158+
159+ func (f * SolanaReportFormatter ) PackReport (lggr logger.Logger , report * secureMintReport ) (* values.Map , error ) {
160+ // TEMPORARY DATA ID
161+ // Convert chain selector to bytes for data ID
162+ // Secure Mint dataID: 0x04 + chain selector as bytes + right padded with 0s
163+ var chainSelectorAsDataID [16 ]byte
164+ chainSelectorAsDataID [0 ] = 0x04
165+ binary .BigEndian .PutUint64 (chainSelectorAsDataID [1 :], uint64 (f .TargetChainSelector ))
166+
167+ // pack answer
168+ smReportAsAnswer , err := packSecureMintReportIntoU128ForSolana (report .Mintable , report .Block )
169+ if err != nil {
170+ return nil , fmt .Errorf ("failed to pack secure mint report for solana into u128: %w" , err )
171+ }
172+ lggr .Debugw ("packed report into answer" , "smReportAsAnswer" , smReportAsAnswer )
173+
174+ // hash account contexts
175+ var accounts = make ([]byte , 0 )
176+ for _ , acc := range f .OnReportAccounts {
177+ accounts = append (accounts , acc .PublicKey [:]... )
178+ }
179+ accountContextHash := sha256 .Sum256 (accounts )
180+ lggr .Debugw ("calculated account context hash" , "accountContextHash" , accountContextHash )
181+
182+ if report .Block > (1 << 32 - 1 ) { // timestamp must fit in u32 in solana
183+ return nil , fmt .Errorf ("timestamp exceeds u32 bounds: %v" , report .Block )
184+ }
185+
186+ toWrap := []any {
187+ map [SolanaEncoderKey ]any {
188+ SolTimestampOutputFieldName : uint32 (report .Block ), // TODO: Verify with Michael/Geert timestamp should be block number?
189+ SolAnswerOutputFieldName : smReportAsAnswer ,
190+ SolDataIDOutputFieldName : chainSelectorAsDataID ,
191+ },
192+ }
193+
194+ wrappedReport , err := values .NewMap (map [string ]any {
195+ TopLevelAccountCtxHashFieldName : accountContextHash ,
196+ TopLevelPayloadListFieldName : toWrap ,
197+ })
198+
199+ if err != nil {
200+ return nil , fmt .Errorf ("failed to wrap report: %w" , err )
201+ }
202+
203+ return wrappedReport , nil
204+ }
205+
206+ func NewSolanaReportFormatter (chainSelector uint64 , config SecureMintAggregatorConfig ) (ChainReportFormatter , error ) {
207+ return & SolanaReportFormatter {TargetChainSelector : chainSelector , OnReportAccounts : config .Solana .AccountContext }, nil
208+ }
209+
210+ type Builder func (chainSelector uint64 , config SecureMintAggregatorConfig ) (ChainReportFormatter , error )
211+
212+ type FormatterFactory interface {
213+ Register (chainSelector uint64 , builder Builder )
214+ Get (chainSelector uint64 , config SecureMintAggregatorConfig ) (ChainReportFormatter , error )
215+ }
216+
217+ type DefaultFormatterFactory struct {
218+ builders map [uint64 ]Builder
219+ }
220+
221+ func (r * DefaultFormatterFactory ) Register (chainSelector uint64 , builder Builder ) {
222+ r .builders [chainSelector ] = builder
223+ }
224+
225+ func (r * DefaultFormatterFactory ) Get (chainSelector uint64 , config SecureMintAggregatorConfig ) (ChainReportFormatter , error ) {
226+ b , ok := r .builders [chainSelector ]
227+ if ! ok {
228+ return nil , fmt .Errorf ("no formatter registered for chain selector: %d" , chainSelector )
229+ }
230+
231+ return b (chainSelector , config )
232+ }
233+
234+ func NewDefaultFormatterFactory () FormatterFactory {
235+ r := DefaultFormatterFactory {
236+ builders : map [uint64 ]Builder {},
237+ }
238+
239+ // EVM
240+ for _ , selector := range chainselectors .EvmChainIdToChainSelector () {
241+ r .Register (selector , NewEVMReportFormatter )
242+ }
243+
244+ // Solana
245+ for _ , selector := range chainselectors .SolanaChainIdToChainSelector () {
246+ r .Register (selector , NewSolanaReportFormatter )
247+ }
248+
249+ return & r
58250}
59251
60252// NewSecureMintAggregator creates a new SecureMintAggregator instance based on the provided configuration.
@@ -64,8 +256,11 @@ func NewSecureMintAggregator(config values.Map) (types.Aggregator, error) {
64256 if err != nil {
65257 return nil , fmt .Errorf ("failed to parse config (%+v): %w" , config , err )
66258 }
259+ registry := NewDefaultFormatterFactory ()
260+
67261 return & SecureMintAggregator {
68- config : parsedConfig ,
262+ config : parsedConfig ,
263+ registry : registry ,
69264 }, nil
70265}
71266
@@ -172,33 +367,19 @@ func (a *SecureMintAggregator) createOutcome(lggr logger.Logger, report *secureM
172367 lggr = logger .Named (lggr , "SecureMintAggregator" )
173368 lggr .Debugw ("createOutcome called" , "report" , report )
174369
175- // Convert chain selector to bytes for data ID
176- // Secure Mint dataID: 0x04 + chain selector as bytes + right padded with 0s
177- var chainSelectorAsDataID [16 ]byte
178- chainSelectorAsDataID [0 ] = 0x04
179- binary .BigEndian .PutUint64 (chainSelectorAsDataID [1 :], uint64 (a .config .TargetChainSelector ))
370+ reportFormatter , err := a .registry .Get (
371+ uint64 (a .config .TargetChainSelector ),
372+ a .config ,
373+ )
180374
181- smReportAsAnswer , err := packSecureMintReportIntoUint224ForEVM (report .Mintable , report .Block )
182375 if err != nil {
183- return nil , fmt .Errorf ("failed to pack secure mint report for evm into uint224: %w" , err )
376+ return nil , fmt .Errorf ("encountered issue fetching report formatter in createOutcome %w" , err )
184377 }
185- lggr .Debugw ("packed report into answer" , "smReportAsAnswer" , smReportAsAnswer )
186378
187- // This is what the DF Cache contract expects:
188- // abi: "(bytes16 dataId, uint32 timestamp, uint224 answer)[] Reports"
189- toWrap := []any {
190- map [EVMEncoderKey ]any {
191- DataIDOutputFieldName : chainSelectorAsDataID ,
192- AnswerOutputFieldName : smReportAsAnswer ,
193- TimestampOutputFieldName : int64 (report .Block ),
194- },
195- }
379+ wrappedReport , err := reportFormatter .PackReport (lggr , report )
196380
197- wrappedReport , err := values .NewMap (map [string ]any {
198- TopLevelListOutputFieldName : toWrap ,
199- })
200381 if err != nil {
201- return nil , fmt .Errorf ("failed to wrap report: %w" , err )
382+ return nil , fmt .Errorf ("encountered issue generating report in createOutcome %w" , err )
202383 }
203384
204385 reportsProto := values .Proto (wrappedReport )
@@ -220,7 +401,8 @@ func (a *SecureMintAggregator) createOutcome(lggr logger.Logger, report *secureM
220401// parseSecureMintConfig parses the user-facing, type-less, SecureMint aggregator config into the internal typed config.
221402func parseSecureMintConfig (config values.Map ) (SecureMintAggregatorConfig , error ) {
222403 type rawConfig struct {
223- TargetChainSelector string `mapstructure:"targetChainSelector"`
404+ TargetChainSelector string `mapstructure:"targetChainSelector"`
405+ Solana SolanaConfig `mapstructure:"solana"`
224406 }
225407
226408 var rawCfg rawConfig
@@ -239,12 +421,13 @@ func parseSecureMintConfig(config values.Map) (SecureMintAggregatorConfig, error
239421
240422 parsedConfig := SecureMintAggregatorConfig {
241423 TargetChainSelector : chainSelector (sel ),
424+ Solana : rawCfg .Solana ,
242425 }
243426
244427 return parsedConfig , nil
245428}
246429
247- var maxMintable = new (big.Int ).Sub (new (big.Int ).Lsh (big .NewInt (1 ), 128 ), big .NewInt (1 )) // 2^128 - 1
430+ var maxMintableEVM = new (big.Int ).Sub (new (big.Int ).Lsh (big .NewInt (1 ), 128 ), big .NewInt (1 )) // 2^128 - 1
248431
249432// packSecureMintReportIntoUint224ForEVM packs the mintable and block number into a single uint224 so that it can be used as a price in the DF Cache contract
250433// (top 32 - not used / middle 64 - block number / lower 128 - mintable amount)
@@ -255,8 +438,8 @@ func packSecureMintReportIntoUint224ForEVM(mintable *big.Int, blockNumber uint64
255438 }
256439
257440 // Validate that mintable fits in 128 bits
258- if mintable .Cmp (maxMintable ) > 0 {
259- return nil , fmt .Errorf ("mintable amount %v exceeds maximum 128-bit value %v" , mintable , maxMintable )
441+ if mintable .Cmp (maxMintableEVM ) > 0 {
442+ return nil , fmt .Errorf ("mintable amount %v exceeds maximum 128-bit value %v" , mintable , maxMintableEVM )
260443 }
261444
262445 packed := big .NewInt (0 )
@@ -269,3 +452,34 @@ func packSecureMintReportIntoUint224ForEVM(mintable *big.Int, blockNumber uint64
269452
270453 return packed , nil
271454}
455+
456+ var maxMintableSolana = new (big.Int ).Sub (new (big.Int ).Lsh (big .NewInt (1 ), 91 ), big .NewInt (1 )) // 2^91 - 1
457+ var maxBlockNumberSolana uint64 = 1 << 36 - 1 // 2^36 - 1
458+
459+ // TODO: will ripcord be added for top bit?
460+ // (top 1 - not used / middle 36 - block number / lower 91 - mintable amount)
461+ func packSecureMintReportIntoU128ForSolana (mintable * big.Int , blockNumber uint64 ) (* big.Int , error ) {
462+ // Handle nil mintable
463+ if mintable == nil {
464+ return nil , fmt .Errorf ("mintable cannot be nil" )
465+ }
466+
467+ // Validate that mintable fits in 91 bits
468+ if mintable .Cmp (maxMintableSolana ) > 0 {
469+ return nil , fmt .Errorf ("mintable amount %v exceeds maximum 91-bit value %v" , mintable , maxMintableSolana )
470+ }
471+
472+ packed := big .NewInt (0 )
473+ // Put mintable in lower 91 bits
474+ packed .Or (packed , mintable )
475+
476+ if blockNumber > maxBlockNumberSolana {
477+ return nil , fmt .Errorf ("block number %d exceeds maximum 36-bit value %d" , blockNumber , maxBlockNumberSolana )
478+ }
479+
480+ // Put block number in middle 36 bits (bits 91-126)
481+ blockNumberAsBigInt := new (big.Int ).SetUint64 (blockNumber )
482+ packed .Or (packed , new (big.Int ).Lsh (blockNumberAsBigInt , 91 ))
483+
484+ return packed , nil
485+ }
0 commit comments