From 9ece21b824a824c7e9105b1dadd5f34bd83f877e Mon Sep 17 00:00:00 2001 From: Awbrey Hughlett Date: Wed, 19 Feb 2025 18:05:27 -0500 Subject: [PATCH 1/5] Chainreader/writer bug fixes (#1088) * use string value for address and update writer to use chain specific name * Add missing call to addReadNameForContract() * change event sig type, assigning subkey values to idx, remove unused code * further bug fixes to enable event querying --------- Co-authored-by: Domino Valdano <2644901+reductionista@users.noreply.github.com> --- pkg/solana/chainreader/chain_reader.go | 1 + pkg/solana/chainreader/event_read_binding.go | 6 +- pkg/solana/codec/parsed_types_test.go | 90 ++++++++++++++++++++ pkg/solana/codec/solana.go | 2 +- pkg/solana/logpoller/loader.go | 9 -- pkg/solana/logpoller/log_poller.go | 12 ++- pkg/solana/logpoller/parser.go | 10 +-- pkg/solana/logpoller/parser_test.go | 6 +- 8 files changed, 112 insertions(+), 24 deletions(-) create mode 100644 pkg/solana/codec/parsed_types_test.go diff --git a/pkg/solana/chainreader/chain_reader.go b/pkg/solana/chainreader/chain_reader.go index 28a43622d..bd59d4b45 100644 --- a/pkg/solana/chainreader/chain_reader.go +++ b/pkg/solana/chainreader/chain_reader.go @@ -628,6 +628,7 @@ func (s *ContractReaderService) addEventRead( reader.SetFilter(toLPFilter(readDefinition.ChainSpecificName, pf, subkeys.subKeys[:], eventDef)) s.bdRegistry.AddReader(namespace, genericName, reader) + s.lookup.addReadNameForContract(namespace, genericName, []read{{readName: genericName, useParams: false}}) return nil } diff --git a/pkg/solana/chainreader/event_read_binding.go b/pkg/solana/chainreader/event_read_binding.go index fb37d078e..e2e105283 100644 --- a/pkg/solana/chainreader/event_read_binding.go +++ b/pkg/solana/chainreader/event_read_binding.go @@ -33,7 +33,7 @@ type eventReadBinding struct { // static data namespace, genericName string - eventSig [logpoller.EventSignatureLength]byte + eventSig logpoller.EventSignature indexedSubKeys *indexedSubkeys readDefinition config.ReadDefinition @@ -210,7 +210,7 @@ func (b *eventReadBinding) GetLatestValue(ctx context.Context, params, returnVal allFilters := []query.Expression{ logpoller.NewAddressFilter(pubKey), - logpoller.NewEventSigFilter(b.eventSig[:]), + logpoller.NewEventSigFilter(b.eventSig), } if len(subkeyFilters) > 0 { @@ -258,7 +258,7 @@ func (b *eventReadBinding) QueryKey( // filter should always use the address and event sig filter.Expressions = append([]query.Expression{ logpoller.NewAddressFilter(pubKey), - logpoller.NewEventSigFilter(b.eventSig[:]), + logpoller.NewEventSigFilter(b.eventSig), }, filter.Expressions...) itemType := strings.Join([]string{b.namespace, b.genericName}, ".") diff --git a/pkg/solana/codec/parsed_types_test.go b/pkg/solana/codec/parsed_types_test.go new file mode 100644 index 000000000..32ad46b4c --- /dev/null +++ b/pkg/solana/codec/parsed_types_test.go @@ -0,0 +1,90 @@ +package codec_test + +import ( + "encoding/json" + "fmt" + "math/big" + "testing" + + "github.com/stretchr/testify/require" + + commoncodec "github.com/smartcontractkit/chainlink-common/pkg/codec" + "github.com/smartcontractkit/chainlink-common/pkg/utils/tests" + + "github.com/smartcontractkit/chainlink-solana/pkg/solana/codec" +) + +func TestEncodeDecodeBigInt(t *testing.T) { + t.Parallel() + + type offChain struct { + A *big.Int + B *big.Int + } + + ctx := tests.Context(t) + types := newTestCodec(t) + typedCodec, err := types.ToCodec() + + require.NoError(t, err) + + value := offChain{ + A: big.NewInt(42), + B: big.NewInt(42), + } + + bts, err := typedCodec.Encode(ctx, &value, codec.WrapItemType(true, namespace, genericName)) + + require.NoError(t, err) + + var output offChain + + require.NoError(t, typedCodec.Decode(ctx, bts, &output, codec.WrapItemType(false, namespace, genericName))) + require.Equal(t, value.A.String(), output.A.String()) + require.Equal(t, value.B.String(), output.B.String()) +} + +func newTestCodec(t *testing.T) *codec.ParsedTypes { + t.Helper() + + rawIDL := fmt.Sprintf(basicEventIDL, testParamType) + + var IDL codec.IDL + require.NoError(t, json.Unmarshal([]byte(rawIDL), &IDL)) + + idlDef, err := codec.FindDefinitionFromIDL(codec.ChainConfigTypeEventDef, "EventType", IDL) + + require.NoError(t, err) + + mods := commoncodec.MultiModifier{ + commoncodec.NewRenamer(map[string]string{"X": "A", "Y": "B"}), + } + + entry, err := codec.CreateCodecEntry(idlDef, "GenericName", IDL, mods) + + require.NoError(t, err) + + return &codec.ParsedTypes{ + EncoderDefs: map[string]codec.Entry{codec.WrapItemType(true, namespace, genericName): entry}, + DecoderDefs: map[string]codec.Entry{codec.WrapItemType(false, namespace, genericName): entry}, + } +} + +const ( + namespace = "TestNamespace" + genericName = "GenericName" + + basicEventIDL = `{ + "version": "0.1.0", + "name": "some_test_idl", + "events": [%s] + }` + + testParamType = `{ + "name": "EventType", + "fields": [ + {"name": "x", "type": "i128"}, + {"name": "y", "type": "u128"} + ] + }` +) diff --git a/pkg/solana/codec/solana.go b/pkg/solana/codec/solana.go index b16e66c44..10cdeea9a 100644 --- a/pkg/solana/codec/solana.go +++ b/pkg/solana/codec/solana.go @@ -430,7 +430,7 @@ func getUIntCodecByStringType(curType IdlTypeAsString, builder commonencodings.B case IdlTypeU64: return builder.Uint64(), nil case IdlTypeU128: - return builder.BigInt(16, true) + return builder.BigInt(16, false) default: return nil, fmt.Errorf(unknownIDLFormat, commontypes.ErrInvalidConfig, curType) } diff --git a/pkg/solana/logpoller/loader.go b/pkg/solana/logpoller/loader.go index 8aa592773..b436c3f1f 100644 --- a/pkg/solana/logpoller/loader.go +++ b/pkg/solana/logpoller/loader.go @@ -21,15 +21,6 @@ type Block struct { Events []ProgramEvent } -type ProgramEventProcessor interface { - // Process should take a ProgramEvent and parseProgramLogs it based on log signature - // and expected encoding. Only return errors that cannot be handled and - // should exit further transaction processing on the running thread. - // - // Process should be thread safe. - Process(Block) error -} - type RPCClient interface { GetBlockWithOpts(context.Context, uint64, *rpc.GetBlockOpts) (*rpc.GetBlockResult, error) GetSignaturesForAddressWithOpts(context.Context, solana.PublicKey, *rpc.GetSignaturesForAddressOpts) ([]*rpc.TransactionSignature, error) diff --git a/pkg/solana/logpoller/log_poller.go b/pkg/solana/logpoller/log_poller.go index 256f51096..bc626150f 100644 --- a/pkg/solana/logpoller/log_poller.go +++ b/pkg/solana/logpoller/log_poller.go @@ -175,17 +175,23 @@ func (lp *Service) Process(ctx context.Context, programEvent ProgramEvent) (err return err } - log.SubkeyValues = make([]IndexedValue, 0, len(filter.SubkeyPaths)) - for _, path := range filter.SubkeyPaths { + log.SubkeyValues = make([]IndexedValue, len(filter.SubkeyPaths)) + for idx, path := range filter.SubkeyPaths { + if len(path) == 0 { + continue + } + subKeyVal, decodeSubKeyErr := lp.filters.DecodeSubKey(ctx, lp.lggr, log.Data, filter.ID, path) if decodeSubKeyErr != nil { return decodeSubKeyErr } + indexedVal, newIndexedValErr := newIndexedValue(subKeyVal) if newIndexedValErr != nil { return newIndexedValErr } - log.SubkeyValues = append(log.SubkeyValues, indexedVal) + + log.SubkeyValues[idx] = indexedVal } log.SequenceNum = lp.filters.IncrementSeqNum(filter.ID) diff --git a/pkg/solana/logpoller/parser.go b/pkg/solana/logpoller/parser.go index 8c330692e..6fdc6bfa6 100644 --- a/pkg/solana/logpoller/parser.go +++ b/pkg/solana/logpoller/parser.go @@ -21,7 +21,7 @@ const ( txHashFieldName = "tx_hash" addressFieldName = "address" eventSigFieldName = "event_sig" - defaultSort = "block_number ASC, log_index ASC" + defaultSort = "block_number DESC, log_index DESC" subKeyValuesFieldName = "subkey_values" subKeyValueArg = "subkey_value" subKeyIndexArgName = "subkey_index" @@ -412,12 +412,12 @@ func orderToString(dir query.SortDirection) (string, error) { } type addressFilter struct { - address solana.PublicKey + address PublicKey } func NewAddressFilter(address solana.PublicKey) query.Expression { return query.Expression{ - Primitive: &addressFilter{address: address}, + Primitive: &addressFilter{address: PublicKey(address)}, } } @@ -429,10 +429,10 @@ func (f *addressFilter) Accept(visitor primitives.Visitor) { } type eventSigFilter struct { - eventSig []byte + eventSig EventSignature } -func NewEventSigFilter(sig []byte) query.Expression { +func NewEventSigFilter(sig EventSignature) query.Expression { return query.Expression{ Primitive: &eventSigFilter{eventSig: sig}, } diff --git a/pkg/solana/logpoller/parser_test.go b/pkg/solana/logpoller/parser_test.go index e9e444161..6c66c0dd9 100644 --- a/pkg/solana/logpoller/parser_test.go +++ b/pkg/solana/logpoller/parser_test.go @@ -60,7 +60,7 @@ func TestDSLParser(t *testing.T) { parser := &pgDSLParser{} expressions := []query.Expression{ NewAddressFilter(pk), - NewEventSigFilter([]byte("test")), + NewEventSigFilter(NewEventSignatureFromName("TestEvent")), subkey, query.Confidence(primitives.Unconfirmed), } @@ -98,7 +98,7 @@ func TestDSLParser(t *testing.T) { parser := &pgDSLParser{} expressions := []query.Expression{ NewAddressFilter(pk), - NewEventSigFilter([]byte("test")), + NewEventSigFilter(NewEventSignatureFromName("TestEvent")), subkey, } limiter := query.NewLimitAndSort(query.CountLimit(20)) @@ -297,7 +297,7 @@ func TestDSLParser(t *testing.T) { t.Parallel() parser := &pgDSLParser{} - sigFilter := NewEventSigFilter([]byte("test")) + sigFilter := NewEventSigFilter(NewEventSignatureFromName("TestEvent")) limiter := query.LimitAndSort{} expressions := []query.Expression{ From 4b4951a208ec6a941534377f4f74f0b13bd8545e Mon Sep 17 00:00:00 2001 From: amit-momin <108959691+amit-momin@users.noreply.github.com> Date: Wed, 19 Feb 2025 18:03:51 -0600 Subject: [PATCH 2/5] Updated DataAvailabilityFee to return 0 instead of nil (#1081) * Updated DataAvailabilityFee to return 0 instead of nil * Fixed debug log format * Updated logs --------- Co-authored-by: Silas Lenihan Co-authored-by: Silas Lenihan <32529249+silaslenihan@users.noreply.github.com> --- pkg/solana/chainwriter/chain_writer.go | 18 +++++++++--------- pkg/solana/chainwriter/chain_writer_test.go | 4 ++-- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/pkg/solana/chainwriter/chain_writer.go b/pkg/solana/chainwriter/chain_writer.go index 169824cf0..0931406ba 100644 --- a/pkg/solana/chainwriter/chain_writer.go +++ b/pkg/solana/chainwriter/chain_writer.go @@ -281,7 +281,7 @@ func CreateATAs(ctx context.Context, args any, lookups []ATALookup, derivedTable Commitment: rpc.CommitmentFinalized, }) if err == nil { - logger.Info("ATA already exists, skipping creation.", lookup.Location) + logger.Infow("ATA already exists, skipping creation.", "location", lookup.Location) continue } if !strings.Contains(err.Error(), "not found") { @@ -348,7 +348,7 @@ func (s *SolanaChainWriterService) SubmitTransaction(ctx context.Context, contra return errorWithDebugID(fmt.Errorf("error getting lookup tables: %w", err), debugID) } - s.lggr.Debug("Resolving account addresses", "contract", contractName, "method", method) + s.lggr.Debugw("Resolving account addresses", "contract", contractName, "method", method) // Resolve account metas accounts, err := GetAddresses(ctx, args, methodConfig.Accounts, derivedTableMap, s.client) if err != nil { @@ -360,13 +360,13 @@ func (s *SolanaChainWriterService) SubmitTransaction(ctx context.Context, contra return errorWithDebugID(fmt.Errorf("error parsing fee payer address: %w", err), debugID) } - s.lggr.Debug("Creating ATAs", "contract", contractName, "method", method) + s.lggr.Debugw("Creating ATAs", "contract", contractName, "method", method) createATAinstructions, err := CreateATAs(ctx, args, methodConfig.ATAs, derivedTableMap, s.client, programConfig.IDL, feePayer, s.lggr) if err != nil { return errorWithDebugID(fmt.Errorf("error resolving account addresses: %w", err), debugID) } - s.lggr.Debug("Filtering lookup table addresses", "contract", contractName, "method", method) + s.lggr.Debugw("Filtering lookup table addresses", "contract", contractName, "method", method) // Filter the lookup table addresses based on which accounts are actually used filteredLookupTableMap := s.FilterLookupTableAddresses(accounts, derivedTableMap, staticTableMap) @@ -376,7 +376,7 @@ func (s *SolanaChainWriterService) SubmitTransaction(ctx context.Context, contra if tfErr != nil { return errorWithDebugID(fmt.Errorf("error finding transform function: %w", tfErr), debugID) } - s.lggr.Debug("Applying args transformation", "contract", contractName, "method", method) + s.lggr.Debugw("Applying args transformation", "contract", contractName, "method", method) args, err = transformFunc(ctx, s, args, accounts, toAddress) if err != nil { return errorWithDebugID(fmt.Errorf("error transforming args: %w", err), debugID) @@ -389,7 +389,7 @@ func (s *SolanaChainWriterService) SubmitTransaction(ctx context.Context, contra return errorWithDebugID(fmt.Errorf("error parsing program ID: %w", err), debugID) } - s.lggr.Debug("Encoding transaction payload", "contract", contractName, "method", method) + s.lggr.Debugw("Encoding transaction payload", "contract", contractName, "method", method) encodedPayload, err := s.encoder.Encode(ctx, args, codec.WrapItemType(true, contractName, method)) if err != nil { @@ -422,7 +422,7 @@ func (s *SolanaChainWriterService) SubmitTransaction(ctx context.Context, contra return errorWithDebugID(fmt.Errorf("error constructing transaction: %w", err), debugID) } - s.lggr.Debug("Sending main transaction", "contract", contractName, "method", method) + s.lggr.Debugw("Sending main transaction", "contract", contractName, "method", method) // Enqueue transaction if err = s.txm.Enqueue(ctx, methodConfig.FromAddress, tx, &transactionID, blockhash.Value.LastValidBlockHeight); err != nil { return errorWithDebugID(fmt.Errorf("error enqueuing transaction: %w", err), debugID) @@ -433,7 +433,7 @@ func (s *SolanaChainWriterService) SubmitTransaction(ctx context.Context, contra // GetTransactionStatus returns the current status of a transaction in the underlying chain's TXM. func (s *SolanaChainWriterService) GetTransactionStatus(ctx context.Context, transactionID string) (types.TransactionStatus, error) { - s.lggr.Debug("Fetching transaction status", "transactionID", transactionID) + s.lggr.Debugw("Fetching transaction status", "transactionID", transactionID) return s.txm.GetTransactionStatus(ctx, transactionID) } @@ -447,7 +447,7 @@ func (s *SolanaChainWriterService) GetFeeComponents(ctx context.Context) (*types fee := s.ge.BaseComputeUnitPrice() return &types.ChainFeeComponents{ ExecutionFee: new(big.Int).SetUint64(fee), - DataAvailabilityFee: nil, + DataAvailabilityFee: big.NewInt(0), // required field so return 0 instead of nil }, nil } diff --git a/pkg/solana/chainwriter/chain_writer_test.go b/pkg/solana/chainwriter/chain_writer_test.go index 17732416e..78ef1754d 100644 --- a/pkg/solana/chainwriter/chain_writer_test.go +++ b/pkg/solana/chainwriter/chain_writer_test.go @@ -1055,11 +1055,11 @@ func TestChainWriter_GetFeeComponents(t *testing.T) { cw, err := chainwriter.NewSolanaChainWriterService(testutils.NewNullLogger(), mc, txm, ge, chainwriter.ChainWriterConfig{}) require.NoError(t, err) - t.Run("returns valid compute unit price", func(t *testing.T) { + t.Run("returns valid compute unit price and non-nil data availability fee", func(t *testing.T) { feeComponents, err := cw.GetFeeComponents(ctx) require.NoError(t, err) require.Equal(t, big.NewInt(100), feeComponents.ExecutionFee) - require.Nil(t, feeComponents.DataAvailabilityFee) // always nil for Solana + require.Equal(t, big.NewInt(0), feeComponents.DataAvailabilityFee) // always nil for Solana }) t.Run("fails if gas estimator not set", func(t *testing.T) { From 8953ce7853821bcbe14eff738c228361c7647dd9 Mon Sep 17 00:00:00 2001 From: ilija42 <57732589+ilija42@users.noreply.github.com> Date: Thu, 20 Feb 2025 03:06:01 +0100 Subject: [PATCH 3/5] Change account reading err handling and fix get token prices for a non loop call #1079 (#1078) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Change CR handling for Addresses not found * lint * Improve account reading err handling and fix get token prices for a non loop call (#1079) * Handle get token prices for map params * Fix getPDAsForGetTokenPrices handling for non Loop call * Fix getPDAsForGetTokenPrices handling for non Loop call * Tidy * lint * fix cr test * Refactor fix, don't discard account data from accounts we were able to read * Implement ErrOnMissingAccountData flag for CR calls * cleanup * fix merge issues --------- Co-authored-by: Blaž Hrastnik --- go.mod | 2 +- .../relayinterface/chain_components_test.go | 217 ++++++++++++------ pkg/solana/chainreader/batch.go | 32 +-- pkg/solana/chainreader/chain_reader.go | 115 +++++++--- pkg/solana/chainreader/chain_reader_test.go | 8 +- pkg/solana/chainreader/client_wrapper.go | 14 +- pkg/solana/chainreader/lookup.go | 2 +- pkg/solana/config/chain_reader.go | 15 +- 8 files changed, 272 insertions(+), 133 deletions(-) diff --git a/go.mod b/go.mod index 539941f03..56dfcb2b5 100644 --- a/go.mod +++ b/go.mod @@ -15,6 +15,7 @@ require ( github.com/hashicorp/go-plugin v1.6.2 github.com/jackc/pgx/v4 v4.18.3 github.com/lib/pq v1.10.9 + github.com/mitchellh/mapstructure v1.5.0 github.com/pelletier/go-toml/v2 v2.2.3 github.com/prometheus/client_golang v1.20.5 github.com/smartcontractkit/chainlink-ccip v0.0.0-20250203132120-f0d42463e405 @@ -99,7 +100,6 @@ require ( github.com/mattn/go-sqlite3 v2.0.3+incompatible // indirect github.com/miekg/dns v1.1.61 // indirect github.com/mitchellh/go-testing-interface v1.14.1 // indirect - github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/moby/sys/sequential v0.6.0 // indirect github.com/moby/sys/user v0.3.0 // indirect github.com/moby/sys/userns v0.1.0 // indirect diff --git a/integration-tests/relayinterface/chain_components_test.go b/integration-tests/relayinterface/chain_components_test.go index e4d1c9ef6..c00371db4 100644 --- a/integration-tests/relayinterface/chain_components_test.go +++ b/integration-tests/relayinterface/chain_components_test.go @@ -113,79 +113,113 @@ func DisableTests(it *SolanaChainComponentsInterfaceTester[*testing.T]) { } func RunChainComponentsSolanaTests[T WrappedTestingT[T]](t T, it *SolanaChainComponentsInterfaceTester[T]) { - testCases := Testcase[T]{ - Name: "Test address groups where first namespace shares address with second namespace", - Test: func(t T) { - ctx := tests.Context(t) - cfg := it.buildContractReaderConfig(t) - cfg.AddressShareGroups = [][]string{{AnyContractNameWithSharedAddress1, AnyContractNameWithSharedAddress2, AnyContractNameWithSharedAddress3}} - cr := it.GetContractReaderWithCustomCfg(t, cfg) - - t.Run("Namespace is part of an address share group that doesn't have a registered address and provides no address during Bind", func(t T) { - bound1 := []types.BoundContract{{ - Name: AnyContractNameWithSharedAddress1, - }} - require.Error(t, cr.Bind(ctx, bound1)) - }) - - addressToBeShared := it.Helper.CreateAccount(t, *it, AnyContractName, AnyValueToReadWithoutAnArgument, CreateTestStruct(0, it)).String() - t.Run("Namespace is part of an address share group that doesn't have a registered address and provides an address during Bind", func(t T) { - bound1 := []types.BoundContract{{Name: AnyContractNameWithSharedAddress1, Address: addressToBeShared}} - - require.NoError(t, cr.Bind(ctx, bound1)) + testCases := []Testcase[T]{ + { + Name: "Test address groups where first namespace shares address with second namespace", + Test: func(t T) { + ctx := tests.Context(t) + cfg := it.buildContractReaderConfig(t) + cfg.AddressShareGroups = [][]string{{AnyContractNameWithSharedAddress1, AnyContractNameWithSharedAddress2, AnyContractNameWithSharedAddress3}} + cr := it.GetContractReaderWithCustomCfg(t, cfg) + + t.Run("Namespace is part of an address share group that doesn't have a registered address and provides no address during Bind", func(t T) { + bound1 := []types.BoundContract{{ + Name: AnyContractNameWithSharedAddress1, + }} + require.Error(t, cr.Bind(ctx, bound1)) + }) + + addressToBeShared := it.Helper.CreateAccount(t, *it, AnyContractName, AnyValueToReadWithoutAnArgument, CreateTestStruct(0, it)).String() + t.Run("Namespace is part of an address share group that doesn't have a registered address and provides an address during Bind", func(t T) { + bound1 := []types.BoundContract{{Name: AnyContractNameWithSharedAddress1, Address: addressToBeShared}} + + require.NoError(t, cr.Bind(ctx, bound1)) + + var prim uint64 + require.NoError(t, cr.GetLatestValue(ctx, bound1[0].ReadIdentifier(MethodReturningUint64), primitives.Unconfirmed, nil, &prim)) + assert.Equal(t, AnyValueToReadWithoutAnArgument, prim) + }) + + t.Run("Namespace is part of an address share group that has a registered address and provides that same address during Bind", func(t T) { + bound2 := []types.BoundContract{{ + Name: AnyContractNameWithSharedAddress2, + Address: addressToBeShared}} + require.NoError(t, cr.Bind(ctx, bound2)) + + var prim uint64 + require.NoError(t, cr.GetLatestValue(ctx, bound2[0].ReadIdentifier(MethodReturningUint64), primitives.Unconfirmed, nil, &prim)) + assert.Equal(t, AnyValueToReadWithoutAnArgument, prim) + assert.Equal(t, addressToBeShared, bound2[0].Address) + }) + + t.Run("Namespace is part of an address share group that has a registered address and provides a wrong address during Bind", func(t T) { + key, err := solana.NewRandomPrivateKey() + require.NoError(t, err) + + bound2 := []types.BoundContract{{ + Name: AnyContractNameWithSharedAddress2, + Address: key.PublicKey().String()}} + require.Error(t, cr.Bind(ctx, bound2)) + }) + + t.Run("Namespace is part of an address share group that has a registered address and provides no address during Bind", func(t T) { + bound3 := []types.BoundContract{{Name: AnyContractNameWithSharedAddress3}} + require.NoError(t, cr.Bind(ctx, bound3)) + + var prim uint64 + require.NoError(t, cr.GetLatestValue(ctx, bound3[0].ReadIdentifier(MethodReturningUint64), primitives.Unconfirmed, nil, &prim)) + assert.Equal(t, AnyValueToReadWithoutAnArgument, prim) + assert.Equal(t, addressToBeShared, bound3[0].Address) + + // when run in a loop Bind address won't be set, so check if CR Method works without set address. + prim = 0 + require.NoError(t, cr.GetLatestValue(ctx, types.BoundContract{ + Address: "", + Name: AnyContractNameWithSharedAddress3, + }.ReadIdentifier(MethodReturningUint64), primitives.Unconfirmed, nil, &prim)) + assert.Equal(t, AnyValueToReadWithoutAnArgument, prim) + }) + + t.Run("Namespace is not part of an address share group that has a registered address and provides no address during Bind", func(t T) { + require.Error(t, cr.Bind(ctx, []types.BoundContract{{Name: AnyContractName}})) + }) + }, + }, - var prim uint64 - require.NoError(t, cr.GetLatestValue(ctx, bound1[0].ReadIdentifier(MethodReturningUint64), primitives.Unconfirmed, nil, &prim)) - assert.Equal(t, AnyValueToReadWithoutAnArgument, prim) - }) + {Name: ContractReaderGetLatestValueGetTokenPrices, + Test: func(t T) { + cr := it.GetContractReader(t) + bindings := it.GetBindings(t) + ctx := tests.Context(t) - t.Run("Namespace is part of an address share group that has a registered address and provides that same address during Bind", func(t T) { - bound2 := []types.BoundContract{{ - Name: AnyContractNameWithSharedAddress2, - Address: addressToBeShared}} - require.NoError(t, cr.Bind(ctx, bound2)) + bound := BindingsByName(bindings, AnyContractName)[0] - var prim uint64 - require.NoError(t, cr.GetLatestValue(ctx, bound2[0].ReadIdentifier(MethodReturningUint64), primitives.Unconfirmed, nil, &prim)) - assert.Equal(t, AnyValueToReadWithoutAnArgument, prim) - assert.Equal(t, addressToBeShared, bound2[0].Address) - }) + require.NoError(t, cr.Bind(ctx, bindings)) - t.Run("Namespace is part of an address share group that has a registered address and provides a wrong address during Bind", func(t T) { - key, err := solana.NewRandomPrivateKey() - require.NoError(t, err) + type TimestampedUnixBig struct { + Value *big.Int `json:"value"` + Timestamp uint32 `json:"timestamp"` + } - bound2 := []types.BoundContract{{ - Name: AnyContractNameWithSharedAddress2, - Address: key.PublicKey().String()}} - require.Error(t, cr.Bind(ctx, bound2)) - }) + res := make([]TimestampedUnixBig, 2) - t.Run("Namespace is part of an address share group that has a registered address and provides no address during Bind", func(t T) { - bound3 := []types.BoundContract{{Name: AnyContractNameWithSharedAddress3}} - require.NoError(t, cr.Bind(ctx, bound3)) + byteTokens := make([][]byte, 0, 2) + pubKey1, err := solana.PublicKeyFromBase58(GetTokenPricesPubKey1) + require.NoError(t, err) + pubKey2, err := solana.PublicKeyFromBase58(GetTokenPricesPubKey2) + require.NoError(t, err) - var prim uint64 - require.NoError(t, cr.GetLatestValue(ctx, bound3[0].ReadIdentifier(MethodReturningUint64), primitives.Unconfirmed, nil, &prim)) - assert.Equal(t, AnyValueToReadWithoutAnArgument, prim) - assert.Equal(t, addressToBeShared, bound3[0].Address) - - // when run in a loop Bind address won't be set, so check if CR Method works without set address. - prim = 0 - require.NoError(t, cr.GetLatestValue(ctx, types.BoundContract{ - Address: "", - Name: AnyContractNameWithSharedAddress3, - }.ReadIdentifier(MethodReturningUint64), primitives.Unconfirmed, nil, &prim)) - assert.Equal(t, AnyValueToReadWithoutAnArgument, prim) - }) - - t.Run("Namespace is not part of an address share group that has a registered address and provides no address during Bind", func(t T) { - require.Error(t, cr.Bind(ctx, []types.BoundContract{{Name: AnyContractName}})) - }) - }, + byteTokens = append(byteTokens, pubKey1.Bytes()) + byteTokens = append(byteTokens, pubKey2.Bytes()) + require.NoError(t, cr.GetLatestValue(ctx, bound.ReadIdentifier(GetTokenPrices), primitives.Unconfirmed, map[string]any{"tokens": byteTokens}, &res)) + require.Equal(t, "7048352069843304521481572571769838000081483315549204879493368331", res[0].Value.String()) + require.Equal(t, uint32(1700000001), res[0].Timestamp) + require.Equal(t, "17980346130170174053328187512531209543631592085982266692926093439168", res[1].Value.String()) + require.Equal(t, uint32(1800000002), res[1].Timestamp) + }}, } - RunTests(t, it, []Testcase[T]{testCases}) + RunTests(t, it, testCases) RunContractReaderTests(t, it) RunChainWriterTests(t, it) } @@ -261,6 +295,7 @@ func RunChainWriterTests[T WrappedTestingT[T]](t T, it *SolanaChainComponentsInt // GetLatestValue method const ( + ContractReaderNotFoundReadsReturnZeroedResponses = "Get latest value not found reads return zeroed responses" ContractReaderGetLatestValueUsingMultiReader = "Get latest value using multi reader" ContractReaderBatchGetLatestValueUsingMultiReader = "Batch Get latest value using multi reader" ContractReaderGetLatestValueWithAddressHardcodedIntoResponse = "Get latest value with AddressHardcoded into response" @@ -269,14 +304,50 @@ const ( ChainWriterLookupTableTest = "Set contract value using a lookup table for addresses" ) -type TimestampedUnixBig struct { - Value *big.Int `json:"value"` - Timestamp uint32 `json:"timestamp"` -} - func RunContractReaderInLoopTests[T WrappedTestingT[T]](t T, it ChainComponentsInterfaceTester[T]) { //RunContractReaderInterfaceTests(t, it, false, true) testCases := []Testcase[T]{ + { + Name: ContractReaderNotFoundReadsReturnZeroedResponses, + Test: func(t T) { + cr := it.GetContractReader(t) + bindings := it.GetBindings(t) + ctx := tests.Context(t) + + bound := BindingsByName(bindings, AnyContractName)[0] + require.NoError(t, cr.Bind(ctx, bindings)) + + dAccRes := contractprimary.DataAccount{} + require.NoError(t, cr.GetLatestValue(ctx, bound.ReadIdentifier(ReadUninitializedPDA), primitives.Unconfirmed, nil, &dAccRes)) + require.Equal(t, contractprimary.DataAccount{}, dAccRes) + + mR3Res := contractprimary.MultiRead3{} + batchGetLatestValueRequest := make(types.BatchGetLatestValuesRequest) + batchGetLatestValueRequest[bound] = []types.BatchRead{ + { + ReadName: ReadUninitializedPDA, + Params: nil, + ReturnVal: &dAccRes, + }, + { + ReadName: MultiReadWithParamsReuse, + Params: map[string]any{"ID": 999}, + ReturnVal: &mR3Res, + }, + } + + batchResult, err := cr.BatchGetLatestValues(ctx, batchGetLatestValueRequest) + require.NoError(t, err) + + result, err := batchResult[bound][0].GetResult() + require.NoError(t, err) + require.Equal(t, &contractprimary.DataAccount{}, result) + + result, err = batchResult[bound][1].GetResult() + require.NoError(t, err) + require.Equal(t, &contractprimary.MultiRead3{}, result) + }, + }, { Name: ContractReaderGetLatestValueWithAddressHardcodedIntoResponse, Test: func(t T) { @@ -767,6 +838,7 @@ func (h *helper) runInitialize( } const ( + ReadUninitializedPDA = "ReadUninitializedPDA" MultiRead = "MultiRead" ReadWithAddressHardCodedIntoResponse = "ReadWithAddressHardCodedIntoResponse" MultiReadWithParamsReuse = "MultiReadWithParamsReuse" @@ -848,6 +920,13 @@ func (it *SolanaChainComponentsInterfaceTester[T]) buildContractReaderConfig(t T AnyContractName: { IDL: idl, Reads: map[string]config.ReadDefinition{ + ReadUninitializedPDA: { + ChainSpecificName: "DataAccount", + ReadType: config.Account, + PDADefinition: codec.PDATypeDef{ + Prefix: []byte("AAAAAAAAAA"), + }, + }, ReadWithAddressHardCodedIntoResponse: readWithAddressHardCodedIntoResponseDef, GetTokenPrices: { ChainSpecificName: "USDPerToken", diff --git a/pkg/solana/chainreader/batch.go b/pkg/solana/chainreader/batch.go index cb1057cdb..90f4467ae 100644 --- a/pkg/solana/chainreader/batch.go +++ b/pkg/solana/chainreader/batch.go @@ -8,13 +8,15 @@ import ( "github.com/gagliardetto/solana-go" + "github.com/smartcontractkit/chainlink-common/pkg/logger" "github.com/smartcontractkit/chainlink-common/pkg/types" "github.com/smartcontractkit/chainlink-common/pkg/values" ) type call struct { - Namespace, ReadName string - Params, ReturnVal any + Namespace, ReadName string + Params, ReturnVal any + ErrOnMissingAccountData bool } type batchResultWithErr struct { @@ -33,20 +35,21 @@ type MultipleAccountGetter interface { } // doMultiRead aggregate results from multiple PDAs from the same contract into one result. -func doMultiRead(ctx context.Context, client MultipleAccountGetter, bdRegistry *bindingsRegistry, rv readValues, params, returnValue any) error { +func doMultiRead(ctx context.Context, lggr logger.Logger, client MultipleAccountGetter, bdRegistry *bindingsRegistry, rv readValues, params, returnValue any) error { batch := make([]call, len(rv.reads)) for idx, r := range rv.reads { batch[idx] = call{ - Namespace: rv.contract, - ReadName: r.readName, - ReturnVal: returnValue, + Namespace: rv.contract, + ReadName: r.readName, + ReturnVal: returnValue, + ErrOnMissingAccountData: r.errOnMissingAccountData, } if r.useParams { batch[idx].Params = params } } - results, err := doMethodBatchCall(ctx, client, bdRegistry, batch) + results, err := doMethodBatchCall(ctx, lggr, client, bdRegistry, batch) if err != nil { return err } @@ -69,15 +72,14 @@ func doMultiRead(ctx context.Context, client MultipleAccountGetter, bdRegistry * return nil } -func doMethodBatchCall(ctx context.Context, client MultipleAccountGetter, bdRegistry *bindingsRegistry, batch []call) ([]batchResultWithErr, error) { +func doMethodBatchCall(ctx context.Context, lggr logger.Logger, client MultipleAccountGetter, bdRegistry *bindingsRegistry, batch []call) ([]batchResultWithErr, error) { results := make([]batchResultWithErr, len(batch)) // create the list of public keys to fetch - keys := []solana.PublicKey{} + var keys []solana.PublicKey // map batch call index to key index (some calls are event reads and will be handled by a different binding) dataMap := make(map[int]int) - for idx, batchCall := range batch { rBinding, err := bdRegistry.GetReader(batchCall.Namespace, batchCall.ReadName) if err != nil { @@ -129,16 +131,18 @@ func doMethodBatchCall(ctx context.Context, client MultipleAccountGetter, bdRegi returnVal: batchCall.ReturnVal, } - if data[dataIdx] == nil || len(data[dataIdx]) == 0 { - results[idx].err = ErrMissingAccountData - + if len(data[idx]) == 0 { + if batchCall.ErrOnMissingAccountData { + results[idx].err = ErrMissingAccountData + continue + } + lggr.Infow("failed to find account, returning zero value instead", "namespace", batchCall.Namespace, "readName", batchCall.ReadName, "address", keys[dataIdx].String()) continue } rBinding, err := bdRegistry.GetReader(results[idx].namespace, results[idx].readName) if err != nil { results[idx].err = err - continue } diff --git a/pkg/solana/chainreader/chain_reader.go b/pkg/solana/chainreader/chain_reader.go index bd59d4b45..482c6da20 100644 --- a/pkg/solana/chainreader/chain_reader.go +++ b/pkg/solana/chainreader/chain_reader.go @@ -13,6 +13,7 @@ import ( bin "github.com/gagliardetto/binary" "github.com/gagliardetto/solana-go" "github.com/gagliardetto/solana-go/rpc" + "github.com/mitchellh/mapstructure" "github.com/smartcontractkit/chainlink-ccip/chains/solana/gobindings/fee_quoter" commoncodec "github.com/smartcontractkit/chainlink-common/pkg/codec" @@ -183,7 +184,7 @@ func (s *ContractReaderService) GetLatestValue(ctx context.Context, readIdentifi } if len(values.reads) > 1 { - return doMultiRead(ctx, s.client, s.bdRegistry, values, params, returnVal) + return doMultiRead(ctx, s.lggr, s.client, s.bdRegistry, values, params, returnVal) } // TODO this is a temporary edge case - NONEVM-1320 @@ -196,14 +197,15 @@ func (s *ContractReaderService) GetLatestValue(ctx context.Context, readIdentifi batch := []call{ { - Namespace: values.contract, - ReadName: values.reads[0].readName, - Params: params, - ReturnVal: returnVal, + Namespace: values.contract, + ReadName: values.reads[0].readName, + Params: params, + ReturnVal: returnVal, + ErrOnMissingAccountData: values.reads[0].errOnMissingAccountData, }, } - results, err := doMethodBatchCall(ctx, s.client, s.bdRegistry, batch) + results, err := doMethodBatchCall(ctx, s.lggr, s.client, s.bdRegistry, batch) if err != nil { return err } @@ -244,7 +246,7 @@ func (s *ContractReaderService) BatchGetLatestValues(ctx context.Context, reques // exclude multi read reads from the big batch request and populate them separately and merge results later. if len(vals.reads) > 1 { - err := doMultiRead(ctx, s.client, s.bdRegistry, vals, readReq.Params, readReq.ReturnVal) + err := doMultiRead(ctx, s.lggr, s.client, s.bdRegistry, vals, readReq.Params, readReq.ReturnVal) multiIdxLookup[bound][idx] = len(multiReadResults) multiReadResults = append(multiReadResults, batchResultWithErr{address: vals.address, namespace: vals.contract, readName: readReq.ReadName, returnVal: readReq.ReturnVal, err: err}) @@ -260,15 +262,16 @@ func (s *ContractReaderService) BatchGetLatestValues(ctx context.Context, reques } batch = append(batch, call{ - Namespace: bound.Name, - ReadName: readReq.ReadName, - Params: readReq.Params, - ReturnVal: readReq.ReturnVal, + Namespace: bound.Name, + ReadName: readReq.ReadName, + Params: readReq.Params, + ReturnVal: readReq.ReturnVal, + ErrOnMissingAccountData: vals.reads[0].errOnMissingAccountData, }) } } - results, err := doMethodBatchCall(ctx, s.client, s.bdRegistry, batch) + results, err := doMethodBatchCall(ctx, s.lggr, s.client, s.bdRegistry, batch) if err != nil { return nil, err } @@ -450,7 +453,7 @@ func (s *ContractReaderService) initNamespace(namespaces map[string]config.Chain } func (s *ContractReaderService) addAccountRead(namespace string, genericName string, idl codec.IDL, outputIDLDef codec.IdlTypeDef, readDefinition config.ReadDefinition) error { - reads := []read{{readName: genericName, useParams: true}} + reads := []read{{readName: genericName, useParams: true, errOnMissingAccountData: readDefinition.ErrOnMissingAccountData}} if readDefinition.MultiReader != nil { multiRead, err := s.addMultiAccountReadToCodec(namespace, readDefinition, idl) if err != nil { @@ -521,8 +524,9 @@ func (s *ContractReaderService) addMultiAccountReadToCodec(namespace string, rea s.bdRegistry.AddReader(namespace, genericName, newAccountReadBinding(namespace, genericName, isPDA, mr.PDADefinition.Prefix, idl, inputIDLDef, accountIDLDef, readDefinition)) reads = append(reads, read{ - readName: genericName, - useParams: readDefinition.MultiReader.ReuseParams, + readName: genericName, + useParams: readDefinition.MultiReader.ReuseParams, + errOnMissingAccountData: mr.ErrOnMissingAccountData, }) } @@ -708,6 +712,11 @@ func (s *ContractReaderService) handleGetTokenPricesGetLatestValue( return err } + if len(pdaAddresses) == 0 { + s.lggr.Infof("No token addresses found in params: %v that were passed into %q, call to contract: %q with address: %q", params, GetTokenPrices, values.contract, values.address) + return nil + } + data, err := s.client.GetMultipleAccountData(ctx, pdaAddresses...) if err != nil { return err @@ -717,7 +726,35 @@ func (s *ContractReaderService) handleGetTokenPricesGetLatestValue( if returnSliceVal.Kind() != reflect.Ptr { return fmt.Errorf("expected <**[]*struct { Value *big.Int; Timestamp *int64 } Value>, got %q", returnSliceVal.String()) } + returnSliceVal = returnSliceVal.Elem() + // if called directly instead of as a loop + if returnSliceVal.Kind() == reflect.Slice { + underlyingType := returnSliceVal.Type().Elem() + if underlyingType.Kind() == reflect.Struct { + if _, hasValue := underlyingType.FieldByName("Value"); hasValue { + if _, hasTimestamp := underlyingType.FieldByName("Timestamp"); hasTimestamp { + sliceVal := reflect.MakeSlice(returnSliceVal.Type(), 0, 0) + for _, d := range data { + var wrapper fee_quoter.BillingTokenConfigWrapper + // if we got back an empty account then the account must not exist yet, use zero value + if len(d) > 0 { + if err = wrapper.UnmarshalWithDecoder(bin.NewBorshDecoder(d)); err != nil { + return err + } + } + newElem := reflect.New(underlyingType).Elem() + newElem.FieldByName("Value").Set(reflect.ValueOf(big.NewInt(0).SetBytes(wrapper.Config.UsdPerToken.Value[:]))) + // nolint:gosec + // G115: integer overflow conversion int64 -> uint32 + newElem.FieldByName("Timestamp").Set(reflect.ValueOf(uint32(wrapper.Config.UsdPerToken.Timestamp))) + sliceVal = reflect.Append(sliceVal, newElem) + } + return mapstructure.Decode(sliceVal.Interface(), returnVal) + } + } + } + } returnSliceValType := returnSliceVal.Type() if returnSliceValType.Kind() != reflect.Ptr { @@ -753,8 +790,11 @@ func (s *ContractReaderService) handleGetTokenPricesGetLatestValue( for _, d := range data { var wrapper fee_quoter.BillingTokenConfigWrapper - if err = wrapper.UnmarshalWithDecoder(bin.NewBorshDecoder(d)); err != nil { - return err + // if we got back an empty account then the account must not exist yet, use zero value + if len(d) > 0 { + if err = wrapper.UnmarshalWithDecoder(bin.NewBorshDecoder(d)); err != nil { + return err + } } newElemPtr := reflect.New(underlyingStruct) @@ -783,25 +823,46 @@ func (s *ContractReaderService) getPDAsForGetTokenPrices(params any, values read if val.Kind() == reflect.Ptr { val = val.Elem() } - if val.Kind() != reflect.Struct { + + var field reflect.Value + switch val.Kind() { + case reflect.Struct: + field = val.FieldByName("Tokens") + if !field.IsValid() { + field = val.FieldByName("tokens") + } + case reflect.Map: + field = val.MapIndex(reflect.ValueOf("Tokens")) + if !field.IsValid() { + field = val.MapIndex(reflect.ValueOf("tokens")) + } + default: return nil, fmt.Errorf( - "for contract %q read %q: expected `params` to be a struct, got %s", - values.contract, values.reads[0].readName, val.Kind(), + "for contract %q read %q: expected `params` to be a struct or map, got %q: %q", + values.contract, values.reads[0].readName, val.Kind(), val.String(), ) } - field := val.FieldByName("Tokens") if !field.IsValid() { return nil, fmt.Errorf( - "for contract %q read %q: no field named 'Tokens' found in params", - values.contract, values.reads[0].readName, + "for contract %q read %q: no field named 'Tokens' found in kind: %q: %q", + values.contract, values.reads[0].readName, val.Kind(), val.String(), ) } - tokens, ok := field.Interface().(*[][32]uint8) - if !ok { + var tokens [][]uint8 + switch x := field.Interface().(type) { + // this is the type when CR is called as LOOP and creates types from IDL + case *[][32]uint8: + for _, arr := range *x { + tokens = append(tokens, arr[:]) // Slice [32]uint8 → []uint8 + } + // this is the expected type when CR is called directly + case [][]uint8: + tokens = x + default: return nil, fmt.Errorf( - "for contract %q read %q: 'Tokens' field is not of type *[][32]uint8", + "for contract %q read %q: 'Tokens' field is neither *[][32]uint8 nor [][]uint8", values.contract, values.reads[0].readName, ) } @@ -816,7 +877,7 @@ func (s *ContractReaderService) getPDAsForGetTokenPrices(params any, values read // Build the PDA addresses for all tokens. var pdaAddresses []solana.PublicKey - for _, token := range *tokens { + for _, token := range tokens { tokenAddr := solana.PublicKeyFromBytes(token[:]) if !tokenAddr.IsOnCurve() || tokenAddr.IsZero() { return nil, fmt.Errorf( diff --git a/pkg/solana/chainreader/chain_reader_test.go b/pkg/solana/chainreader/chain_reader_test.go index eb905b6bf..e087047f9 100644 --- a/pkg/solana/chainreader/chain_reader_test.go +++ b/pkg/solana/chainreader/chain_reader_test.go @@ -16,11 +16,12 @@ import ( "github.com/cometbft/cometbft/libs/service" "github.com/gagliardetto/solana-go" "github.com/google/uuid" - "github.com/smartcontractkit/chainlink-common/pkg/sqlutil/sqltest" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" + "github.com/smartcontractkit/chainlink-common/pkg/sqlutil/sqltest" + "github.com/smartcontractkit/libocr/commontypes" codeccommon "github.com/smartcontractkit/chainlink-common/pkg/codec" @@ -620,8 +621,9 @@ func newTestConfAndCodec(t *testing.T) (types.RemoteCodec, config.ContractReader IDL: mustUnmarshalIDL(t, rawIDL), Reads: map[string]config.ReadDefinition{ NamedMethod: { - ChainSpecificName: testutils.TestStructWithNestedStruct, - ReadType: config.Account, + ChainSpecificName: testutils.TestStructWithNestedStruct, + ReadType: config.Account, + ErrOnMissingAccountData: true, OutputModifications: codeccommon.ModifiersConfig{ &codeccommon.RenameModifierConfig{Fields: map[string]string{"Value": "V"}}, }, diff --git a/pkg/solana/chainreader/client_wrapper.go b/pkg/solana/chainreader/client_wrapper.go index 4755d93d6..8196e1cde 100644 --- a/pkg/solana/chainreader/client_wrapper.go +++ b/pkg/solana/chainreader/client_wrapper.go @@ -28,18 +28,10 @@ func (w *RPCClientWrapper) GetMultipleAccountData(ctx context.Context, keys ...s bts := make([][]byte, len(result.Value)) for idx, res := range result.Value { - if res == nil { - return nil, rpc.ErrNotFound + if res == nil || res.Data == nil || res.Data.GetBinary() == nil { + // any accounts that can't be resolved will be nil + continue } - - if res.Data == nil { - return nil, rpc.ErrNotFound - } - - if res.Data.GetBinary() == nil { - return nil, rpc.ErrNotFound - } - bts[idx] = res.Data.GetBinary() } diff --git a/pkg/solana/chainreader/lookup.go b/pkg/solana/chainreader/lookup.go index ef7b1a551..1ca9c6dc4 100644 --- a/pkg/solana/chainreader/lookup.go +++ b/pkg/solana/chainreader/lookup.go @@ -9,7 +9,7 @@ import ( type read struct { readName string // useParams is used when this read is part of a multi read to determine if it should use parent read params. - useParams bool + useParams, errOnMissingAccountData bool } type readValues struct { diff --git a/pkg/solana/config/chain_reader.go b/pkg/solana/config/chain_reader.go index dfeae9e6d..7dd6921a9 100644 --- a/pkg/solana/config/chain_reader.go +++ b/pkg/solana/config/chain_reader.go @@ -83,13 +83,14 @@ type MultiReader struct { } type ReadDefinition struct { - ChainSpecificName string `json:"chainSpecificName"` - ReadType ReadType `json:"readType,omitempty"` - InputModifications commoncodec.ModifiersConfig `json:"inputModifications,omitempty"` - OutputModifications commoncodec.ModifiersConfig `json:"outputModifications,omitempty"` - PDADefinition codec.PDATypeDef `json:"pdaDefinition,omitempty"` // Only used for PDA account reads - MultiReader *MultiReader `json:"multiReader,omitempty"` - EventDefinitions *EventDefinitions `json:"eventDefinitions,omitempty"` + ChainSpecificName string `json:"chainSpecificName"` + ReadType ReadType `json:"readType,omitempty"` + ErrOnMissingAccountData bool `json:"errOnMissingAccountData,omitempty"` + InputModifications commoncodec.ModifiersConfig `json:"inputModifications,omitempty"` + OutputModifications commoncodec.ModifiersConfig `json:"outputModifications,omitempty"` + PDADefinition codec.PDATypeDef `json:"pdaDefinition,omitempty"` // Only used for PDA account reads + MultiReader *MultiReader `json:"multiReader,omitempty"` + EventDefinitions *EventDefinitions `json:"eventDefinitions,omitempty"` // ResponseAddressHardCoder hardcodes the address of the contract into the defined field in the response. ResponseAddressHardCoder *commoncodec.HardCodeModifierConfig `json:"responseAddressHardCoder,omitempty"` } From e8b2d8a71a0f90dbd672dfc61720571c8c9d8d4d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bla=C5=BE=20Hrastnik?= Date: Thu, 20 Feb 2025 17:51:29 +0900 Subject: [PATCH 4/5] chainreader: fix regression: RPC requires an array of items, not nil (#1092) --- pkg/solana/chainreader/batch.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pkg/solana/chainreader/batch.go b/pkg/solana/chainreader/batch.go index 90f4467ae..f715216f9 100644 --- a/pkg/solana/chainreader/batch.go +++ b/pkg/solana/chainreader/batch.go @@ -76,7 +76,8 @@ func doMethodBatchCall(ctx context.Context, lggr logger.Logger, client MultipleA results := make([]batchResultWithErr, len(batch)) // create the list of public keys to fetch - var keys []solana.PublicKey + // Solana RPC expects at least an empty list, not nil + keys := []solana.PublicKey{} // map batch call index to key index (some calls are event reads and will be handled by a different binding) dataMap := make(map[int]int) From 3333854258e84f322f74388e7470e4873c52c52b Mon Sep 17 00:00:00 2001 From: Silas Lenihan <32529249+silaslenihan@users.noreply.github.com> Date: Thu, 20 Feb 2025 04:09:55 -0500 Subject: [PATCH 5/5] Specify Ignorable Errors for Optional Lookups (#1091) Co-authored-by: Jonghyeon Park --- .../relayinterface/lookups_test.go | 42 +++++++++++++++---- pkg/solana/chainwriter/chain_writer.go | 11 ++++- pkg/solana/chainwriter/lookups.go | 12 ++++-- 3 files changed, 53 insertions(+), 12 deletions(-) diff --git a/integration-tests/relayinterface/lookups_test.go b/integration-tests/relayinterface/lookups_test.go index 910b2cf69..bc0e3eb6e 100644 --- a/integration-tests/relayinterface/lookups_test.go +++ b/integration-tests/relayinterface/lookups_test.go @@ -150,7 +150,7 @@ func TestAccountLookups(t *testing.T) { IsWritable: chainwriter.MetaBool{Value: true}, }} _, err := lookupConfig.AccountLookup.Resolve(testArgs) - require.Error(t, err) + require.ErrorIs(t, err, chainwriter.ErrLookupNotFoundAtLocation) }) t.Run("AccountLookup works with MetaBool bitmap lookups", func(t *testing.T) { @@ -360,8 +360,7 @@ func TestPDALookups(t *testing.T) { } _, err := pdaLookup.Resolve(ctx, args, nil, client.MultiClient{}) - require.Error(t, err) - require.Contains(t, err.Error(), "key not found") + require.ErrorIs(t, err, chainwriter.ErrGettingSeedAtLocation) }) t.Run("PDALookup resolves valid PDA with address lookup seeds", func(t *testing.T) { @@ -569,6 +568,21 @@ func TestLookupTables(t *testing.T) { require.Contains(t, err.Error(), "error fetching account info for table") // Example error message }) + t.Run("Derived lookup table fails with invalid table name", func(t *testing.T) { + derivedTableMap := map[string]map[string][]*solana.AccountMeta{ + "DerivedTable": {}, + } + accountsFromLookupTable := chainwriter.Lookup{ + AccountsFromLookupTable: &chainwriter.AccountsFromLookupTable{ + LookupTableName: "InvalidTable", + IncludeIndexes: []int{}, + }, + } + + _, err = accountsFromLookupTable.Resolve(ctx, nil, derivedTableMap, multiClient) + require.ErrorIs(t, err, chainwriter.ErrLookupTableNotFound) + }) + t.Run("Static lookup table fails with invalid address", func(t *testing.T) { invalidTable := chainwriter.GetRandomPubKey(t) @@ -608,8 +622,15 @@ func TestLookupTables(t *testing.T) { derivedTableMap, _, err := cw.ResolveLookupTables(ctx, testArgs, lookupConfig) require.NoError(t, err) - addresses, ok := derivedTableMap["DerivedTable"][table.String()] - require.True(t, ok) + accountsFromLookupTable := chainwriter.Lookup{ + AccountsFromLookupTable: &chainwriter.AccountsFromLookupTable{ + LookupTableName: "DerivedTable", + IncludeIndexes: []int{}, + }, + } + + addresses, err := accountsFromLookupTable.Resolve(ctx, nil, derivedTableMap, multiClient) + require.NoError(t, err) for i, address := range addresses { require.Equal(t, pubKeys[i], address.PublicKey) } @@ -654,8 +675,15 @@ func TestLookupTables(t *testing.T) { derivedTableMap, _, err := cw.ResolveLookupTables(ctx, args, lookupConfig) require.NoError(t, err) - addresses, ok := derivedTableMap["DerivedTable"][lookupTable.String()] - require.True(t, ok) + accountsFromLookupTable := chainwriter.Lookup{ + AccountsFromLookupTable: &chainwriter.AccountsFromLookupTable{ + LookupTableName: "DerivedTable", + IncludeIndexes: []int{}, + }, + } + + addresses, err := accountsFromLookupTable.Resolve(ctx, args, derivedTableMap, multiClient) + require.NoError(t, err) for i, address := range addresses { require.Equal(t, lookupKeys[i], address.PublicKey) } diff --git a/pkg/solana/chainwriter/chain_writer.go b/pkg/solana/chainwriter/chain_writer.go index 0931406ba..9ba2c1e5f 100644 --- a/pkg/solana/chainwriter/chain_writer.go +++ b/pkg/solana/chainwriter/chain_writer.go @@ -155,7 +155,7 @@ func GetAddresses(ctx context.Context, args any, accounts []Lookup, derivedTable var addresses []*solana.AccountMeta for _, accountConfig := range accounts { meta, err := accountConfig.Resolve(ctx, args, derivedTableMap, client) - if accountConfig.Optional && err != nil { + if accountConfig.Optional && err != nil && isIgnorableError(err) { // skip optional accounts if they are not found continue } @@ -167,6 +167,13 @@ func GetAddresses(ctx context.Context, args any, accounts []Lookup, derivedTable return addresses, nil } +// These errors are ignorable if the lookup is optional. +func isIgnorableError(err error) bool { + return errors.Is(err, ErrLookupNotFoundAtLocation) || + errors.Is(err, ErrLookupTableNotFound) || + errors.Is(err, ErrGettingSeedAtLocation) +} + // FilterLookupTableAddresses takes a list of accounts and two lookup table maps // (one for derived tables, one for static tables) and filters out any addresses that are // not used by the accounts. It returns a map of only those lookup table @@ -460,7 +467,7 @@ func (s *SolanaChainWriterService) ResolveLookupTables(ctx context.Context, args // Load the lookup table - note: This could be multiple tables if the lookup is a PDALookups that resolves to more // than one address lookupTableMap, err := s.loadTable(ctx, args, derivedLookup) - if derivedLookup.Optional && err != nil { + if derivedLookup.Optional && err != nil && isIgnorableError(err) { continue } if err != nil { diff --git a/pkg/solana/chainwriter/lookups.go b/pkg/solana/chainwriter/lookups.go index 2ae81b16f..e3782b56d 100644 --- a/pkg/solana/chainwriter/lookups.go +++ b/pkg/solana/chainwriter/lookups.go @@ -13,6 +13,12 @@ import ( "github.com/smartcontractkit/chainlink-solana/pkg/solana/codec" ) +var ( + ErrLookupNotFoundAtLocation = fmt.Errorf("error getting account from lookup") + ErrLookupTableNotFound = fmt.Errorf("lookup table not found") + ErrGettingSeedAtLocation = fmt.Errorf("error getting address seed for location") +) + type Lookup struct { Optional bool AccountConstant *AccountConstant `json:"accountConstant,omitempty"` @@ -146,7 +152,7 @@ func (ac AccountConstant) Resolve() ([]*solana.AccountMeta, error) { func (al AccountLookup) Resolve(args any) ([]*solana.AccountMeta, error) { derivedValues, err := GetValuesAtLocation(args, al.Location) if err != nil { - return nil, fmt.Errorf("error getting account from lookup: %w", err) + return nil, fmt.Errorf("%w: %v", ErrLookupNotFoundAtLocation, err) } var metas []*solana.AccountMeta @@ -206,7 +212,7 @@ func (alt AccountsFromLookupTable) Resolve(derivedTableMap map[string]map[string // Fetch the inner map for the specified lookup table name innerMap, ok := derivedTableMap[alt.LookupTableName] if !ok { - return nil, fmt.Errorf("lookup table not found: %s", alt.LookupTableName) + return nil, fmt.Errorf("%w: %s", ErrLookupTableNotFound, alt.LookupTableName) } var result []*solana.AccountMeta @@ -332,7 +338,7 @@ func getSeedBytesCombinations( // Get value from a location (This doesn't have to be an address, it can be any value) bytes, err := GetValuesAtLocation(args, lookupSeed.Location) if err != nil { - return nil, fmt.Errorf("error getting address seed for location %q: %w", lookupSeed.Location, err) + return nil, fmt.Errorf("%w %q: %w", ErrGettingSeedAtLocation, lookupSeed.Location, err) } // append each byte array to the expansions for _, b := range bytes {