Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve account reading err handling and fix get token prices for a non loop call #1079

Merged
merged 6 commits into from
Feb 17, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
173 changes: 102 additions & 71 deletions integration-tests/relayinterface/chain_components_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down Expand Up @@ -334,8 +368,9 @@ func RunContractReaderInLoopTests[T WrappedTestingT[T]](t T, it ChainComponentsI
AddressToShare []byte
}

c := int16(0)
mRR := MultiReadResult{}
require.NoError(t, cr.GetLatestValue(ctx, bound.ReadIdentifier(ReadWithAddressHardCodedIntoResponse), primitives.Unconfirmed, nil, &mRR))
require.NoError(t, cr.GetLatestValue(ctx, bound.ReadIdentifier(ReadWithAddressHardCodedIntoResponse), primitives.Unconfirmed, nil, &c))

expectedMRR := MultiReadResult{A: 1, B: 2, SharedAddress: boundAddress.Bytes(), AddressToShare: boundAddress.Bytes()}
require.Equal(t, expectedMRR, mRR)
Expand Down Expand Up @@ -841,12 +876,8 @@ func (it *SolanaChainComponentsInterfaceTester[T]) buildContractReaderConfig(t T
PDADefinition: codec.PDATypeDef{
Prefix: []byte("multi_read1"),
},
ResponseAddressHardCoder: &commoncodec.HardCodeModifierConfig{
// placeholder values, whatever is put as value gets replaced with a solana pub key anyway
OffChainValues: map[string]any{
"SharedAddress": "",
"AddressToShare": "",
},
OutputModifications: commoncodec.ModifiersConfig{
&commoncodec.PropertyExtractorConfig{FieldName: "B"},
},
}

Expand Down
72 changes: 62 additions & 10 deletions pkg/solana/chainreader/chain_reader.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -696,6 +697,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
Expand All @@ -705,7 +711,32 @@ 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 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 -&gt; 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 {
Expand Down Expand Up @@ -771,25 +802,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,
)
}
Expand All @@ -804,7 +856,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(
Expand Down
Loading