diff --git a/pkg/capabilities/consensus/ocr3/datafeeds/securemint_aggregator.go b/pkg/capabilities/consensus/ocr3/datafeeds/securemint_aggregator.go index cffdf6ff46..8613595d08 100644 --- a/pkg/capabilities/consensus/ocr3/datafeeds/securemint_aggregator.go +++ b/pkg/capabilities/consensus/ocr3/datafeeds/securemint_aggregator.go @@ -2,12 +2,13 @@ package datafeeds import ( "crypto/sha256" - "encoding/binary" + "encoding/hex" "encoding/json" "errors" "fmt" "math/big" "strconv" + "strings" chainselectors "github.com/smartcontractkit/chain-selectors" ocrcommon "github.com/smartcontractkit/libocr/commontypes" @@ -21,10 +22,6 @@ import ( "github.com/smartcontractkit/chainlink-protos/cre/go/values" ) -var ( - ErrNoMatchingChainSelector = errors.New("no matching chain selector found") -) - type SolanaEncoderKey = string const ( @@ -84,6 +81,7 @@ type SolanaConfig struct { type SecureMintAggregatorConfig struct { // TargetChainSelector is the chain selector to look for TargetChainSelector chainSelector `mapstructure:"targetChainSelector"` + DataID [16]byte `mapstructure:"dataID"` Solana SolanaConfig `mapstructure:"solana"` } @@ -101,25 +99,20 @@ func (c SecureMintAggregatorConfig) ToMap() (*values.Map, error) { var _ types.Aggregator = (*SecureMintAggregator)(nil) type SecureMintAggregator struct { - config SecureMintAggregatorConfig - registry FormatterFactory + config SecureMintAggregatorConfig + formatters *formatterFactory } -type ChainReportFormatter interface { - PackReport(lggr logger.Logger, report *secureMintReport) (*values.Map, error) +type chainReportFormatter interface { + packReport(lggr logger.Logger, report *secureMintReport) (*values.Map, error) } -type EVMReportFormatter struct { - TargetChainSelector uint64 +type evmReportFormatter struct { + targetChainSelector chainSelector + dataID [16]byte } -func (f *EVMReportFormatter) PackReport(lggr logger.Logger, report *secureMintReport) (*values.Map, error) { - // Convert chain selector to bytes for data ID - // Secure Mint dataID: 0x04 + chain selector as bytes + right padded with 0s - var chainSelectorAsDataID [16]byte - chainSelectorAsDataID[0] = 0x04 - binary.BigEndian.PutUint64(chainSelectorAsDataID[1:], uint64(f.TargetChainSelector)) - +func (f *evmReportFormatter) packReport(lggr logger.Logger, report *secureMintReport) (*values.Map, error) { smReportAsAnswer, err := packSecureMintReportIntoUint224ForEVM(report.Mintable, report.Block) if err != nil { return nil, fmt.Errorf("failed to pack secure mint report for evm into uint224: %w", err) @@ -131,9 +124,9 @@ func (f *EVMReportFormatter) PackReport(lggr logger.Logger, report *secureMintRe // abi: "(bytes16 dataId, uint32 timestamp, uint224 answer)[] Reports" toWrap := []any{ map[EVMEncoderKey]any{ - DataIDOutputFieldName: chainSelectorAsDataID, + DataIDOutputFieldName: f.dataID, AnswerOutputFieldName: smReportAsAnswer, - TimestampOutputFieldName: int64(report.Block), + TimestampOutputFieldName: uint32(report.Block), }, } @@ -147,23 +140,17 @@ func (f *EVMReportFormatter) PackReport(lggr logger.Logger, report *secureMintRe return wrappedReport, nil } -func NewEVMReportFormatter(chainSelector uint64, config SecureMintAggregatorConfig) (ChainReportFormatter, error) { - return &EVMReportFormatter{TargetChainSelector: chainSelector}, nil +func newEVMReportFormatter(chainSelector chainSelector, config SecureMintAggregatorConfig) chainReportFormatter { + return &evmReportFormatter{targetChainSelector: chainSelector, dataID: config.DataID} } -type SolanaReportFormatter struct { - TargetChainSelector uint64 - OnReportAccounts solana.AccountMetaSlice +type solanaReportFormatter struct { + targetChainSelector chainSelector + dataID [16]byte + onReportAccounts solana.AccountMetaSlice } -func (f *SolanaReportFormatter) PackReport(lggr logger.Logger, report *secureMintReport) (*values.Map, error) { - // TEMPORARY DATA ID - // Convert chain selector to bytes for data ID - // Secure Mint dataID: 0x04 + chain selector as bytes + right padded with 0s - var chainSelectorAsDataID [16]byte - chainSelectorAsDataID[0] = 0x04 - binary.BigEndian.PutUint64(chainSelectorAsDataID[1:], uint64(f.TargetChainSelector)) - +func (f *solanaReportFormatter) packReport(lggr logger.Logger, report *secureMintReport) (*values.Map, error) { // pack answer smReportAsAnswer, err := packSecureMintReportIntoU128ForSolana(report.Mintable, report.Block) if err != nil { @@ -173,7 +160,7 @@ func (f *SolanaReportFormatter) PackReport(lggr logger.Logger, report *secureMin // hash account contexts var accounts = make([]byte, 0) - for _, acc := range f.OnReportAccounts { + for _, acc := range f.onReportAccounts { accounts = append(accounts, acc.PublicKey[:]...) } accountContextHash := sha256.Sum256(accounts) @@ -187,7 +174,7 @@ func (f *SolanaReportFormatter) PackReport(lggr logger.Logger, report *secureMin map[SolanaEncoderKey]any{ SolTimestampOutputFieldName: uint32(report.Block), // TODO: Verify with Michael/Geert timestamp should be block number? SolAnswerOutputFieldName: smReportAsAnswer, - SolDataIDOutputFieldName: chainSelectorAsDataID, + SolDataIDOutputFieldName: f.dataID, }, } @@ -203,47 +190,46 @@ func (f *SolanaReportFormatter) PackReport(lggr logger.Logger, report *secureMin return wrappedReport, nil } -func NewSolanaReportFormatter(chainSelector uint64, config SecureMintAggregatorConfig) (ChainReportFormatter, error) { - return &SolanaReportFormatter{TargetChainSelector: chainSelector, OnReportAccounts: config.Solana.AccountContext}, nil +func newSolanaReportFormatter(chainSelector chainSelector, config SecureMintAggregatorConfig) chainReportFormatter { + return &solanaReportFormatter{targetChainSelector: chainSelector, onReportAccounts: config.Solana.AccountContext, dataID: config.DataID} } -type Builder func(chainSelector uint64, config SecureMintAggregatorConfig) (ChainReportFormatter, error) - -type FormatterFactory interface { - Register(chainSelector uint64, builder Builder) - Get(chainSelector uint64, config SecureMintAggregatorConfig) (ChainReportFormatter, error) -} +// chainReportFormatterBuilder is a function that returns a chainReportFormatter for a given chain selector and config +type chainReportFormatterBuilder func(chainSelector chainSelector, config SecureMintAggregatorConfig) chainReportFormatter -type DefaultFormatterFactory struct { - builders map[uint64]Builder +type formatterFactory struct { + builders map[chainSelector]chainReportFormatterBuilder } -func (r *DefaultFormatterFactory) Register(chainSelector uint64, builder Builder) { - r.builders[chainSelector] = builder +// register registers a new chain report formatter builder for a given chain selector +func (r *formatterFactory) register(chSel chainSelector, builder chainReportFormatterBuilder) { + r.builders[chSel] = builder } -func (r *DefaultFormatterFactory) Get(chainSelector uint64, config SecureMintAggregatorConfig) (ChainReportFormatter, error) { - b, ok := r.builders[chainSelector] +// get uses a chain report formatter builder to create a chain report formatter +func (r *formatterFactory) get(chSel chainSelector, config SecureMintAggregatorConfig) (chainReportFormatter, error) { + b, ok := r.builders[chSel] if !ok { - return nil, fmt.Errorf("no formatter registered for chain selector: %d", chainSelector) + return nil, fmt.Errorf("no formatter registered for chain selector: %d", chSel) } - return b(chainSelector, config) + return b(chSel, config), nil } -func NewDefaultFormatterFactory() FormatterFactory { - r := DefaultFormatterFactory{ - builders: map[uint64]Builder{}, +// newFormatterFactory collects all chain report formatters per chain family so that they can be used to pack reports for different chains +func newFormatterFactory() *formatterFactory { + r := formatterFactory{ + builders: map[chainSelector]chainReportFormatterBuilder{}, } // EVM for _, selector := range chainselectors.EvmChainIdToChainSelector() { - r.Register(selector, NewEVMReportFormatter) + r.register(chainSelector(selector), newEVMReportFormatter) } // Solana for _, selector := range chainselectors.SolanaChainIdToChainSelector() { - r.Register(selector, NewSolanaReportFormatter) + r.register(chainSelector(selector), newSolanaReportFormatter) } return &r @@ -256,11 +242,11 @@ func NewSecureMintAggregator(config values.Map) (types.Aggregator, error) { if err != nil { return nil, fmt.Errorf("failed to parse config (%+v): %w", config, err) } - registry := NewDefaultFormatterFactory() + registry := newFormatterFactory() return &SecureMintAggregator{ - config: parsedConfig, - registry: registry, + config: parsedConfig, + formatters: registry, }, nil } @@ -315,7 +301,6 @@ func (a *SecureMintAggregator) extractAndValidateReports(lggr logger.Logger, obs for _, observation := range nodeObservations { lggr.Debugw("processing observation", "observation", observation) - lggr.Debugf("processing observation %+v", observation) // Extract OCRTriggerEvent from the observation triggerEvent := &capabilities.OCRTriggerEvent{} @@ -367,16 +352,15 @@ func (a *SecureMintAggregator) createOutcome(lggr logger.Logger, report *secureM lggr = logger.Named(lggr, "SecureMintAggregator") lggr.Debugw("createOutcome called", "report", report) - reportFormatter, err := a.registry.Get( - uint64(a.config.TargetChainSelector), + reportFormatter, err := a.formatters.get( + a.config.TargetChainSelector, a.config, ) - if err != nil { return nil, fmt.Errorf("encountered issue fetching report formatter in createOutcome %w", err) } - wrappedReport, err := reportFormatter.PackReport(lggr, report) + wrappedReport, err := reportFormatter.packReport(lggr, report) if err != nil { return nil, fmt.Errorf("encountered issue generating report in createOutcome %w", err) @@ -402,6 +386,7 @@ func (a *SecureMintAggregator) createOutcome(lggr logger.Logger, report *secureM func parseSecureMintConfig(config values.Map) (SecureMintAggregatorConfig, error) { type rawConfig struct { TargetChainSelector string `mapstructure:"targetChainSelector"` + DataID string `mapstructure:"dataID"` Solana SolanaConfig `mapstructure:"solana"` } @@ -419,8 +404,33 @@ func parseSecureMintConfig(config values.Map) (SecureMintAggregatorConfig, error return SecureMintAggregatorConfig{}, fmt.Errorf("invalid chain selector: %w", err) } + if rawCfg.DataID == "" { + return SecureMintAggregatorConfig{}, errors.New("dataID is required") + } + + // strip 0x prefix if present + dataID := strings.TrimPrefix(rawCfg.DataID, "0x") + + decodedDataID, err := hex.DecodeString(dataID) + if err != nil { + return SecureMintAggregatorConfig{}, fmt.Errorf("invalid dataID: %v %w", dataID, err) + } + + if len(decodedDataID) != 16 { + return SecureMintAggregatorConfig{}, fmt.Errorf("dataID must be 16 bytes, got %d", len(decodedDataID)) + } + + if len(rawCfg.Solana.AccountContext) > 0 { + for _, acc := range rawCfg.Solana.AccountContext { + if acc.PublicKey == [32]byte{} { + return SecureMintAggregatorConfig{}, errors.New("solana account context public key must not be all zeros") + } + } + } + parsedConfig := SecureMintAggregatorConfig{ TargetChainSelector: chainSelector(sel), + DataID: [16]byte(decodedDataID), Solana: rawCfg.Solana, } @@ -434,7 +444,7 @@ var maxMintableEVM = new(big.Int).Sub(new(big.Int).Lsh(big.NewInt(1), 128), big. func packSecureMintReportIntoUint224ForEVM(mintable *big.Int, blockNumber uint64) (*big.Int, error) { // Handle nil mintable if mintable == nil { - return nil, fmt.Errorf("mintable cannot be nil") + return nil, errors.New("mintable cannot be nil") } // Validate that mintable fits in 128 bits @@ -461,7 +471,7 @@ var maxBlockNumberSolana uint64 = 1<<36 - 1 func packSecureMintReportIntoU128ForSolana(mintable *big.Int, blockNumber uint64) (*big.Int, error) { // Handle nil mintable if mintable == nil { - return nil, fmt.Errorf("mintable cannot be nil") + return nil, errors.New("mintable cannot be nil") } // Validate that mintable fits in 91 bits diff --git a/pkg/capabilities/consensus/ocr3/datafeeds/securemint_aggregator_test.go b/pkg/capabilities/consensus/ocr3/datafeeds/securemint_aggregator_test.go index dcbac4754e..d4e25ea223 100644 --- a/pkg/capabilities/consensus/ocr3/datafeeds/securemint_aggregator_test.go +++ b/pkg/capabilities/consensus/ocr3/datafeeds/securemint_aggregator_test.go @@ -2,7 +2,7 @@ package datafeeds import ( "crypto/sha256" - "encoding/binary" + "encoding/hex" "encoding/json" "math/big" "testing" @@ -32,107 +32,82 @@ func TestSecureMintAggregator_Aggregate(t *testing.T) { lggr := logger.Test(t) type tcase struct { - name string - config *values.Map - previousOutcome *types.AggregationOutcome - observations map[ocrcommon.OracleID][]values.Value - f int - expectedShouldReport bool - expectedChainSelector chainSelector - expectError bool - errorContains string - shouldReportAssertFn func(t *testing.T, tc tcase, outcome *types.AggregationOutcome) + name string + chainSelector string + dataID string + solAccounts [][32]byte + previousOutcome *types.AggregationOutcome + observations map[ocrcommon.OracleID][]values.Value + f int + expectedShouldReport bool + expectError bool + errorContains string + shouldReportAssertFn func(t *testing.T, tc tcase, topLevelMap map[string]any) } acc1 := [32]byte{4, 5, 6} acc2 := [32]byte{3, 2, 1} - ethReportAssertFn := func(t *testing.T, tc tcase, outcome *types.AggregationOutcome) { - // Verify the output structure matches the feeds aggregator format - val, err := values.FromMapValueProto(outcome.EncodableOutcome) - require.NoError(t, err) - - topLevelMap, err := val.Unwrap() - require.NoError(t, err) - mm, ok := topLevelMap.(map[string]any) - require.True(t, ok) - + ethReportAssertFn := func(t *testing.T, tc tcase, topLevelMap map[string]any) { // Check that we have the expected reports - reportsList, ok := mm[TopLevelListOutputFieldName].([]any) + reportsList, ok := topLevelMap[TopLevelListOutputFieldName].([]any) require.True(t, ok) - require.Len(t, reportsList, 1) + assert.Len(t, reportsList, 1) // Check the first (and only) report report, ok := reportsList[0].(map[string]any) - require.True(t, ok) + assert.True(t, ok) // Verify dataID dataIDBytes, ok := report[DataIDOutputFieldName].([]byte) - require.True(t, ok) - // Should be 0x04 + chain selector as bytes + right padded with 0s - var expectedChainSelectorBytes [16]byte - expectedChainSelectorBytes[0] = 0x04 - binary.BigEndian.PutUint64(expectedChainSelectorBytes[1:], uint64(tc.expectedChainSelector)) - require.Equal(t, expectedChainSelectorBytes[:], dataIDBytes) - t.Logf("Data ID: 0x%x", dataIDBytes) + assert.True(t, ok, "expected dataID to be []byte but got %T", report[DataIDOutputFieldName]) + assert.Len(t, dataIDBytes, 16) + assert.Equal(t, tc.dataID, "0x"+hex.EncodeToString(dataIDBytes)) // Verify other fields exist answer, ok := report[AnswerOutputFieldName].(*big.Int) - require.True(t, ok) - require.NotNil(t, answer) + assert.True(t, ok) + assert.NotNil(t, answer) timestamp := report[TimestampOutputFieldName].(int64) - require.Equal(t, int64(1000), timestamp) + assert.Equal(t, int64(1000), timestamp) } - solReportAssertFn := func(t *testing.T, tc tcase, outcome *types.AggregationOutcome) { - // Verify the output structure matches the feeds aggregator format - val, err := values.FromMapValueProto(outcome.EncodableOutcome) - require.NoError(t, err) - - topLevelMap, err := val.Unwrap() - require.NoError(t, err) - mm, ok := topLevelMap.(map[string]any) - require.True(t, ok) - + solReportAssertFn := func(t *testing.T, tc tcase, topLevelMap map[string]any) { // Check that we have the expected reports - reportsList, ok := mm[TopLevelPayloadListFieldName].([]any) - require.True(t, ok) - require.Len(t, reportsList, 1) + reportsList, ok := topLevelMap[TopLevelPayloadListFieldName].([]any) + assert.True(t, ok) + assert.Len(t, reportsList, 1) // Check that we have expected account hash - var accHash [32]byte - err = val.Underlying[TopLevelAccountCtxHashFieldName].UnwrapTo(&accHash) - require.NoError(t, err) + accHash, ok := topLevelMap[TopLevelAccountCtxHashFieldName].([]byte) + require.True(t, ok, "expected account hash to be []byte but got %T", topLevelMap[TopLevelAccountCtxHashFieldName]) + require.Len(t, accHash, 32) expHash := sha256.Sum256(append(acc1[:], acc2[:]...)) - - require.Equal(t, expHash, accHash) + assert.Equal(t, expHash, ([32]byte)(accHash)) // Check the first (and only) report report, ok := reportsList[0].(map[string]any) - require.True(t, ok) + assert.True(t, ok) // Verify dataID dataIDBytes, ok := report[SolDataIDOutputFieldName].([]byte) - require.True(t, ok) - // Should be 0x04 + chain selector as bytes + right padded with 0s - var expectedChainSelectorBytes [16]byte - expectedChainSelectorBytes[0] = 0x04 - binary.BigEndian.PutUint64(expectedChainSelectorBytes[1:], uint64(tc.expectedChainSelector)) - require.Equal(t, expectedChainSelectorBytes[:], dataIDBytes) - t.Logf("Data ID: 0x%x", dataIDBytes) + assert.True(t, ok, "expected dataID to be []byte but got %T", report[DataIDOutputFieldName]) + assert.Len(t, dataIDBytes, 16) + assert.Equal(t, tc.dataID, "0x"+hex.EncodeToString(dataIDBytes)) // Verify other fields exist answer, ok := report[SolAnswerOutputFieldName].(*big.Int) - require.True(t, ok) - require.NotNil(t, answer) + assert.True(t, ok) + assert.NotNil(t, answer) timestamp := report[SolTimestampOutputFieldName].(int64) - require.Equal(t, int64(1000), timestamp) + assert.Equal(t, int64(1000), timestamp) } tests := []tcase{ { - name: "successful eth report extraction", - config: configWithChainSelector(t, "16015286601757825753"), + name: "successful eth report extraction", + chainSelector: "16015286601757825753", + dataID: "0x01c508f42b0201320000000000000000", observations: createSecureMintObservations(t, []ocrTriggerEventData{ { chainSelector: ethSepoliaChainSelector, @@ -155,15 +130,15 @@ func TestSecureMintAggregator_Aggregate(t *testing.T) { }, }, }), - f: 1, - expectedShouldReport: true, - expectedChainSelector: ethSepoliaChainSelector, - expectError: false, - shouldReportAssertFn: ethReportAssertFn, + f: 1, + expectedShouldReport: true, + expectError: false, + shouldReportAssertFn: ethReportAssertFn, }, { - name: "no matching chain selector found", - config: configWithChainSelector(t, "16015286601757825753"), + name: "no matching chain selector found", + chainSelector: "16015286601757825753", + dataID: "0x01c508f42b0201320000000000000000", observations: createSecureMintObservations(t, []ocrTriggerEventData{ { chainSelector: bnbTestnetChainSelector, @@ -183,16 +158,18 @@ func TestSecureMintAggregator_Aggregate(t *testing.T) { }, { name: "no observations", - config: configWithChainSelector(t, "16015286601757825753"), + chainSelector: "16015286601757825753", + dataID: "0x01c508f42b0201320000000000000000", observations: map[ocrcommon.OracleID][]values.Value{}, f: 1, expectError: true, errorContains: "no observations", }, { - name: "successful sol report extraction", - config: solConfig(t, "16423721717087811551", // solana devnet - solana.AccountMetaSlice{&solana.AccountMeta{PublicKey: acc1}, &solana.AccountMeta{PublicKey: acc2}}), + name: "successful sol report extraction", + chainSelector: "16423721717087811551", // solana devnet + dataID: "0x01c508f42b0201320000000000000000", + solAccounts: [][32]byte{acc1, acc2}, observations: createSecureMintObservations(t, []ocrTriggerEventData{ { chainSelector: solDevnetChainSelector, @@ -215,18 +192,34 @@ func TestSecureMintAggregator_Aggregate(t *testing.T) { }, }, }), - f: 1, - expectedShouldReport: true, - expectedChainSelector: solDevnetChainSelector, - expectError: false, - shouldReportAssertFn: solReportAssertFn, + f: 1, + expectedShouldReport: true, + expectError: false, + shouldReportAssertFn: solReportAssertFn, }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { // Create aggregator - aggregator, err := NewSecureMintAggregator(*tc.config) + rawCfg := map[string]any{ + "targetChainSelector": tc.chainSelector, + "dataID": tc.dataID, + } + if len(tc.solAccounts) > 0 { + accountMetaSlice := make(solana.AccountMetaSlice, len(tc.solAccounts)) + for i, acc := range tc.solAccounts { + accountMetaSlice[i] = &solana.AccountMeta{PublicKey: acc} + } + + rawCfg["solana"] = map[string]any{ + "remaining_accounts": accountMetaSlice, + } + } + + configMap, err := values.WrapMap(rawCfg) + require.NoError(t, err) + aggregator, err := NewSecureMintAggregator(*configMap) require.NoError(t, err) // Run aggregation @@ -234,95 +227,148 @@ func TestSecureMintAggregator_Aggregate(t *testing.T) { // Check error expectations if tc.expectError { - require.Error(t, err) + assert.Error(t, err) if tc.errorContains != "" { - require.Contains(t, err.Error(), tc.errorContains) + assert.Contains(t, err.Error(), tc.errorContains) } return } require.NoError(t, err) - require.Equal(t, tc.expectedShouldReport, outcome.ShouldReport) + assert.Equal(t, tc.expectedShouldReport, outcome.ShouldReport) if outcome.ShouldReport { - tc.shouldReportAssertFn(t, tc, outcome) + // Verify the output structure matches the feeds aggregator format + val, err := values.FromMapValueProto(outcome.EncodableOutcome) + require.NoError(t, err) + + topLevelMap, err := val.Unwrap() + require.NoError(t, err) + mm, ok := topLevelMap.(map[string]any) + require.True(t, ok) + + tc.shouldReportAssertFn(t, tc, mm) } }) } } -func configWithChainSelector(t *testing.T, chainSelector string) *values.Map { - m, err := values.NewMap(map[string]any{ - "targetChainSelector": chainSelector, - }) - require.NoError(t, err) - return m -} - -func solConfig(t *testing.T, chainSelector string, meta solana.AccountMetaSlice) *values.Map { - m, err := values.NewMap(map[string]any{ - "targetChainSelector": chainSelector, - "solana": map[string]any{ - "remaining_accounts": meta, - }, - }) - - require.NoError(t, err) - return m -} - func TestSecureMintAggregatorConfig_Validation(t *testing.T) { + acc1 := [32]byte{4, 5, 6} + tests := []struct { - name string - chainSelector string - expected chainSelector - expectError bool - errorMsg string + name string + chainSelector string + dataID string + solanaAccounts solana.AccountMetaSlice + expectedChainSelector chainSelector + expectedDataID [16]byte + expectError bool + errorMsg string }{ { - name: "valid chain selector", - chainSelector: "1", - expected: 1, - expectError: false, + name: "valid chain selector, dataID and solana accounts", + chainSelector: "1", + dataID: "0x01c508f42b0201320000000000000000", + solanaAccounts: solana.AccountMetaSlice{&solana.AccountMeta{PublicKey: acc1, IsWritable: true, IsSigner: false}}, + expectedChainSelector: 1, + expectedDataID: [16]byte{0x01, 0xc5, 0x08, 0xf4, 0x2b, 0x02, 0x01, 0x32, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, + expectError: false, }, + { + name: "large chain selector", + chainSelector: "16015286601757825753", // ethereum-testnet-sepolia + dataID: "0x01c508f42b0201320000000000000000", + expectedChainSelector: 16015286601757825753, + expectedDataID: [16]byte{0x01, 0xc5, 0x08, 0xf4, 0x2b, 0x02, 0x01, 0x32, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, + expectError: false, + }, + { + name: "dataID without 0x prefix", + chainSelector: "1", + dataID: "01c508f42b0201320000000000000000", + expectedChainSelector: 1, + expectedDataID: [16]byte{0x01, 0xc5, 0x08, 0xf4, 0x2b, 0x02, 0x01, 0x32, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, + expectError: false}, { name: "invalid chain selector", chainSelector: "invalid", expectError: true, errorMsg: "invalid chain selector", }, - { - name: "large chain selector", - chainSelector: "16015286601757825753", // ethereum-testnet-sepolia - expected: 16015286601757825753, - expectError: false, - }, { name: "negative chain selector", chainSelector: "-1", + dataID: "0x01c508f42b0201320000000000000000", expectError: true, errorMsg: "invalid chain selector", }, + { + name: "invalid dataID", + chainSelector: "1", + dataID: "invalid_data_id", + expectError: true, + errorMsg: "invalid dataID", + }, + { + name: "dataID too short", + chainSelector: "1", + dataID: "0x0000", + expectError: true, + errorMsg: "dataID must be 16 bytes", + }, + { + name: "dataID with odd length", + chainSelector: "1", + dataID: "0x0", + expectError: true, + errorMsg: "odd length hex string", + }, + { + name: "dataID too long", + chainSelector: "1", + dataID: "0x01111111111111111111111111111111111111111111", + expectError: true, + errorMsg: "dataID must be 16 bytes", + }, + { + name: "solana account context with invalid public key", + chainSelector: "1", + dataID: "0x01c508f42b0201320000000000000000", + solanaAccounts: solana.AccountMetaSlice{&solana.AccountMeta{PublicKey: [32]byte{}}}, + expectError: true, + errorMsg: "solana account context public key must not be all zeros", + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - configMap, err := values.WrapMap(map[string]any{ + rawCfg := map[string]any{ "targetChainSelector": tt.chainSelector, - }) + "dataID": tt.dataID, + } + if len(tt.solanaAccounts) > 0 { + rawCfg["solana"] = map[string]any{ + "remaining_accounts": tt.solanaAccounts, + } + } + + configMap, err := values.WrapMap(rawCfg) require.NoError(t, err) aggregator, err := NewSecureMintAggregator(*configMap) if tt.expectError { - require.Error(t, err) + assert.Error(t, err) if tt.errorMsg != "" { - require.Contains(t, err.Error(), tt.errorMsg) + assert.Contains(t, err.Error(), tt.errorMsg) } return } require.NoError(t, err) - assert.Equal(t, tt.expected, aggregator.(*SecureMintAggregator).config.TargetChainSelector) + assert.Equal(t, tt.expectedChainSelector, aggregator.(*SecureMintAggregator).config.TargetChainSelector) + assert.Equal(t, tt.expectedDataID, aggregator.(*SecureMintAggregator).config.DataID) + assert.Equal(t, tt.solanaAccounts, aggregator.(*SecureMintAggregator).config.Solana.AccountContext) }) } }