From e67d492ba2fda8f4b3d97ad935211acc6a18c95a Mon Sep 17 00:00:00 2001 From: Silas Lenihan Date: Fri, 11 Oct 2024 10:18:21 -0500 Subject: [PATCH 01/22] Starting mapping out Solana ChainWriter --- pkg/solana/chainwriter/chain_writer.go | 128 +++++++++++++++++++++++++ 1 file changed, 128 insertions(+) create mode 100644 pkg/solana/chainwriter/chain_writer.go diff --git a/pkg/solana/chainwriter/chain_writer.go b/pkg/solana/chainwriter/chain_writer.go new file mode 100644 index 000000000..f01345d2c --- /dev/null +++ b/pkg/solana/chainwriter/chain_writer.go @@ -0,0 +1,128 @@ +package chainwriter + +import ( + "context" + "fmt" + "math/big" + + "github.com/gagliardetto/solana-go" + + "github.com/smartcontractkit/chainlink-common/pkg/codec" + "github.com/smartcontractkit/chainlink-common/pkg/services" + "github.com/smartcontractkit/chainlink-common/pkg/types" + + "github.com/smartcontractkit/chainlink-solana/pkg/solana/client" + "github.com/smartcontractkit/chainlink-solana/pkg/solana/fees" + "github.com/smartcontractkit/chainlink-solana/pkg/solana/txm" +) + +type SolanaChainWriterService struct { + reader client.Reader + txm txm.Txm + ge fees.Estimator +} + +type ChainWriterConfig struct { + Programs map[string]ProgramConfig `json:"contracts" toml:"contracts"` +} + +type ProgramConfig struct { + Methods map[string]MethodConfig `json:"methods" toml:"methods"` +} + +type MethodConfig struct { + InputModifications codec.ModifiersConfig `json:"inputModifications,omitempty"` + ChainSpecificName string `json:"chainSpecificName"` +} + +func NewSolanaChainWriterService(reader client.Reader, txm txm.Txm, ge fees.Estimator) *SolanaChainWriterService { + return &SolanaChainWriterService{ + reader: reader, + txm: txm, + ge: ge, + } +} + +var ( + _ services.Service = &SolanaChainWriterService{} + _ types.ChainWriter = &SolanaChainWriterService{} +) + +func (s *SolanaChainWriterService) SubmitTransaction(ctx context.Context, contractName, method string, args any, transactionID string, toAddress string, meta *types.TxMeta, value *big.Int) error { + data, ok := args.([]byte) + if !ok { + return fmt.Errorf("Unable to convert args to []byte") + } + + blockhash, err := s.reader.LatestBlockhash() + + programId, err := solana.PublicKeyFromBase58(contractName) + if err != nil { + return fmt.Errorf("Error getting programId: %w", err) + } + + // placeholder method to get accounts + accounts, feePayer, err := getAccounts(contractName, method, args) + if err != nil || len(accounts) == 0 { + return fmt.Errorf("Error getting accounts: %w", err) + } + + tx, err := solana.NewTransaction( + []solana.Instruction{ + solana.NewInstruction(programId, accounts, data), + }, + blockhash.Value.Blockhash, + solana.TransactionPayer(feePayer.PublicKey), + ) + if err != nil { + return fmt.Errorf("error creating new transaction: %w", err) + } + + if err = s.txm.Enqueue(accounts[0].PublicKey.String(), tx); err != nil { + return fmt.Errorf("error on sending trasnaction to TXM: %w", err) + } + return nil +} + +func getAccounts(contractName string, method string, args any) (accounts []*solana.AccountMeta, feePayer *solana.AccountMeta, err error) { + // TO DO: Use on-chain team's helper functions to get the accounts from CCIP related metadata. + return nil, nil, nil +} + +// 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) { + return types.Unknown, nil +} + +// GetFeeComponents retrieves the associated gas costs for executing a transaction. +func (s *SolanaChainWriterService) GetFeeComponents(ctx context.Context) (*types.ChainFeeComponents, error) { + if s.ge == nil { + return nil, fmt.Errorf("gas estimator not available") + } + + fee := s.ge.BaseComputeUnitPrice() + return &types.ChainFeeComponents{ + ExecutionFee: big.NewInt(int64(fee)), + DataAvailabilityFee: nil, + }, nil +} + +func (s *SolanaChainWriterService) Start(context.Context) error { + return nil +} + +func (s *SolanaChainWriterService) Close() error { + return nil +} + +func (s *SolanaChainWriterService) HealthReport() map[string]error { + return nil +} + +func (s *SolanaChainWriterService) Name() string { + return "" +} + +func (s *SolanaChainWriterService) Ready() error { + return nil +} From 3802482ce236de717912de88ba3ce72a77a4bf21 Mon Sep 17 00:00:00 2001 From: Silas Lenihan Date: Fri, 25 Oct 2024 15:09:13 -0400 Subject: [PATCH 02/22] Added Address searcher for decoded data --- .../actions/projectserum_version/action.yml | 1 + pkg/solana/chainwriter/chain_writer.go | 70 ++++++++++++---- pkg/solana/chainwriter/helpers.go | 83 +++++++++++++++++++ pkg/solana/chainwriter/helpers_test.go | 69 +++++++++++++++ 4 files changed, 205 insertions(+), 18 deletions(-) create mode 100644 pkg/solana/chainwriter/helpers.go create mode 100644 pkg/solana/chainwriter/helpers_test.go diff --git a/.github/actions/projectserum_version/action.yml b/.github/actions/projectserum_version/action.yml index 02e0406e8..9bc91323a 100644 --- a/.github/actions/projectserum_version/action.yml +++ b/.github/actions/projectserum_version/action.yml @@ -14,3 +14,4 @@ runs: run: | PSVERSION=$(make projectserum_version) echo "PSVERSION=${PSVERSION}" >>$GITHUB_OUTPUT +EVM2AnyRampMessage \ No newline at end of file diff --git a/pkg/solana/chainwriter/chain_writer.go b/pkg/solana/chainwriter/chain_writer.go index f01345d2c..9353b6b8b 100644 --- a/pkg/solana/chainwriter/chain_writer.go +++ b/pkg/solana/chainwriter/chain_writer.go @@ -2,16 +2,20 @@ package chainwriter import ( "context" + "encoding/json" "fmt" "math/big" + "reflect" "github.com/gagliardetto/solana-go" - "github.com/smartcontractkit/chainlink-common/pkg/codec" + commoncodec "github.com/smartcontractkit/chainlink-common/pkg/codec" + "github.com/smartcontractkit/chainlink-common/pkg/codec/encodings/binary" "github.com/smartcontractkit/chainlink-common/pkg/services" "github.com/smartcontractkit/chainlink-common/pkg/types" "github.com/smartcontractkit/chainlink-solana/pkg/solana/client" + "github.com/smartcontractkit/chainlink-solana/pkg/solana/codec" "github.com/smartcontractkit/chainlink-solana/pkg/solana/fees" "github.com/smartcontractkit/chainlink-solana/pkg/solana/txm" ) @@ -20,52 +24,77 @@ type SolanaChainWriterService struct { reader client.Reader txm txm.Txm ge fees.Estimator + codec types.Codec + config ChainWriterConfig } type ChainWriterConfig struct { - Programs map[string]ProgramConfig `json:"contracts" toml:"contracts"` + Programs map[string]ProgramConfig } type ProgramConfig struct { - Methods map[string]MethodConfig `json:"methods" toml:"methods"` + Methods map[string]MethodConfig + IDL string } type MethodConfig struct { - InputModifications codec.ModifiersConfig `json:"inputModifications,omitempty"` - ChainSpecificName string `json:"chainSpecificName"` + InputModifications commoncodec.ModifiersConfig + EncodedTypeIDL string + DataType any + DecodedTypeName string + ChainSpecificName string + AddressLocations []string + Signers []solana.AccountMeta + Writables []solana.AccountMeta } -func NewSolanaChainWriterService(reader client.Reader, txm txm.Txm, ge fees.Estimator) *SolanaChainWriterService { +func NewSolanaChainWriterService(reader client.Reader, txm txm.Txm, ge fees.Estimator, config ChainWriterConfig) *SolanaChainWriterService { return &SolanaChainWriterService{ reader: reader, txm: txm, ge: ge, + config: config, } } -var ( - _ services.Service = &SolanaChainWriterService{} - _ types.ChainWriter = &SolanaChainWriterService{} -) - func (s *SolanaChainWriterService) SubmitTransaction(ctx context.Context, contractName, method string, args any, transactionID string, toAddress string, meta *types.TxMeta, value *big.Int) error { + programConfig := s.config.Programs[contractName] + methodConfig := programConfig.Methods[method] + data, ok := args.([]byte) if !ok { return fmt.Errorf("Unable to convert args to []byte") } - blockhash, err := s.reader.LatestBlockhash() + // decode data + var idl codec.IDL + err := json.Unmarshal([]byte(methodConfig.EncodedTypeIDL), &idl) + if err != nil { + return fmt.Errorf("error unmarshalling IDL: %w", err) + } + cwCodec, err := codec.NewIDLAccountCodec(idl, binary.LittleEndian()) + if err != nil { + return fmt.Errorf("error creating new IDLAccountCodec: %w", err) + } + + // Create an instance of the type defined by methodConfig.DataType + decoded := reflect.New(reflect.TypeOf(methodConfig.DataType)).Interface() + err = cwCodec.Decode(ctx, data, decoded, methodConfig.DecodedTypeName) + + accounts, err := GetAddressesFromDecodedData(decoded, methodConfig.AddressLocations) + if err != nil { + return fmt.Errorf("error getting addresses from decoded data: %w", err) + } + + blockhash, err := s.reader.LatestBlockhash(ctx) programId, err := solana.PublicKeyFromBase58(contractName) if err != nil { return fmt.Errorf("Error getting programId: %w", err) } - // placeholder method to get accounts - accounts, feePayer, err := getAccounts(contractName, method, args) - if err != nil || len(accounts) == 0 { - return fmt.Errorf("Error getting accounts: %w", err) - } + // This isn't a real method, TBD how we will get this + feePayer := accounts[0] tx, err := solana.NewTransaction( []solana.Instruction{ @@ -78,12 +107,17 @@ func (s *SolanaChainWriterService) SubmitTransaction(ctx context.Context, contra return fmt.Errorf("error creating new transaction: %w", err) } - if err = s.txm.Enqueue(accounts[0].PublicKey.String(), tx); err != nil { + if err = s.txm.Enqueue(ctx, accounts[0].PublicKey.String(), tx); err != nil { return fmt.Errorf("error on sending trasnaction to TXM: %w", err) } return nil } +var ( + _ services.Service = &SolanaChainWriterService{} + _ types.ChainWriter = &SolanaChainWriterService{} +) + func getAccounts(contractName string, method string, args any) (accounts []*solana.AccountMeta, feePayer *solana.AccountMeta, err error) { // TO DO: Use on-chain team's helper functions to get the accounts from CCIP related metadata. return nil, nil, nil diff --git a/pkg/solana/chainwriter/helpers.go b/pkg/solana/chainwriter/helpers.go new file mode 100644 index 000000000..a59f5d6df --- /dev/null +++ b/pkg/solana/chainwriter/helpers.go @@ -0,0 +1,83 @@ +package chainwriter + +import ( + "errors" + "fmt" + "reflect" + "strings" + + "github.com/gagliardetto/solana-go" +) + +// GetAddressesFromDecodedData parses through nested types and arrays to find all address locations. +func GetAddressesFromDecodedData(decoded any, addressLocations []string) ([]*solana.AccountMeta, error) { + var addresses []*solana.AccountMeta + + for _, location := range addressLocations { + path := strings.Split(location, ".") + + addressList, err := traversePath(decoded, path) + if err != nil { + return nil, err + } + + for _, value := range addressList { + if byteArray, ok := value.([]byte); ok { + // TODO: How to handle IsSigner and IsWritable? + accountMeta := &solana.AccountMeta{ + PublicKey: solana.PublicKeyFromBytes(byteArray), + IsSigner: false, + IsWritable: true, + } + addresses = append(addresses, accountMeta) + } else { + return nil, fmt.Errorf("invalid address format at path: %s", location) + } + } + } + + return addresses, nil +} + +// traversePath recursively traverses the given structure based on the provided path. +func traversePath(data any, path []string) ([]any, error) { + if len(path) == 0 { + return []any{data}, nil + } + + var result []any + + val := reflect.ValueOf(data) + + if val.Kind() == reflect.Ptr { + val = val.Elem() + } + + switch val.Kind() { + case reflect.Struct: + field := val.FieldByName(path[0]) + if !field.IsValid() { + return nil, errors.New("field not found: " + path[0]) + } + return traversePath(field.Interface(), path[1:]) + + case reflect.Slice, reflect.Array: + for i := 0; i < val.Len(); i++ { + element := val.Index(i).Interface() + elements, err := traversePath(element, path) + if err == nil { + result = append(result, elements...) + } + } + if len(result) > 0 { + return result, nil + } + return nil, errors.New("no matching field found in array") + + default: + if len(path) == 1 && val.Kind() == reflect.Slice && val.Type().Elem().Kind() == reflect.Uint8 { + return []any{val.Interface()}, nil + } + return nil, errors.New("unexpected type encountered at path: " + path[0]) + } +} diff --git a/pkg/solana/chainwriter/helpers_test.go b/pkg/solana/chainwriter/helpers_test.go new file mode 100644 index 000000000..712ce255b --- /dev/null +++ b/pkg/solana/chainwriter/helpers_test.go @@ -0,0 +1,69 @@ +package chainwriter_test + +import ( + "testing" + + "github.com/gagliardetto/solana-go" + "github.com/smartcontractkit/chainlink-solana/pkg/solana/chainwriter" + "github.com/stretchr/testify/assert" +) + +type TestStruct struct { + Messages []Message +} + +type Message struct { + TokenAmounts []TokenAmount +} + +type TokenAmount struct { + SourceTokenAddress []byte + DestTokenAddress []byte +} + +func TestHelperLookupFunction(t *testing.T) { + addresses := make([][]byte, 8) + for i := 0; i < 8; i++ { + privKey, err := solana.NewRandomPrivateKey() + assert.NoError(t, err) + addresses[i] = privKey.PublicKey().Bytes() + } + + exampleDecoded := TestStruct{ + Messages: []Message{ + { + TokenAmounts: []TokenAmount{ + {addresses[0], addresses[1]}, + {addresses[2], addresses[3]}, + }, + }, + { + TokenAmounts: []TokenAmount{ + {addresses[4], addresses[5]}, + {addresses[6], addresses[7]}, + }, + }, + }, + } + + addressLocations := []string{ + "Messages.TokenAmounts.SourceTokenAddress", + "Messages.TokenAmounts.DestTokenAddress", + } + + derivedAddresses, err := chainwriter.GetAddressesFromDecodedData(exampleDecoded, addressLocations) + assert.NoError(t, err) + assert.Equal(t, 8, len(derivedAddresses)) + + // Create a map of the expected addresses for fast lookup + expectedAddresses := make(map[string]bool) + for _, addr := range addresses { + expectedAddresses[string(addr)] = true + } + + // Verify that each derived address matches an expected address + for _, derivedAddr := range derivedAddresses { + derivedBytes := derivedAddr.PublicKey.Bytes() + assert.True(t, expectedAddresses[string(derivedBytes)], "Address not found in expected list") + } +} From aa5733c414a642f073c6f33e2ab8aab16ca9cb28 Mon Sep 17 00:00:00 2001 From: Silas Lenihan Date: Mon, 28 Oct 2024 14:11:07 -0400 Subject: [PATCH 03/22] Introduced new Solana config --- pkg/solana/chainwriter/chain_writer.go | 107 ++++++++++++++++++++++--- pkg/solana/chainwriter/helpers.go | 34 +++----- pkg/solana/chainwriter/helpers_test.go | 13 ++- 3 files changed, 120 insertions(+), 34 deletions(-) diff --git a/pkg/solana/chainwriter/chain_writer.go b/pkg/solana/chainwriter/chain_writer.go index 9353b6b8b..90dac1edf 100644 --- a/pkg/solana/chainwriter/chain_writer.go +++ b/pkg/solana/chainwriter/chain_writer.go @@ -43,9 +43,98 @@ type MethodConfig struct { DataType any DecodedTypeName string ChainSpecificName string - AddressLocations []string - Signers []solana.AccountMeta - Writables []solana.AccountMeta + Accounts []Account + LookupTables []LookupTable +} + +type Account interface { +} + +type AccountConstant struct { + Address string + IsSigner bool + IsWritable bool +} + +type AccountLookup struct { + Location string + IsSigner bool + IsWritable bool +} + +type PDALookup struct { + PublicKey solana.PublicKey + Seeds [][]byte + IsSigner bool + IsWritable bool +} + +type LookupTable struct { + Address solana.PublicKey + Identifier Account + AccountIndices []int +} + +func (s *SolanaChainWriterService) GetAddresses(decoded any, accounts []Account) ([]*solana.AccountMeta, error) { + var addresses []*solana.AccountMeta + for _, accountConfig := range accounts { + switch lookupType := accountConfig.(type) { + case AccountConstant: + address, err := solana.PublicKeyFromBase58(lookupType.Address) + if err != nil { + return nil, fmt.Errorf("error getting account from constant: %w", err) + } + addresses = append(addresses, &solana.AccountMeta{ + PublicKey: address, + IsSigner: lookupType.IsSigner, + IsWritable: lookupType.IsWritable, + }) + case AccountLookup: + derivedAddresses, err := GetAddressAtLocation(decoded, lookupType.Location) + if err != nil { + return nil, fmt.Errorf("error getting account from lookup: %w", err) + } + for _, address := range derivedAddresses { + addresses = append(addresses, &solana.AccountMeta{ + PublicKey: address, + IsSigner: lookupType.IsSigner, + IsWritable: lookupType.IsWritable, + }) + } + case PDALookup: + pda, _, err := solana.FindProgramAddress(lookupType.Seeds, lookupType.PublicKey) + if err != nil { + return nil, fmt.Errorf("error finding program address: %w", err) + } + addresses = append(addresses, &solana.AccountMeta{ + PublicKey: pda, + IsSigner: lookupType.IsSigner, + IsWritable: lookupType.IsWritable, + }) + default: + return nil, fmt.Errorf("unsupported account type: %T", lookupType) + } + } + return addresses, nil +} + +func (s *SolanaChainWriterService) GetLookupTables(decoded any, accounts []*solana.AccountMeta, lookupTables []LookupTable) (map[solana.PublicKey]solana.PublicKeySlice, error) { + tables := make(map[solana.PublicKey]solana.PublicKeySlice) + for _, lookupTable := range lookupTables { + if reflect.TypeOf(lookupTable.Identifier) == reflect.TypeOf(LookupTable{}) { + return nil, fmt.Errorf("nested lookup tables are not supported") + } + // ids, err := s.GetAddresses(decoded, []Account{lookupTable.Identifier}) + // if err != nil { + // return nil, fmt.Errorf("error getting account from lookup table: %w", err) + // } + addresses := make(solana.PublicKeySlice, len(lookupTable.AccountIndices)) + for i, index := range lookupTable.AccountIndices { + addresses[i] = accounts[index].PublicKey + } + tables[lookupTable.Address] = addresses + } + return tables, nil } func NewSolanaChainWriterService(reader client.Reader, txm txm.Txm, ge fees.Estimator, config ChainWriterConfig) *SolanaChainWriterService { @@ -81,10 +170,14 @@ func (s *SolanaChainWriterService) SubmitTransaction(ctx context.Context, contra decoded := reflect.New(reflect.TypeOf(methodConfig.DataType)).Interface() err = cwCodec.Decode(ctx, data, decoded, methodConfig.DecodedTypeName) - accounts, err := GetAddressesFromDecodedData(decoded, methodConfig.AddressLocations) + accounts, err := s.GetAddresses(decoded, methodConfig.Accounts) if err != nil { return fmt.Errorf("error getting addresses from decoded data: %w", err) } + lookupTables, err := s.GetLookupTables(decoded, accounts, methodConfig.LookupTables) + if err != nil { + return fmt.Errorf("error getting lookup tables from decoded data: %w", err) + } blockhash, err := s.reader.LatestBlockhash(ctx) @@ -102,6 +195,7 @@ func (s *SolanaChainWriterService) SubmitTransaction(ctx context.Context, contra }, blockhash.Value.Blockhash, solana.TransactionPayer(feePayer.PublicKey), + solana.TransactionAddressTables(lookupTables), ) if err != nil { return fmt.Errorf("error creating new transaction: %w", err) @@ -118,11 +212,6 @@ var ( _ types.ChainWriter = &SolanaChainWriterService{} ) -func getAccounts(contractName string, method string, args any) (accounts []*solana.AccountMeta, feePayer *solana.AccountMeta, err error) { - // TO DO: Use on-chain team's helper functions to get the accounts from CCIP related metadata. - return nil, nil, nil -} - // 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) { return types.Unknown, nil diff --git a/pkg/solana/chainwriter/helpers.go b/pkg/solana/chainwriter/helpers.go index a59f5d6df..a5f9c1518 100644 --- a/pkg/solana/chainwriter/helpers.go +++ b/pkg/solana/chainwriter/helpers.go @@ -9,30 +9,22 @@ import ( "github.com/gagliardetto/solana-go" ) -// GetAddressesFromDecodedData parses through nested types and arrays to find all address locations. -func GetAddressesFromDecodedData(decoded any, addressLocations []string) ([]*solana.AccountMeta, error) { - var addresses []*solana.AccountMeta +// GetAddressAtLocation parses through nested types and arrays to find all address locations. +func GetAddressAtLocation(decoded any, location string) ([]solana.PublicKey, error) { + var addresses []solana.PublicKey - for _, location := range addressLocations { - path := strings.Split(location, ".") + path := strings.Split(location, ".") - addressList, err := traversePath(decoded, path) - if err != nil { - return nil, err - } + addressList, err := traversePath(decoded, path) + if err != nil { + return nil, err + } - for _, value := range addressList { - if byteArray, ok := value.([]byte); ok { - // TODO: How to handle IsSigner and IsWritable? - accountMeta := &solana.AccountMeta{ - PublicKey: solana.PublicKeyFromBytes(byteArray), - IsSigner: false, - IsWritable: true, - } - addresses = append(addresses, accountMeta) - } else { - return nil, fmt.Errorf("invalid address format at path: %s", location) - } + for _, value := range addressList { + if byteArray, ok := value.([]byte); ok { + addresses = append(addresses, solana.PublicKeyFromBytes(byteArray)) + } else { + return nil, fmt.Errorf("invalid address format at path: %s", location) } } diff --git a/pkg/solana/chainwriter/helpers_test.go b/pkg/solana/chainwriter/helpers_test.go index 712ce255b..45a9cdb6a 100644 --- a/pkg/solana/chainwriter/helpers_test.go +++ b/pkg/solana/chainwriter/helpers_test.go @@ -1,6 +1,7 @@ package chainwriter_test import ( + "fmt" "testing" "github.com/gagliardetto/solana-go" @@ -50,9 +51,13 @@ func TestHelperLookupFunction(t *testing.T) { "Messages.TokenAmounts.SourceTokenAddress", "Messages.TokenAmounts.DestTokenAddress", } - - derivedAddresses, err := chainwriter.GetAddressesFromDecodedData(exampleDecoded, addressLocations) - assert.NoError(t, err) + derivedAddresses := make([]solana.PublicKey, 0) + for _, location := range addressLocations { + addr, err := chainwriter.GetAddressAtLocation(exampleDecoded, location) + assert.NoError(t, err) + fmt.Println(len(addr)) + derivedAddresses = append(derivedAddresses, addr...) + } assert.Equal(t, 8, len(derivedAddresses)) // Create a map of the expected addresses for fast lookup @@ -63,7 +68,7 @@ func TestHelperLookupFunction(t *testing.T) { // Verify that each derived address matches an expected address for _, derivedAddr := range derivedAddresses { - derivedBytes := derivedAddr.PublicKey.Bytes() + derivedBytes := derivedAddr.Bytes() assert.True(t, expectedAddresses[string(derivedBytes)], "Address not found in expected list") } } From 71867ed0db7398dada45bdac5462f94563b1fc88 Mon Sep 17 00:00:00 2001 From: Silas Lenihan Date: Tue, 29 Oct 2024 15:09:05 -0400 Subject: [PATCH 04/22] Completed iteration of ChainWriter config --- pkg/solana/chainwriter/chain_writer.go | 385 ++++++++++++++++---- pkg/solana/chainwriter/chain_writer_test.go | 125 +++++++ pkg/solana/chainwriter/helpers.go | 43 ++- pkg/solana/chainwriter/helpers_test.go | 74 ---- 4 files changed, 476 insertions(+), 151 deletions(-) create mode 100644 pkg/solana/chainwriter/chain_writer_test.go delete mode 100644 pkg/solana/chainwriter/helpers_test.go diff --git a/pkg/solana/chainwriter/chain_writer.go b/pkg/solana/chainwriter/chain_writer.go index 90dac1edf..97a180c77 100644 --- a/pkg/solana/chainwriter/chain_writer.go +++ b/pkg/solana/chainwriter/chain_writer.go @@ -8,6 +8,7 @@ import ( "reflect" "github.com/gagliardetto/solana-go" + "github.com/gagliardetto/solana-go/rpc" commoncodec "github.com/smartcontractkit/chainlink-common/pkg/codec" "github.com/smartcontractkit/chainlink-common/pkg/codec/encodings/binary" @@ -38,114 +39,336 @@ type ProgramConfig struct { } type MethodConfig struct { - InputModifications commoncodec.ModifiersConfig - EncodedTypeIDL string - DataType any - DecodedTypeName string - ChainSpecificName string - Accounts []Account - LookupTables []LookupTable + InputModifications commoncodec.ModifiersConfig + EncodedTypeIDL string + DataType reflect.Type + DecodedTypeName string + ChainSpecificName string + ReadableLookupTables []ReadableLookupTable + Accounts []Lookup + LookupTables []LookupTable + // Location in the decoded data where the debug ID is stored + DebugIDLocation string } -type Account interface { +type Lookup interface { } type AccountConstant struct { + Name string Address string IsSigner bool IsWritable bool } type AccountLookup struct { + Name string Location string IsSigner bool IsWritable bool } type PDALookup struct { - PublicKey solana.PublicKey - Seeds [][]byte - IsSigner bool - IsWritable bool + Name string + PublicKey Lookup + AddressSeeds []Lookup + ValueSeeds []ValueLookup + IsSigner bool + IsWritable bool +} + +type ValueLookup struct { + Location string } type LookupTable struct { + Name string + Address solana.PublicKey + Identifier Lookup +} + +type ReadableLookupTable struct { + Name string Address solana.PublicKey - Identifier Account - AccountIndices []int + Identifier Lookup + EncodedTypeIDL string + Locations []AccountLookup + DecodedType reflect.Type +} + +func NewSolanaChainWriterService(reader client.Reader, txm txm.Txm, ge fees.Estimator, config ChainWriterConfig) *SolanaChainWriterService { + return &SolanaChainWriterService{ + reader: reader, + txm: txm, + ge: ge, + config: config, + } } -func (s *SolanaChainWriterService) GetAddresses(decoded any, accounts []Account) ([]*solana.AccountMeta, error) { +/* +GetAddresses resolves account addresses from various `Lookup` configurations to build the required `solana.AccountMeta` list +for Solana transactions. It handles constant addresses, dynamic lookups, program-derived addresses (PDAs), and lookup tables. + +### Parameters: +- `ctx`: Context for request lifecycle management. +- `decoded`: Decoded data used for dynamic lookups. +- `accounts`: List of `Lookup` configurations specifying how addresses are derived. +- `readableTableMap`: Map of pre-loaded lookup table addresses. +- `debugID`: Debug identifier for tracing errors. + +### Return: +- A slice of `solana.AccountMeta` containing derived addresses and associated metadata. + +### Account Types: +1. **AccountConstant**: + - A fixed address, provided in Base58 format, converted into a `solana.PublicKey`. + - Example: A pre-defined fee payer or system account. + +2. **AccountLookup**: + - Dynamically derived from decoded data using a specified location path (e.g., `user.walletAddress`). + - If the lookup table is pre-loaded, the address is fetched from `readableTableMap`. + +3. **PDALookup**: + - Generates Program Derived Addresses (PDA) by combining a derived public key with one or more seeds. + - Seeds can be `AddressSeeds` (public keys from the decoded data) or `ValueSeeds` (byte arrays). + - Ensures there is only one public key if multiple seeds are provided. + +### Error Handling: +- Errors are wrapped with the `debugID` for easier tracing. +*/ +// GetAddresses resolves account addresses from various `Lookup` configurations to build the required `solana.AccountMeta` list +// for Solana transactions. +func (s *SolanaChainWriterService) GetAddresses(ctx context.Context, decoded any, accounts []Lookup, readableTableMap map[string][]*solana.AccountMeta, debugID string) ([]*solana.AccountMeta, error) { var addresses []*solana.AccountMeta for _, accountConfig := range accounts { - switch lookupType := accountConfig.(type) { - case AccountConstant: - address, err := solana.PublicKeyFromBase58(lookupType.Address) - if err != nil { - return nil, fmt.Errorf("error getting account from constant: %w", err) - } - addresses = append(addresses, &solana.AccountMeta{ - PublicKey: address, - IsSigner: lookupType.IsSigner, - IsWritable: lookupType.IsWritable, - }) - case AccountLookup: - derivedAddresses, err := GetAddressAtLocation(decoded, lookupType.Location) - if err != nil { - return nil, fmt.Errorf("error getting account from lookup: %w", err) - } - for _, address := range derivedAddresses { - addresses = append(addresses, &solana.AccountMeta{ - PublicKey: address, - IsSigner: lookupType.IsSigner, - IsWritable: lookupType.IsWritable, - }) - } - case PDALookup: - pda, _, err := solana.FindProgramAddress(lookupType.Seeds, lookupType.PublicKey) - if err != nil { - return nil, fmt.Errorf("error finding program address: %w", err) + meta, err := s.getAccountMeta(ctx, decoded, accountConfig, readableTableMap, debugID) + if err != nil { + return nil, err + } + addresses = append(addresses, meta...) + } + return addresses, nil +} + +// getAccountMeta processes a single account configuration and returns the corresponding `solana.AccountMeta` slice. +func (s *SolanaChainWriterService) getAccountMeta(ctx context.Context, decoded any, accountConfig Lookup, readableTableMap map[string][]*solana.AccountMeta, debugID string) ([]*solana.AccountMeta, error) { + switch lookup := accountConfig.(type) { + case AccountConstant: + return s.handleAccountConstant(lookup, debugID) + case AccountLookup: + return s.handleAccountLookup(decoded, lookup, readableTableMap, debugID) + case PDALookup: + return s.handlePDALookup(ctx, decoded, lookup, readableTableMap, debugID) + default: + return nil, errorWithDebugID(fmt.Errorf("unsupported account type: %T", lookup), debugID) + } +} + +// handleAccountConstant processes an `AccountConstant` and returns the corresponding `solana.AccountMeta`. +func (s *SolanaChainWriterService) handleAccountConstant(lookup AccountConstant, debugID string) ([]*solana.AccountMeta, error) { + address, err := solana.PublicKeyFromBase58(lookup.Address) + if err != nil { + return nil, errorWithDebugID(fmt.Errorf("error getting account from constant: %w", err), debugID) + } + return []*solana.AccountMeta{ + { + PublicKey: address, + IsSigner: lookup.IsSigner, + IsWritable: lookup.IsWritable, + }, + }, nil +} + +// handleAccountLookup processes an `AccountLookup` by either fetching from the lookup table or dynamically deriving the address. +func (s *SolanaChainWriterService) handleAccountLookup(decoded any, lookup AccountLookup, readableTableMap map[string][]*solana.AccountMeta, debugID string) ([]*solana.AccountMeta, error) { + if derivedAddresses, ok := readableTableMap[lookup.Name]; ok { + return derivedAddresses, nil + } + + derivedAddresses, err := GetAddressAtLocation(decoded, lookup.Location, debugID) + if err != nil { + return nil, errorWithDebugID(fmt.Errorf("error getting account from lookup: %w", err), debugID) + } + + var metas []*solana.AccountMeta + for _, address := range derivedAddresses { + metas = append(metas, &solana.AccountMeta{ + PublicKey: address, + IsSigner: lookup.IsSigner, + IsWritable: lookup.IsWritable, + }) + } + return metas, nil +} + +// handlePDALookup processes a `PDALookup` by resolving seeds and generating the PDA address. +func (s *SolanaChainWriterService) handlePDALookup(ctx context.Context, decoded any, lookup PDALookup, readableTableMap map[string][]*solana.AccountMeta, debugID string) ([]*solana.AccountMeta, error) { + publicKeys, err := s.GetAddresses(ctx, decoded, []Lookup{lookup.PublicKey}, readableTableMap, debugID) + if err != nil { + return nil, errorWithDebugID(fmt.Errorf("error getting public key for PDALookup: %w", err), debugID) + } + + seeds, err := s.getSeedBytes(ctx, lookup, decoded, readableTableMap, debugID) + if err != nil { + return nil, errorWithDebugID(fmt.Errorf("error getting seeds for PDALookup: %w", err), debugID) + } + + return s.generatePDAs(publicKeys, seeds, lookup, debugID) +} + +// generatePDAs generates program-derived addresses (PDAs) from public keys and seeds. +func (s *SolanaChainWriterService) generatePDAs(publicKeys []*solana.AccountMeta, seeds [][]byte, lookup PDALookup, debugID string) ([]*solana.AccountMeta, error) { + if len(seeds) > 1 && len(publicKeys) > 1 { + return nil, errorWithDebugID(fmt.Errorf("multiple public keys and multiple seeds are not allowed"), debugID) + } + + var addresses []*solana.AccountMeta + for _, publicKeyMeta := range publicKeys { + address, _, err := solana.FindProgramAddress(seeds, publicKeyMeta.PublicKey) + if err != nil { + return nil, errorWithDebugID(fmt.Errorf("error finding program address: %w", err), debugID) + } + addresses = append(addresses, &solana.AccountMeta{ + PublicKey: address, + IsSigner: lookup.IsSigner, + IsWritable: lookup.IsWritable, + }) + } + return addresses, nil +} + +func (s *SolanaChainWriterService) getReadableTableMap(ctx context.Context, decoded any, lookupTables []ReadableLookupTable, debugID string) ([]*solana.AccountMeta, map[string][]*solana.AccountMeta, error) { + var addresses []*solana.AccountMeta + var addressMap = make(map[string][]*solana.AccountMeta) + for _, lookup := range lookupTables { + lookupTableAddresses, err := s.LoadTable(lookup, ctx, s.reader, addressMap, debugID) + if err != nil { + return nil, nil, errorWithDebugID(fmt.Errorf("error loading lookup table: %w", err), debugID) + } + for name, addressList := range lookupTableAddresses { + for _, address := range addressList { + addresses = append(addresses, address) } - addresses = append(addresses, &solana.AccountMeta{ - PublicKey: pda, - IsSigner: lookupType.IsSigner, - IsWritable: lookupType.IsWritable, - }) - default: - return nil, fmt.Errorf("unsupported account type: %T", lookupType) + addressMap[name] = addressList + } + } + return addresses, addressMap, nil +} + +// getSeedBytes extracts the seeds for the PDALookup. +// It handles both AddressSeeds (which are public keys) and ValueSeeds (which are byte arrays from decoded data). +func (s *SolanaChainWriterService) getSeedBytes(ctx context.Context, lookup PDALookup, decoded any, readableTableMap map[string][]*solana.AccountMeta, debugID string) ([][]byte, error) { + var seedBytes [][]byte + + // Process AddressSeeds first (e.g., public keys) + for _, seed := range lookup.AddressSeeds { + // Get the address(es) at the seed location + seedAddresses, err := s.GetAddresses(ctx, decoded, []Lookup{seed}, readableTableMap, debugID) + if err != nil { + return nil, errorWithDebugID(fmt.Errorf("error getting address seed: %w", err), debugID) + } + + // Add each address seed as bytes + for _, address := range seedAddresses { + seedBytes = append(seedBytes, address.PublicKey.Bytes()) + } + } + + // Process ValueSeeds (e.g., raw byte values found in decoded data) + for _, valueSeed := range lookup.ValueSeeds { + // Get the byte array value at the seed location + values, err := GetValueAtLocation(decoded, valueSeed.Location) + if err != nil { + return nil, errorWithDebugID(fmt.Errorf("error getting value seed: %w", err), debugID) + } + + // Add each value seed (which is a byte array) + seedBytes = append(seedBytes, values...) + } + + return seedBytes, nil +} + +// LoadTable reads the lookup table from the Solana chain, decodes it into the specified type, and returns a slice of addresses. +func (s *SolanaChainWriterService) LoadTable(rlt ReadableLookupTable, ctx context.Context, reader client.Reader, readableTableMap map[string][]*solana.AccountMeta, debugID string) (map[string][]*solana.AccountMeta, error) { + // Fetch the account data using client.Reader.GetAccountInfoWithOpts + accountInfo, err := reader.GetAccountInfoWithOpts(ctx, rlt.Address, &rpc.GetAccountInfoOpts{ + Encoding: "base64", // or "jsonParsed" if needed + Commitment: rpc.CommitmentConfirmed, + }) + if err != nil { + return nil, errorWithDebugID(fmt.Errorf("failed to get account info: %w", err), debugID) + } + if accountInfo == nil || accountInfo.Value == nil { + return nil, errorWithDebugID(fmt.Errorf("no data found for account: %s", rlt.Address.String()), debugID) + } + + // Decode the table data using the codec/EncodedTypeIDL + decodedData, err := rlt.DecodeTableData(accountInfo.Value.Data.GetBinary(), debugID) + if err != nil { + return nil, errorWithDebugID(fmt.Errorf("failed to decode table data: %w", err), debugID) + } + + // Convert the decoded entries into solana.PublicKey and return them + var addresses map[string][]*solana.AccountMeta + for _, location := range rlt.Locations { + derivedAddresses, err := s.GetAddresses(ctx, decodedData, []Lookup{location}, readableTableMap, debugID) + if err != nil { + return nil, errorWithDebugID(fmt.Errorf("error getting addresses from decoded data: %w", err), debugID) } + addresses[fmt.Sprintf("%s.%s", rlt.Name, location.Name)] = derivedAddresses } + return addresses, nil } -func (s *SolanaChainWriterService) GetLookupTables(decoded any, accounts []*solana.AccountMeta, lookupTables []LookupTable) (map[solana.PublicKey]solana.PublicKeySlice, error) { +// DecodeTableData decodes the raw table data using the EncodedTypeIDL and the specified DecodedType. +func (rlt *ReadableLookupTable) DecodeTableData(data []byte, debugID string) (any, error) { + var idl codec.IDL + err := json.Unmarshal([]byte(rlt.EncodedTypeIDL), &idl) + if err != nil { + return nil, errorWithDebugID(fmt.Errorf("error unmarshalling IDL: %w", err), debugID) + } + + cwCodec, err := codec.NewIDLAccountCodec(idl, binary.LittleEndian()) + if err != nil { + return nil, errorWithDebugID(fmt.Errorf("error creating new IDLAccountCodec: %w", err), debugID) + } + + decoded := reflect.New(rlt.DecodedType).Interface() + + err = cwCodec.Decode(nil, data, decoded, "") + if err != nil { + return nil, errorWithDebugID(fmt.Errorf("error decoding table data: %w", err), debugID) + } + + return decoded, nil +} + +func (s *SolanaChainWriterService) GetLookupTables(ctx context.Context, decoded any, lookupTables []LookupTable, readableTableMap map[string][]*solana.AccountMeta, debugID string) (map[solana.PublicKey]solana.PublicKeySlice, error) { tables := make(map[solana.PublicKey]solana.PublicKeySlice) for _, lookupTable := range lookupTables { + // Prevent nested lookup tables. if reflect.TypeOf(lookupTable.Identifier) == reflect.TypeOf(LookupTable{}) { - return nil, fmt.Errorf("nested lookup tables are not supported") + return nil, errorWithDebugID(fmt.Errorf("nested lookup tables are not supported"), debugID) } - // ids, err := s.GetAddresses(decoded, []Account{lookupTable.Identifier}) - // if err != nil { - // return nil, fmt.Errorf("error getting account from lookup table: %w", err) - // } - addresses := make(solana.PublicKeySlice, len(lookupTable.AccountIndices)) - for i, index := range lookupTable.AccountIndices { - addresses[i] = accounts[index].PublicKey + + // Get the public keys for the lookup table's identifier (can be one or more). + ids, err := s.GetAddresses(ctx, decoded, []Lookup{lookupTable.Identifier}, readableTableMap, debugID) + if err != nil { + return nil, errorWithDebugID(fmt.Errorf("error getting accounts from lookup table: %w", err), debugID) + } + + // Convert the ids to a solana.PublicKeySlice and add to the lookup table map. + addresses := make(solana.PublicKeySlice, len(ids)) + for i, accountMeta := range ids { + addresses[i] = accountMeta.PublicKey } tables[lookupTable.Address] = addresses } return tables, nil } -func NewSolanaChainWriterService(reader client.Reader, txm txm.Txm, ge fees.Estimator, config ChainWriterConfig) *SolanaChainWriterService { - return &SolanaChainWriterService{ - reader: reader, - txm: txm, - ge: ge, - config: config, - } -} - func (s *SolanaChainWriterService) SubmitTransaction(ctx context.Context, contractName, method string, args any, transactionID string, toAddress string, meta *types.TxMeta, value *big.Int) error { programConfig := s.config.Programs[contractName] methodConfig := programConfig.Methods[method] @@ -167,23 +390,35 @@ func (s *SolanaChainWriterService) SubmitTransaction(ctx context.Context, contra } // Create an instance of the type defined by methodConfig.DataType - decoded := reflect.New(reflect.TypeOf(methodConfig.DataType)).Interface() + decoded := reflect.New(methodConfig.DataType).Interface() err = cwCodec.Decode(ctx, data, decoded, methodConfig.DecodedTypeName) - accounts, err := s.GetAddresses(decoded, methodConfig.Accounts) + debugID := "" + if methodConfig.DebugIDLocation != "" { + debugID, err = GetDebugIDAtLocation(decoded, methodConfig.DebugIDLocation) + if err != nil { + return errorWithDebugID(fmt.Errorf("error getting debug ID from decoded data: %w", err), debugID) + } + } + readableTableAccounts, readableTableMap, err := s.getReadableTableMap(ctx, decoded, methodConfig.ReadableLookupTables, debugID) + if err != nil { + return errorWithDebugID(fmt.Errorf("error getting readable table map: %w", err), debugID) + } + accounts, err := s.GetAddresses(ctx, decoded, methodConfig.Accounts, readableTableMap, debugID) + accounts = append(accounts, readableTableAccounts...) if err != nil { - return fmt.Errorf("error getting addresses from decoded data: %w", err) + return errorWithDebugID(fmt.Errorf("error getting addresses from decoded data: %w", err), debugID) } - lookupTables, err := s.GetLookupTables(decoded, accounts, methodConfig.LookupTables) + lookupTables, err := s.GetLookupTables(ctx, decoded, methodConfig.LookupTables, readableTableMap, debugID) if err != nil { - return fmt.Errorf("error getting lookup tables from decoded data: %w", err) + return errorWithDebugID(fmt.Errorf("error getting lookup tables from decoded data: %w", err), debugID) } blockhash, err := s.reader.LatestBlockhash(ctx) programId, err := solana.PublicKeyFromBase58(contractName) if err != nil { - return fmt.Errorf("Error getting programId: %w", err) + return errorWithDebugID(fmt.Errorf("Error getting programId: %w", err), debugID) } // This isn't a real method, TBD how we will get this @@ -198,11 +433,11 @@ func (s *SolanaChainWriterService) SubmitTransaction(ctx context.Context, contra solana.TransactionAddressTables(lookupTables), ) if err != nil { - return fmt.Errorf("error creating new transaction: %w", err) + return errorWithDebugID(fmt.Errorf("error creating new transaction: %w", err), debugID) } if err = s.txm.Enqueue(ctx, accounts[0].PublicKey.String(), tx); err != nil { - return fmt.Errorf("error on sending trasnaction to TXM: %w", err) + return errorWithDebugID(fmt.Errorf("error on sending trasnaction to TXM: %w", err), debugID) } return nil } diff --git a/pkg/solana/chainwriter/chain_writer_test.go b/pkg/solana/chainwriter/chain_writer_test.go new file mode 100644 index 000000000..e7c304c65 --- /dev/null +++ b/pkg/solana/chainwriter/chain_writer_test.go @@ -0,0 +1,125 @@ +package chainwriter_test + +import ( + "context" + "testing" + + "github.com/gagliardetto/solana-go" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/smartcontractkit/chainlink-solana/pkg/solana/chainwriter" + + clientmocks "github.com/smartcontractkit/chainlink-solana/pkg/solana/client/mocks" + gemocks "github.com/smartcontractkit/chainlink-solana/pkg/solana/fees/mocks" + "github.com/smartcontractkit/chainlink-solana/pkg/solana/txm" +) + +type TestStruct struct { + Messages []Message +} + +type Message struct { + TokenAmounts []TokenAmount +} + +type TokenAmount struct { + SourceTokenAddress []byte + DestTokenAddress []byte +} + +func TestGetAddresses(t *testing.T) { + ctx := context.TODO() + + // Create a mock for the Reader interface + readerMock := clientmocks.NewReaderWriter(t) + txmMock := txm.Txm{} + geMock := gemocks.NewEstimator(t) + + chainWriterConfig := chainwriter.ChainWriterConfig{ + Programs: map[string]chainwriter.ProgramConfig{ + + } + + // Create a test instance of SolanaChainWriterService + service := chainwriter.NewSolanaChainWriterService(readerMock, txmMock, geMock, chainWriterConfig) + + t.Run("success with AccountConstant", func(t *testing.T) { + accounts := []chainwriter.Lookup{ + chainwriter.AccountConstant{ + Name: "test-account", + Address: "4Nn9dsYBcSTzRbK9hg9kzCUdrCSkMZq1UR6Vw1Tkaf6M", + IsSigner: true, + IsWritable: false, + }, + } + + // Call GetAddresses with the constant account + addresses, err := service.GetAddresses(ctx, nil, accounts, nil, "test-debug-id") + require.NoError(t, err) + require.Len(t, addresses, 1) + require.Equal(t, addresses[0].PublicKey.String(), "4Nn9dsYBcSTzRbK9hg9kzCUdrCSkMZq1UR6Vw1Tkaf6M") + require.True(t, addresses[0].IsSigner) + require.False(t, addresses[0].IsWritable) + }) + + t.Run("success with AccountLookup", func(t *testing.T) { + accounts := []chainwriter.Lookup{ + chainwriter.AccountLookup{ + Name: "test-account", + Location: "Messages.TokenAmounts.SourceTokenAddress", + IsSigner: true, + IsWritable: false, + }, + chainwriter.AccountLookup{ + Name: "test-account", + Location: "Messages.TokenAmounts.DestTokenAddress", + IsSigner: true, + IsWritable: false, + }, + } + + // Create a test struct with the expected address + addresses := make([][]byte, 8) + for i := 0; i < 8; i++ { + privKey, err := solana.NewRandomPrivateKey() + require.NoError(t, err) + addresses[i] = privKey.PublicKey().Bytes() + } + + exampleDecoded := TestStruct{ + Messages: []Message{ + { + TokenAmounts: []TokenAmount{ + {addresses[0], addresses[1]}, + {addresses[2], addresses[3]}, + }, + }, + { + TokenAmounts: []TokenAmount{ + {addresses[4], addresses[5]}, + {addresses[6], addresses[7]}, + }, + }, + }, + } + // Call GetAddresses with the lookup account + derivedAddresses, err := service.GetAddresses(ctx, exampleDecoded, accounts, nil, "test-debug-id") + + // Create a map of the expected addresses for fast lookup + expectedAddresses := make(map[string]bool) + for _, addr := range addresses { + expectedAddresses[string(addr)] = true + } + + // Verify that each derived address matches an expected address + for _, derivedAddr := range derivedAddresses { + derivedBytes := derivedAddr.PublicKey.Bytes() + assert.True(t, expectedAddresses[string(derivedBytes)], "Address not found in expected list") + } + + require.NoError(t, err) + require.Len(t, derivedAddresses, 8) + }) + +} diff --git a/pkg/solana/chainwriter/helpers.go b/pkg/solana/chainwriter/helpers.go index a5f9c1518..369ca69fe 100644 --- a/pkg/solana/chainwriter/helpers.go +++ b/pkg/solana/chainwriter/helpers.go @@ -10,7 +10,7 @@ import ( ) // GetAddressAtLocation parses through nested types and arrays to find all address locations. -func GetAddressAtLocation(decoded any, location string) ([]solana.PublicKey, error) { +func GetAddressAtLocation(decoded any, location string, debugID string) ([]solana.PublicKey, error) { var addresses []solana.PublicKey path := strings.Split(location, ".") @@ -24,13 +24,52 @@ func GetAddressAtLocation(decoded any, location string) ([]solana.PublicKey, err if byteArray, ok := value.([]byte); ok { addresses = append(addresses, solana.PublicKeyFromBytes(byteArray)) } else { - return nil, fmt.Errorf("invalid address format at path: %s", location) + return nil, errorWithDebugID(fmt.Errorf("invalid address format at path: %s", location), debugID) } } return addresses, nil } +func GetDebugIDAtLocation(decoded any, location string) (string, error) { + debugIDList, err := GetValueAtLocation(decoded, location) + if err != nil { + return "", err + } + + // there should only be one debug ID, others will be ignored. + debugID := string(debugIDList[0]) + + return debugID, nil +} + +func GetValueAtLocation(decoded any, location string) ([][]byte, error) { + path := strings.Split(location, ".") + + valueList, err := traversePath(decoded, path) + if err != nil { + return nil, err + } + + var values [][]byte + for _, value := range valueList { + if byteArray, ok := value.([]byte); ok { + values = append(values, byteArray) + } else { + return nil, fmt.Errorf("invalid value format at path: %s", location) + } + } + + return values, nil +} + +func errorWithDebugID(err error, debugID string) error { + if debugID == "" { + return err + } + return fmt.Errorf("Debug ID: %s: Error: %s", debugID, err) +} + // traversePath recursively traverses the given structure based on the provided path. func traversePath(data any, path []string) ([]any, error) { if len(path) == 0 { diff --git a/pkg/solana/chainwriter/helpers_test.go b/pkg/solana/chainwriter/helpers_test.go deleted file mode 100644 index 45a9cdb6a..000000000 --- a/pkg/solana/chainwriter/helpers_test.go +++ /dev/null @@ -1,74 +0,0 @@ -package chainwriter_test - -import ( - "fmt" - "testing" - - "github.com/gagliardetto/solana-go" - "github.com/smartcontractkit/chainlink-solana/pkg/solana/chainwriter" - "github.com/stretchr/testify/assert" -) - -type TestStruct struct { - Messages []Message -} - -type Message struct { - TokenAmounts []TokenAmount -} - -type TokenAmount struct { - SourceTokenAddress []byte - DestTokenAddress []byte -} - -func TestHelperLookupFunction(t *testing.T) { - addresses := make([][]byte, 8) - for i := 0; i < 8; i++ { - privKey, err := solana.NewRandomPrivateKey() - assert.NoError(t, err) - addresses[i] = privKey.PublicKey().Bytes() - } - - exampleDecoded := TestStruct{ - Messages: []Message{ - { - TokenAmounts: []TokenAmount{ - {addresses[0], addresses[1]}, - {addresses[2], addresses[3]}, - }, - }, - { - TokenAmounts: []TokenAmount{ - {addresses[4], addresses[5]}, - {addresses[6], addresses[7]}, - }, - }, - }, - } - - addressLocations := []string{ - "Messages.TokenAmounts.SourceTokenAddress", - "Messages.TokenAmounts.DestTokenAddress", - } - derivedAddresses := make([]solana.PublicKey, 0) - for _, location := range addressLocations { - addr, err := chainwriter.GetAddressAtLocation(exampleDecoded, location) - assert.NoError(t, err) - fmt.Println(len(addr)) - derivedAddresses = append(derivedAddresses, addr...) - } - assert.Equal(t, 8, len(derivedAddresses)) - - // Create a map of the expected addresses for fast lookup - expectedAddresses := make(map[string]bool) - for _, addr := range addresses { - expectedAddresses[string(addr)] = true - } - - // Verify that each derived address matches an expected address - for _, derivedAddr := range derivedAddresses { - derivedBytes := derivedAddr.Bytes() - assert.True(t, expectedAddresses[string(derivedBytes)], "Address not found in expected list") - } -} From fa85301f34d1c7e778de7ec84bb6f44fe68a77b7 Mon Sep 17 00:00:00 2001 From: Silas Lenihan Date: Wed, 30 Oct 2024 11:11:33 -0400 Subject: [PATCH 05/22] Refactored lookup tables --- pkg/solana/chainwriter/chain_writer.go | 269 ++++++++++++++----------- 1 file changed, 150 insertions(+), 119 deletions(-) diff --git a/pkg/solana/chainwriter/chain_writer.go b/pkg/solana/chainwriter/chain_writer.go index 97a180c77..bdd413bfa 100644 --- a/pkg/solana/chainwriter/chain_writer.go +++ b/pkg/solana/chainwriter/chain_writer.go @@ -39,14 +39,14 @@ type ProgramConfig struct { } type MethodConfig struct { - InputModifications commoncodec.ModifiersConfig - EncodedTypeIDL string - DataType reflect.Type - DecodedTypeName string - ChainSpecificName string - ReadableLookupTables []ReadableLookupTable - Accounts []Lookup - LookupTables []LookupTable + InputModifications commoncodec.ModifiersConfig + EncodedTypeIDL string + DataType reflect.Type + DecodedTypeName string + ChainSpecificName string + DerivedLookupTables []DerivedLookupTable + Accounts []Lookup + LookupTables []string // Location in the decoded data where the debug ID is stored DebugIDLocation string } @@ -69,27 +69,24 @@ type AccountLookup struct { } type PDALookup struct { - Name string - PublicKey Lookup + Name string + // The public key of the PDA to be combined with seeds. If there are multiple PublicKeys + // there will be multiple PDAs generated by combining each PublicKey with the seeds. + PublicKey Lookup + // Seeds to be derived from an additional lookup AddressSeeds []Lookup - ValueSeeds []ValueLookup - IsSigner bool - IsWritable bool + // Seeds to be derived from a value in the decoded data + ValueSeeds []ValueLookup + IsSigner bool + IsWritable bool } type ValueLookup struct { Location string } -type LookupTable struct { - Name string - Address solana.PublicKey - Identifier Lookup -} - -type ReadableLookupTable struct { +type DerivedLookupTable struct { Name string - Address solana.PublicKey Identifier Lookup EncodedTypeIDL string Locations []AccountLookup @@ -113,7 +110,7 @@ for Solana transactions. It handles constant addresses, dynamic lookups, program - `ctx`: Context for request lifecycle management. - `decoded`: Decoded data used for dynamic lookups. - `accounts`: List of `Lookup` configurations specifying how addresses are derived. -- `readableTableMap`: Map of pre-loaded lookup table addresses. +- `derivedTableMap`: Map of pre-loaded lookup table addresses. - `debugID`: Debug identifier for tracing errors. ### Return: @@ -126,7 +123,7 @@ for Solana transactions. It handles constant addresses, dynamic lookups, program 2. **AccountLookup**: - Dynamically derived from decoded data using a specified location path (e.g., `user.walletAddress`). - - If the lookup table is pre-loaded, the address is fetched from `readableTableMap`. + - If the lookup table is pre-loaded, the address is fetched from `derivedTableMap`. 3. **PDALookup**: - Generates Program Derived Addresses (PDA) by combining a derived public key with one or more seeds. @@ -138,10 +135,10 @@ for Solana transactions. It handles constant addresses, dynamic lookups, program */ // GetAddresses resolves account addresses from various `Lookup` configurations to build the required `solana.AccountMeta` list // for Solana transactions. -func (s *SolanaChainWriterService) GetAddresses(ctx context.Context, decoded any, accounts []Lookup, readableTableMap map[string][]*solana.AccountMeta, debugID string) ([]*solana.AccountMeta, error) { +func (s *SolanaChainWriterService) GetAddresses(ctx context.Context, decoded any, accounts []Lookup, derivedTableMap map[string][]*solana.AccountMeta, debugID string) ([]*solana.AccountMeta, error) { var addresses []*solana.AccountMeta for _, accountConfig := range accounts { - meta, err := s.getAccountMeta(ctx, decoded, accountConfig, readableTableMap, debugID) + meta, err := s.getAccountMeta(ctx, decoded, accountConfig, derivedTableMap, debugID) if err != nil { return nil, err } @@ -151,14 +148,14 @@ func (s *SolanaChainWriterService) GetAddresses(ctx context.Context, decoded any } // getAccountMeta processes a single account configuration and returns the corresponding `solana.AccountMeta` slice. -func (s *SolanaChainWriterService) getAccountMeta(ctx context.Context, decoded any, accountConfig Lookup, readableTableMap map[string][]*solana.AccountMeta, debugID string) ([]*solana.AccountMeta, error) { +func (s *SolanaChainWriterService) getAccountMeta(ctx context.Context, decoded any, accountConfig Lookup, derivedTableMap map[string][]*solana.AccountMeta, debugID string) ([]*solana.AccountMeta, error) { switch lookup := accountConfig.(type) { case AccountConstant: return s.handleAccountConstant(lookup, debugID) case AccountLookup: - return s.handleAccountLookup(decoded, lookup, readableTableMap, debugID) + return s.handleAccountLookup(decoded, lookup, derivedTableMap, debugID) case PDALookup: - return s.handlePDALookup(ctx, decoded, lookup, readableTableMap, debugID) + return s.handlePDALookup(ctx, decoded, lookup, derivedTableMap, debugID) default: return nil, errorWithDebugID(fmt.Errorf("unsupported account type: %T", lookup), debugID) } @@ -180,8 +177,8 @@ func (s *SolanaChainWriterService) handleAccountConstant(lookup AccountConstant, } // handleAccountLookup processes an `AccountLookup` by either fetching from the lookup table or dynamically deriving the address. -func (s *SolanaChainWriterService) handleAccountLookup(decoded any, lookup AccountLookup, readableTableMap map[string][]*solana.AccountMeta, debugID string) ([]*solana.AccountMeta, error) { - if derivedAddresses, ok := readableTableMap[lookup.Name]; ok { +func (s *SolanaChainWriterService) handleAccountLookup(decoded any, lookup AccountLookup, derivedTableMap map[string][]*solana.AccountMeta, debugID string) ([]*solana.AccountMeta, error) { + if derivedAddresses, ok := derivedTableMap[lookup.Name]; ok { return derivedAddresses, nil } @@ -202,13 +199,13 @@ func (s *SolanaChainWriterService) handleAccountLookup(decoded any, lookup Accou } // handlePDALookup processes a `PDALookup` by resolving seeds and generating the PDA address. -func (s *SolanaChainWriterService) handlePDALookup(ctx context.Context, decoded any, lookup PDALookup, readableTableMap map[string][]*solana.AccountMeta, debugID string) ([]*solana.AccountMeta, error) { - publicKeys, err := s.GetAddresses(ctx, decoded, []Lookup{lookup.PublicKey}, readableTableMap, debugID) +func (s *SolanaChainWriterService) handlePDALookup(ctx context.Context, decoded any, lookup PDALookup, derivedTableMap map[string][]*solana.AccountMeta, debugID string) ([]*solana.AccountMeta, error) { + publicKeys, err := s.GetAddresses(ctx, decoded, []Lookup{lookup.PublicKey}, derivedTableMap, debugID) if err != nil { return nil, errorWithDebugID(fmt.Errorf("error getting public key for PDALookup: %w", err), debugID) } - seeds, err := s.getSeedBytes(ctx, lookup, decoded, readableTableMap, debugID) + seeds, err := s.getSeedBytes(ctx, lookup, decoded, derivedTableMap, debugID) if err != nil { return nil, errorWithDebugID(fmt.Errorf("error getting seeds for PDALookup: %w", err), debugID) } @@ -216,54 +213,15 @@ func (s *SolanaChainWriterService) handlePDALookup(ctx context.Context, decoded return s.generatePDAs(publicKeys, seeds, lookup, debugID) } -// generatePDAs generates program-derived addresses (PDAs) from public keys and seeds. -func (s *SolanaChainWriterService) generatePDAs(publicKeys []*solana.AccountMeta, seeds [][]byte, lookup PDALookup, debugID string) ([]*solana.AccountMeta, error) { - if len(seeds) > 1 && len(publicKeys) > 1 { - return nil, errorWithDebugID(fmt.Errorf("multiple public keys and multiple seeds are not allowed"), debugID) - } - - var addresses []*solana.AccountMeta - for _, publicKeyMeta := range publicKeys { - address, _, err := solana.FindProgramAddress(seeds, publicKeyMeta.PublicKey) - if err != nil { - return nil, errorWithDebugID(fmt.Errorf("error finding program address: %w", err), debugID) - } - addresses = append(addresses, &solana.AccountMeta{ - PublicKey: address, - IsSigner: lookup.IsSigner, - IsWritable: lookup.IsWritable, - }) - } - return addresses, nil -} - -func (s *SolanaChainWriterService) getReadableTableMap(ctx context.Context, decoded any, lookupTables []ReadableLookupTable, debugID string) ([]*solana.AccountMeta, map[string][]*solana.AccountMeta, error) { - var addresses []*solana.AccountMeta - var addressMap = make(map[string][]*solana.AccountMeta) - for _, lookup := range lookupTables { - lookupTableAddresses, err := s.LoadTable(lookup, ctx, s.reader, addressMap, debugID) - if err != nil { - return nil, nil, errorWithDebugID(fmt.Errorf("error loading lookup table: %w", err), debugID) - } - for name, addressList := range lookupTableAddresses { - for _, address := range addressList { - addresses = append(addresses, address) - } - addressMap[name] = addressList - } - } - return addresses, addressMap, nil -} - // getSeedBytes extracts the seeds for the PDALookup. // It handles both AddressSeeds (which are public keys) and ValueSeeds (which are byte arrays from decoded data). -func (s *SolanaChainWriterService) getSeedBytes(ctx context.Context, lookup PDALookup, decoded any, readableTableMap map[string][]*solana.AccountMeta, debugID string) ([][]byte, error) { +func (s *SolanaChainWriterService) getSeedBytes(ctx context.Context, lookup PDALookup, decoded any, derivedTableMap map[string][]*solana.AccountMeta, debugID string) ([][]byte, error) { var seedBytes [][]byte // Process AddressSeeds first (e.g., public keys) for _, seed := range lookup.AddressSeeds { // Get the address(es) at the seed location - seedAddresses, err := s.GetAddresses(ctx, decoded, []Lookup{seed}, readableTableMap, debugID) + seedAddresses, err := s.GetAddresses(ctx, decoded, []Lookup{seed}, derivedTableMap, debugID) if err != nil { return nil, errorWithDebugID(fmt.Errorf("error getting address seed: %w", err), debugID) } @@ -289,41 +247,90 @@ func (s *SolanaChainWriterService) getSeedBytes(ctx context.Context, lookup PDAL return seedBytes, nil } -// LoadTable reads the lookup table from the Solana chain, decodes it into the specified type, and returns a slice of addresses. -func (s *SolanaChainWriterService) LoadTable(rlt ReadableLookupTable, ctx context.Context, reader client.Reader, readableTableMap map[string][]*solana.AccountMeta, debugID string) (map[string][]*solana.AccountMeta, error) { - // Fetch the account data using client.Reader.GetAccountInfoWithOpts - accountInfo, err := reader.GetAccountInfoWithOpts(ctx, rlt.Address, &rpc.GetAccountInfoOpts{ - Encoding: "base64", // or "jsonParsed" if needed - Commitment: rpc.CommitmentConfirmed, - }) - if err != nil { - return nil, errorWithDebugID(fmt.Errorf("failed to get account info: %w", err), debugID) +// generatePDAs generates program-derived addresses (PDAs) from public keys and seeds. +func (s *SolanaChainWriterService) generatePDAs(publicKeys []*solana.AccountMeta, seeds [][]byte, lookup PDALookup, debugID string) ([]*solana.AccountMeta, error) { + if len(seeds) > 1 && len(publicKeys) > 1 { + return nil, errorWithDebugID(fmt.Errorf("multiple public keys and multiple seeds are not allowed"), debugID) } - if accountInfo == nil || accountInfo.Value == nil { - return nil, errorWithDebugID(fmt.Errorf("no data found for account: %s", rlt.Address.String()), debugID) + + var addresses []*solana.AccountMeta + for _, publicKeyMeta := range publicKeys { + address, _, err := solana.FindProgramAddress(seeds, publicKeyMeta.PublicKey) + if err != nil { + return nil, errorWithDebugID(fmt.Errorf("error finding program address: %w", err), debugID) + } + addresses = append(addresses, &solana.AccountMeta{ + PublicKey: address, + IsSigner: lookup.IsSigner, + IsWritable: lookup.IsWritable, + }) } + return addresses, nil +} + +func (s *SolanaChainWriterService) getDerivedTableMap(ctx context.Context, lookupTables []DerivedLookupTable, debugID string) ([]*solana.AccountMeta, []string, map[string][]*solana.AccountMeta, error) { + var accounts []*solana.AccountMeta + var lookupTableAddresses []string + var addressMap = make(map[string][]*solana.AccountMeta) + + for _, lookup := range lookupTables { + lookupTableMap, tableAddresses, err := s.LoadTable(lookup, ctx, s.reader, addressMap, debugID) + if err != nil { + return nil, nil, nil, errorWithDebugID(fmt.Errorf("error loading lookup table: %w", err), debugID) + } + for _, address := range tableAddresses { + lookupTableAddresses = append(lookupTableAddresses, address.PublicKey.String()) + } + for name, addressList := range lookupTableMap { + for _, address := range addressList { + accounts = append(accounts, address) + } + addressMap[name] = addressList + } + } + return accounts, lookupTableAddresses, addressMap, nil +} - // Decode the table data using the codec/EncodedTypeIDL - decodedData, err := rlt.DecodeTableData(accountInfo.Value.Data.GetBinary(), debugID) +// LoadTable fetches addresses specified by Identifier, loads data for each, and decodes it into solana.PublicKey slices. +func (s *SolanaChainWriterService) LoadTable(rlt DerivedLookupTable, ctx context.Context, reader client.Reader, derivedTableMap map[string][]*solana.AccountMeta, debugID string) (map[string][]*solana.AccountMeta, []*solana.AccountMeta, error) { + // Use GetAddresses to resolve all addresses specified by Identifier. + lookupTableAddresses, err := s.GetAddresses(ctx, nil, []Lookup{rlt.Identifier}, derivedTableMap, debugID) if err != nil { - return nil, errorWithDebugID(fmt.Errorf("failed to decode table data: %w", err), debugID) + return nil, nil, errorWithDebugID(fmt.Errorf("error resolving addresses for lookup table: %w", err), debugID) } - // Convert the decoded entries into solana.PublicKey and return them - var addresses map[string][]*solana.AccountMeta - for _, location := range rlt.Locations { - derivedAddresses, err := s.GetAddresses(ctx, decodedData, []Lookup{location}, readableTableMap, debugID) + // Map to store address metadata grouped by location. + resultMap := make(map[string][]*solana.AccountMeta) + for _, addressMeta := range lookupTableAddresses { + // Fetch account data for each resolved address. + accountInfo, err := reader.GetAccountInfoWithOpts(ctx, addressMeta.PublicKey, &rpc.GetAccountInfoOpts{ + Encoding: "base64", + Commitment: rpc.CommitmentConfirmed, + }) + if err != nil || accountInfo == nil || accountInfo.Value == nil { + return nil, nil, errorWithDebugID(fmt.Errorf("error fetching account info for address %s: %w", addressMeta.PublicKey.String(), err), debugID) + } + + // Decode account data based on the IDL specified in EncodedTypeIDL. + decodedData, err := rlt.decodeAccountData(accountInfo.Value.Data.GetBinary(), debugID) if err != nil { - return nil, errorWithDebugID(fmt.Errorf("error getting addresses from decoded data: %w", err), debugID) + return nil, nil, errorWithDebugID(fmt.Errorf("error decoding data for address %s: %w", addressMeta.PublicKey.String(), err), debugID) } - addresses[fmt.Sprintf("%s.%s", rlt.Name, location.Name)] = derivedAddresses - } - return addresses, nil + // Get derived addresses from the decoded data for each location specified. + for _, location := range rlt.Locations { + derivedAddresses, err := s.GetAddresses(ctx, decodedData, []Lookup{location}, derivedTableMap, debugID) + if err != nil { + return nil, nil, errorWithDebugID(fmt.Errorf("error resolving derived addresses: %w", err), debugID) + } + resultMap[fmt.Sprintf("%s.%s", rlt.Name, location.Name)] = derivedAddresses + } + } + return resultMap, lookupTableAddresses, nil } -// DecodeTableData decodes the raw table data using the EncodedTypeIDL and the specified DecodedType. -func (rlt *ReadableLookupTable) DecodeTableData(data []byte, debugID string) (any, error) { +// Decode account data for the DerivedLookupTable based on its EncodedTypeIDL. +func (rlt *DerivedLookupTable) decodeAccountData(data []byte, debugID string) (any, error) { var idl codec.IDL err := json.Unmarshal([]byte(rlt.EncodedTypeIDL), &idl) if err != nil { @@ -332,43 +339,67 @@ func (rlt *ReadableLookupTable) DecodeTableData(data []byte, debugID string) (an cwCodec, err := codec.NewIDLAccountCodec(idl, binary.LittleEndian()) if err != nil { - return nil, errorWithDebugID(fmt.Errorf("error creating new IDLAccountCodec: %w", err), debugID) + return nil, errorWithDebugID(fmt.Errorf("error creating IDLAccountCodec: %w", err), debugID) } decoded := reflect.New(rlt.DecodedType).Interface() - err = cwCodec.Decode(nil, data, decoded, "") if err != nil { - return nil, errorWithDebugID(fmt.Errorf("error decoding table data: %w", err), debugID) + return nil, errorWithDebugID(fmt.Errorf("error decoding account data: %w", err), debugID) } - return decoded, nil } -func (s *SolanaChainWriterService) GetLookupTables(ctx context.Context, decoded any, lookupTables []LookupTable, readableTableMap map[string][]*solana.AccountMeta, debugID string) (map[solana.PublicKey]solana.PublicKeySlice, error) { +func (s *SolanaChainWriterService) GetLookupTables(ctx context.Context, lookupTables []string, debugID string) (map[solana.PublicKey]solana.PublicKeySlice, error) { tables := make(map[solana.PublicKey]solana.PublicKeySlice) - for _, lookupTable := range lookupTables { - // Prevent nested lookup tables. - if reflect.TypeOf(lookupTable.Identifier) == reflect.TypeOf(LookupTable{}) { - return nil, errorWithDebugID(fmt.Errorf("nested lookup tables are not supported"), debugID) + + for _, addressStr := range lookupTables { + // Convert the string address to solana.PublicKey + tableAddress, err := solana.PublicKeyFromBase58(addressStr) + if err != nil { + return nil, errorWithDebugID(fmt.Errorf("invalid lookup table address: %s, error: %w", addressStr, err), debugID) } - // Get the public keys for the lookup table's identifier (can be one or more). - ids, err := s.GetAddresses(ctx, decoded, []Lookup{lookupTable.Identifier}, readableTableMap, debugID) + // Fetch the lookup table data from the blockchain + accountInfo, err := s.reader.GetAccountInfoWithOpts(ctx, tableAddress, &rpc.GetAccountInfoOpts{ + Encoding: "base64", + Commitment: rpc.CommitmentConfirmed, + }) if err != nil { - return nil, errorWithDebugID(fmt.Errorf("error getting accounts from lookup table: %w", err), debugID) + return nil, errorWithDebugID(fmt.Errorf("error fetching account info for lookup table %s: %w", addressStr, err), debugID) + } + if accountInfo == nil || accountInfo.Value == nil { + return nil, errorWithDebugID(fmt.Errorf("no data found for lookup table at address: %s", addressStr), debugID) } - // Convert the ids to a solana.PublicKeySlice and add to the lookup table map. - addresses := make(solana.PublicKeySlice, len(ids)) - for i, accountMeta := range ids { - addresses[i] = accountMeta.PublicKey + // Decode and extract public keys within the lookup table + addresses, err := decodeLookupTable(accountInfo.Value.Data.GetBinary()) + if err != nil { + return nil, errorWithDebugID(fmt.Errorf("error decoding lookup table data for %s: %w", addressStr, err), debugID) } - tables[lookupTable.Address] = addresses + + // Add the addresses to the lookup table map + tables[tableAddress] = addresses } return tables, nil } +func decodeLookupTable(data []byte) (solana.PublicKeySlice, error) { + // Example logic to decode lookup table data; you may need to adjust based on the actual format of the data. + var addresses solana.PublicKeySlice + + // Assuming the data is a list of 32-byte public keys in binary format: + for i := 0; i < len(data); i += solana.PublicKeyLength { + if i+solana.PublicKeyLength > len(data) { + return nil, fmt.Errorf("invalid lookup table data length") + } + address := solana.PublicKeyFromBytes(data[i : i+solana.PublicKeyLength]) + addresses = append(addresses, address) + } + + return addresses, nil +} + func (s *SolanaChainWriterService) SubmitTransaction(ctx context.Context, contractName, method string, args any, transactionID string, toAddress string, meta *types.TxMeta, value *big.Int) error { programConfig := s.config.Programs[contractName] methodConfig := programConfig.Methods[method] @@ -400,16 +431,16 @@ func (s *SolanaChainWriterService) SubmitTransaction(ctx context.Context, contra return errorWithDebugID(fmt.Errorf("error getting debug ID from decoded data: %w", err), debugID) } } - readableTableAccounts, readableTableMap, err := s.getReadableTableMap(ctx, decoded, methodConfig.ReadableLookupTables, debugID) + derivedTableAccounts, lookupTableAddresses, derivedTableMap, err := s.getDerivedTableMap(ctx, methodConfig.DerivedLookupTables, debugID) if err != nil { return errorWithDebugID(fmt.Errorf("error getting readable table map: %w", err), debugID) } - accounts, err := s.GetAddresses(ctx, decoded, methodConfig.Accounts, readableTableMap, debugID) - accounts = append(accounts, readableTableAccounts...) + accounts, err := s.GetAddresses(ctx, decoded, methodConfig.Accounts, derivedTableMap, debugID) + accounts = append(accounts, derivedTableAccounts...) if err != nil { return errorWithDebugID(fmt.Errorf("error getting addresses from decoded data: %w", err), debugID) } - lookupTables, err := s.GetLookupTables(ctx, decoded, methodConfig.LookupTables, readableTableMap, debugID) + lookupTables, err := s.GetLookupTables(ctx, append(methodConfig.LookupTables, lookupTableAddresses...), debugID) if err != nil { return errorWithDebugID(fmt.Errorf("error getting lookup tables from decoded data: %w", err), debugID) } From fa40469feefbb76c196c5107fbac33f4b05a7424 Mon Sep 17 00:00:00 2001 From: Silas Lenihan Date: Wed, 30 Oct 2024 11:12:57 -0400 Subject: [PATCH 06/22] Created sample configuration for execute method --- pkg/solana/chainwriter/chain_writer_test.go | 321 ++++++++++++++------ pkg/solana/chainwriter/helpers_test.go | 109 +++++++ 2 files changed, 337 insertions(+), 93 deletions(-) create mode 100644 pkg/solana/chainwriter/helpers_test.go diff --git a/pkg/solana/chainwriter/chain_writer_test.go b/pkg/solana/chainwriter/chain_writer_test.go index e7c304c65..cd5ad1701 100644 --- a/pkg/solana/chainwriter/chain_writer_test.go +++ b/pkg/solana/chainwriter/chain_writer_test.go @@ -1,125 +1,260 @@ package chainwriter_test import ( - "context" + "fmt" + "reflect" "testing" - "github.com/gagliardetto/solana-go" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "github.com/smartcontractkit/chainlink-solana/pkg/solana/chainwriter" - - clientmocks "github.com/smartcontractkit/chainlink-solana/pkg/solana/client/mocks" - gemocks "github.com/smartcontractkit/chainlink-solana/pkg/solana/fees/mocks" - "github.com/smartcontractkit/chainlink-solana/pkg/solana/txm" ) -type TestStruct struct { - Messages []Message +type ExecutionReportSingleChain struct { + SourceChainSelector uint64 `json:"source_chain_selector"` + Message Any2SolanaRampMessage `json:"message"` + Root [32]byte `json:"root"` + Proofs [][]byte `json:"proofs"` } -type Message struct { - TokenAmounts []TokenAmount +type Any2SolanaRampMessage struct { + Header RampMessageHeader `json:"header"` + Sender []byte `json:"sender"` + Data []byte `json:"data"` + Receiver [32]byte `json:"receiver"` + ExtraArgs SolanaExtraArgs `json:"extra_args"` } -type TokenAmount struct { - SourceTokenAddress []byte - DestTokenAddress []byte +type RampMessageHeader struct { + MessageID [32]byte `json:"message_id"` + SourceChainSelector uint64 `json:"source_chain_selector"` + DestChainSelector uint64 `json:"dest_chain_selector"` + SequenceNumber uint64 `json:"sequence_number"` + Nonce uint64 `json:"nonce"` } -func TestGetAddresses(t *testing.T) { - ctx := context.TODO() +type SolanaExtraArgs struct { + ComputeUnits uint32 `json:"compute_units"` + AllowOutOfOrderExecution bool `json:"allow_out_of_order_execution"` +} - // Create a mock for the Reader interface - readerMock := clientmocks.NewReaderWriter(t) - txmMock := txm.Txm{} - geMock := gemocks.NewEstimator(t) +type RegistryTokenState struct { + PoolProgram [32]byte `json:"pool_program"` + PoolConfig [32]byte `json:"pool_config"` + TokenProgram [32]byte `json:"token_program"` + TokenState [32]byte `json:"token_state"` + PoolAssociatedTokenAccount [32]byte `json:"pool_associated_token_account"` +} - chainWriterConfig := chainwriter.ChainWriterConfig{ - Programs: map[string]chainwriter.ProgramConfig{ - - } +func TestGetAddresses(t *testing.T) { + registryAddress := "4Nn9dsYBcSTzRbK9hg9kzCUdrCSkMZq1UR6Vw1Tkaf6A" + routerProgramAddress := "4Nn9dsYBcSTzRbK9hg9kzCUdrCSkMZq1UR6Vw1Tkaf6B" + routerAccountConfigAddress := "4Nn9dsYBcSTzRbK9hg9kzCUdrCSkMZq1UR6Vw1Tkaf6C" + cpiSignerAddress := "4Nn9dsYBcSTzRbK9hg9kzCUdrCSkMZq1UR6Vw1Tkaf6D" + systemProgramAddress := "4Nn9dsYBcSTzRbK9hg9kzCUdrCSkMZq1UR6Vw1Tkaf6E" + computeBudgetProgramAddress := "4Nn9dsYBcSTzRbK9hg9kzCUdrCSkMZq1UR6Vw1Tkaf6F" + sysvarProgramAddress := "4Nn9dsYBcSTzRbK9hg9kzCUdrCSkMZq1UR6Vw1Tkaf6G" + commonAddressesLookupTable := "4Nn9dsYBcSTzRbK9hg9kzCUdrCSkMZq1UR6Vw1Tkaf6H" + routerLookupTable := "4Nn9dsYBcSTzRbK9hg9kzCUdrCSkMZq1UR6Vw1Tkaf6I" - // Create a test instance of SolanaChainWriterService - service := chainwriter.NewSolanaChainWriterService(readerMock, txmMock, geMock, chainWriterConfig) + executionReportSingleChainIDL := `{"name":"ExecutionReportSingleChain","type":{"kind":"struct","fields":[{"name":"source_chain_selector","type":"u64"},{"name":"message","type":{"defined":"Any2SolanaRampMessage"}},{"name":"root","type":{"array":["u8",32]}},{"name":"proofs","type":{"vec":{"array":["u8",32]}}}]}},{"name":"Any2SolanaRampMessage","type":{"kind":"struct","fields":[{"name":"header","type":{"defined":"RampMessageHeader"}},{"name":"sender","type":{"vec":"u8"}},{"name":"data","type":{"vec":"u8"}},{"name":"receiver","type":{"array":["u8",32]}},{"name":"extra_args","type":{"defined":"SolanaExtraArgs"}}]}},{"name":"RampMessageHeader","type":{"kind":"struct","fields":[{"name":"message_id","type":{"array":["u8",32]}},{"name":"source_chain_selector","type":"u64"},{"name":"dest_chain_selector","type":"u64"},{"name":"sequence_number","type":"u64"},{"name":"nonce","type":"u64"}]}},{"name":"SolanaExtraArgs","type":{"kind":"struct","fields":[{"name":"compute_units","type":"u32"},{"name":"allow_out_of_order_execution","type":"bool"}]}}` + registryTokenStateIDL := `{"name":"RegistryTokenState","type":"struct","fields":[{"name":"pool_program","type":{"array":[{"type":"u8"},32]}},{"name":"pool_config","type":{"array":[{"type":"u8"},32]}},{"name":"token_program","type":{"array":[{"type":"u8"},32]}},{"name":"token_state","type":{"array":[{"type":"u8"},32]}},{"name":"pool_associated_token_account","type":{"array":[{"type":"u8"},32]}}]}` - t.Run("success with AccountConstant", func(t *testing.T) { - accounts := []chainwriter.Lookup{ + executeConfig := chainwriter.MethodConfig{ + InputModifications: nil, + EncodedTypeIDL: executionReportSingleChainIDL, + DataType: reflect.TypeOf(ExecutionReportSingleChain{}), + DecodedTypeName: "ExecutionReportSingleChain", + ChainSpecificName: "execute", + DerivedLookupTables: []chainwriter.DerivedLookupTable{ + { + Name: "RegistryTokenState", + Identifier: chainwriter.PDALookup{ + Name: "RegistryTokenState", + PublicKey: chainwriter.AccountConstant{ + Address: registryAddress, + IsSigner: false, + IsWritable: false, + }, + AddressSeeds: nil, + ValueSeeds: []chainwriter.ValueLookup{ + {Location: "Message.TokenAmounts.DestTokenAddress"}, + }, + IsSigner: false, + IsWritable: false, + }, + EncodedTypeIDL: registryTokenStateIDL, + Locations: []chainwriter.AccountLookup{ + { + Name: "PoolProgram", + Location: "PoolProgram", + IsSigner: false, + IsWritable: false, + }, + { + Name: "PoolConfig", + Location: "PoolConfig", + IsSigner: false, + IsWritable: false, + }, + { + Name: "TokenProgram", + Location: "TokenProgram", + IsSigner: false, + IsWritable: false, + }, + { + Name: "TokenState", + Location: "TokenState", + IsSigner: false, + IsWritable: false, + }, + { + Name: "PoolAssociatedTokenAccount", + Location: "PoolAssociatedTokenAccount", + IsSigner: false, + IsWritable: false, + }, + }, + }, + }, + Accounts: []chainwriter.Lookup{ + chainwriter.PDALookup{ + Name: "PerChainRateLimit", + PublicKey: chainwriter.AccountConstant{ + Address: registryAddress, + IsSigner: false, + IsWritable: false, + }, + AddressSeeds: nil, + ValueSeeds: []chainwriter.ValueLookup{ + {Location: "Message.TokenAmounts.DestTokenAddress"}, + }, + IsSigner: false, + IsWritable: false, + }, + chainwriter.AccountLookup{ + Name: "TokenAccount", + Location: "Message.TokenAmounts.DestTokenAddress", + IsSigner: false, + IsWritable: false, + }, + chainwriter.PDALookup{ + Name: "ReceiverAssociatedTokenAccount", + PublicKey: chainwriter.AccountLookup{ + Name: "TokenAccount", + Location: "Message.TokenAmounts.DestTokenAddress", + IsSigner: false, + IsWritable: false, + }, + AddressSeeds: []chainwriter.Lookup{ + chainwriter.AccountLookup{ + Name: "Receiver", + Location: "Message.Receiver", + IsSigner: false, + IsWritable: false, + }, + }, + }, + chainwriter.AccountConstant{ + Name: "Registry", + Address: registryAddress, + IsSigner: false, + IsWritable: false, + }, + chainwriter.PDALookup{ + Name: "RegistryTokenConfig", + PublicKey: chainwriter.AccountConstant{ + Address: registryAddress, + IsSigner: false, + IsWritable: false, + }, + AddressSeeds: nil, + ValueSeeds: []chainwriter.ValueLookup{ + {Location: "Message.TokenAmounts.DestTokenAddress"}, + }, + IsSigner: false, + IsWritable: false, + }, + chainwriter.AccountConstant{ + Name: "RouterProgram", + Address: routerProgramAddress, + IsSigner: false, + IsWritable: false, + }, + chainwriter.AccountConstant{ + Name: "RouterAccountConfig", + Address: routerAccountConfigAddress, + IsSigner: false, + IsWritable: false, + }, + chainwriter.PDALookup{ + Name: "RouterReportAccount", + PublicKey: chainwriter.AccountConstant{ + Address: routerProgramAddress, + IsSigner: false, + IsWritable: false, + }, + AddressSeeds: nil, + ValueSeeds: []chainwriter.ValueLookup{ + // TBD - need to clarify how merkle roots are handled + {Location: "Message.ExtraArgs.MerkleRoot"}, + }, + IsSigner: false, + IsWritable: false, + }, + chainwriter.PDALookup{ + Name: "UserNoncePerChain", + PublicKey: chainwriter.AccountConstant{ + Address: routerProgramAddress, + IsSigner: false, + IsWritable: false, + }, + AddressSeeds: nil, + ValueSeeds: []chainwriter.ValueLookup{ + {Location: "Message.Receiver"}, + {Location: "Message.DestChainSelector"}, + }, + }, chainwriter.AccountConstant{ - Name: "test-account", - Address: "4Nn9dsYBcSTzRbK9hg9kzCUdrCSkMZq1UR6Vw1Tkaf6M", + Name: "CPISigner", + Address: cpiSignerAddress, IsSigner: true, IsWritable: false, }, - } - - // Call GetAddresses with the constant account - addresses, err := service.GetAddresses(ctx, nil, accounts, nil, "test-debug-id") - require.NoError(t, err) - require.Len(t, addresses, 1) - require.Equal(t, addresses[0].PublicKey.String(), "4Nn9dsYBcSTzRbK9hg9kzCUdrCSkMZq1UR6Vw1Tkaf6M") - require.True(t, addresses[0].IsSigner) - require.False(t, addresses[0].IsWritable) - }) - - t.Run("success with AccountLookup", func(t *testing.T) { - accounts := []chainwriter.Lookup{ - chainwriter.AccountLookup{ - Name: "test-account", - Location: "Messages.TokenAmounts.SourceTokenAddress", + chainwriter.AccountConstant{ + Name: "SystemProgram", + Address: systemProgramAddress, IsSigner: true, IsWritable: false, }, - chainwriter.AccountLookup{ - Name: "test-account", - Location: "Messages.TokenAmounts.DestTokenAddress", + chainwriter.AccountConstant{ + Name: "ComputeBudgetProgram", + Address: computeBudgetProgramAddress, IsSigner: true, IsWritable: false, }, - } - - // Create a test struct with the expected address - addresses := make([][]byte, 8) - for i := 0; i < 8; i++ { - privKey, err := solana.NewRandomPrivateKey() - require.NoError(t, err) - addresses[i] = privKey.PublicKey().Bytes() - } + chainwriter.AccountConstant{ + Name: "SysvarProgram", + Address: sysvarProgramAddress, + IsSigner: true, + IsWritable: false, + }, + }, + LookupTables: []string{ + commonAddressesLookupTable, + routerLookupTable, + }, + // TBD where this will be in the report + DebugIDLocation: "Message.ExtraArgs.DebugID", + } - exampleDecoded := TestStruct{ - Messages: []Message{ - { - TokenAmounts: []TokenAmount{ - {addresses[0], addresses[1]}, - {addresses[2], addresses[3]}, - }, - }, - { - TokenAmounts: []TokenAmount{ - {addresses[4], addresses[5]}, - {addresses[6], addresses[7]}, - }, + chainWriterConfig := chainwriter.ChainWriterConfig{ + Programs: map[string]chainwriter.ProgramConfig{ + "ccip-router": { + Methods: map[string]chainwriter.MethodConfig{ + "execute": executeConfig, }, }, - } - // Call GetAddresses with the lookup account - derivedAddresses, err := service.GetAddresses(ctx, exampleDecoded, accounts, nil, "test-debug-id") - - // Create a map of the expected addresses for fast lookup - expectedAddresses := make(map[string]bool) - for _, addr := range addresses { - expectedAddresses[string(addr)] = true - } - - // Verify that each derived address matches an expected address - for _, derivedAddr := range derivedAddresses { - derivedBytes := derivedAddr.PublicKey.Bytes() - assert.True(t, expectedAddresses[string(derivedBytes)], "Address not found in expected list") - } - - require.NoError(t, err) - require.Len(t, derivedAddresses, 8) - }) - + }, + } + fmt.Println(chainWriterConfig) } diff --git a/pkg/solana/chainwriter/helpers_test.go b/pkg/solana/chainwriter/helpers_test.go new file mode 100644 index 000000000..ba9df5c58 --- /dev/null +++ b/pkg/solana/chainwriter/helpers_test.go @@ -0,0 +1,109 @@ +package chainwriter_test + +// import ( +// "context" +// "testing" + +// "github.com/gagliardetto/solana-go" +// "github.com/smartcontractkit/chainlink-solana/pkg/solana/chainwriter" +// "github.com/test-go/testify/assert" +// "github.com/test-go/testify/require" +// ) + +// type TestStruct struct { +// Messages []Message +// } + +// type Message struct { +// TokenAmounts []TokenAmount +// } + +// type TokenAmount struct { +// SourceTokenAddress []byte +// DestTokenAddress []byte +// } + +// func TestHelpersTestGetAddresses(t *testing.T) { +// ctx := context.TODO() + +// chainWriterConfig := chainwriter.ChainWriterConfig{} +// service := chainwriter.NewChainWriterService(chainWriterConfig) + +// t.Run("success with AccountConstant", func(t *testing.T) { +// accounts := []chainwriter.Lookup{ +// chainwriter.AccountConstant{ +// Name: "test-account", +// Address: "4Nn9dsYBcSTzRbK9hg9kzCUdrCSkMZq1UR6Vw1Tkaf6M", +// IsSigner: true, +// IsWritable: false, +// }, +// } + +// // Call GetAddresses with the constant account +// addresses, err := service.GetAddresses(ctx, nil, accounts, nil, "test-debug-id") +// require.NoError(t, err) +// require.Len(t, addresses, 1) +// require.Equal(t, addresses[0].PublicKey.String(), "4Nn9dsYBcSTzRbK9hg9kzCUdrCSkMZq1UR6Vw1Tkaf6M") +// require.True(t, addresses[0].IsSigner) +// require.False(t, addresses[0].IsWritable) +// }) + +// t.Run("success with AccountLookup", func(t *testing.T) { +// accounts := []chainwriter.Lookup{ +// chainwriter.AccountLookup{ +// Name: "test-account", +// Location: "Messages.TokenAmounts.SourceTokenAddress", +// IsSigner: true, +// IsWritable: false, +// }, +// chainwriter.AccountLookup{ +// Name: "test-account", +// Location: "Messages.TokenAmounts.DestTokenAddress", +// IsSigner: true, +// IsWritable: false, +// }, +// } + +// // Create a test struct with the expected address +// addresses := make([][]byte, 8) +// for i := 0; i < 8; i++ { +// privKey, err := solana.NewRandomPrivateKey() +// require.NoError(t, err) +// addresses[i] = privKey.PublicKey().Bytes() +// } + +// exampleDecoded := TestStruct{ +// Messages: []Message{ +// { +// TokenAmounts: []TokenAmount{ +// {addresses[0], addresses[1]}, +// {addresses[2], addresses[3]}, +// }, +// }, +// { +// TokenAmounts: []TokenAmount{ +// {addresses[4], addresses[5]}, +// {addresses[6], addresses[7]}, +// }, +// }, +// }, +// } +// // Call GetAddresses with the lookup account +// derivedAddresses, err := service.GetAddresses(ctx, exampleDecoded, accounts, nil, "test-debug-id") + +// // Create a map of the expected addresses for fast lookup +// expectedAddresses := make(map[string]bool) +// for _, addr := range addresses { +// expectedAddresses[string(addr)] = true +// } + +// // Verify that each derived address matches an expected address +// for _, derivedAddr := range derivedAddresses { +// derivedBytes := derivedAddr.PublicKey.Bytes() +// assert.True(t, expectedAddresses[string(derivedBytes)], "Address not found in expected list") +// } + +// require.NoError(t, err) +// require.Len(t, derivedAddresses, 8) +// }) +// } From 786d9f888b7eb57f38e489de3b27e996428ef2c8 Mon Sep 17 00:00:00 2001 From: pablolagreca Date: Wed, 30 Oct 2024 14:22:17 -0300 Subject: [PATCH 07/22] Update chain_writer_test.go --- pkg/solana/chainwriter/chain_writer_test.go | 92 ++++++++++++--------- 1 file changed, 55 insertions(+), 37 deletions(-) diff --git a/pkg/solana/chainwriter/chain_writer_test.go b/pkg/solana/chainwriter/chain_writer_test.go index cd5ad1701..4fb2fd79d 100644 --- a/pkg/solana/chainwriter/chain_writer_test.go +++ b/pkg/solana/chainwriter/chain_writer_test.go @@ -44,6 +44,25 @@ type RegistryTokenState struct { PoolAssociatedTokenAccount [32]byte `json:"pool_associated_token_account"` } +inputParams := []{ + generatedReports, + rs, + vs, + .. +} + +inputParams := []{ + merketRoots, + generatedReports, + rs, + vs, + .. +} + +CW.SubmitTransaction(address, "router", "executeReport", inputParams, ...) + +SubmitReport([]report, ...) + func TestGetAddresses(t *testing.T) { registryAddress := "4Nn9dsYBcSTzRbK9hg9kzCUdrCSkMZq1UR6Vw1Tkaf6A" routerProgramAddress := "4Nn9dsYBcSTzRbK9hg9kzCUdrCSkMZq1UR6Vw1Tkaf6B" @@ -64,10 +83,10 @@ func TestGetAddresses(t *testing.T) { DataType: reflect.TypeOf(ExecutionReportSingleChain{}), DecodedTypeName: "ExecutionReportSingleChain", ChainSpecificName: "execute", - DerivedLookupTables: []chainwriter.DerivedLookupTable{ + LookupTables: []chainwriter.LookupTable{ { Name: "RegistryTokenState", - Identifier: chainwriter.PDALookup{ + Accounts: chainwriter.PDALookups{ Name: "RegistryTokenState", PublicKey: chainwriter.AccountConstant{ Address: registryAddress, @@ -80,43 +99,17 @@ func TestGetAddresses(t *testing.T) { }, IsSigner: false, IsWritable: false, - }, - EncodedTypeIDL: registryTokenStateIDL, - Locations: []chainwriter.AccountLookup{ - { - Name: "PoolProgram", - Location: "PoolProgram", - IsSigner: false, - IsWritable: false, - }, - { - Name: "PoolConfig", - Location: "PoolConfig", - IsSigner: false, - IsWritable: false, - }, - { - Name: "TokenProgram", - Location: "TokenProgram", - IsSigner: false, - IsWritable: false, - }, - { - Name: "TokenState", - Location: "TokenState", - IsSigner: false, - IsWritable: false, - }, - { - Name: "PoolAssociatedTokenAccount", - Location: "PoolAssociatedTokenAccount", - IsSigner: false, - IsWritable: false, - }, - }, + } --> ["a", "b", "c"] each being a PDA account for a token which are address lookup table accounts, }, }, Accounts: []chainwriter.Lookup{ + // Account constant + // Account Lookup - Based on data from input parameters + // Lookup Table content - Get all the accounts from a lookup table + // PDA Account Lookup - Based on another account and a seed/s + // Nested PDA Account with seeds from: + // input paramters + // constant chainwriter.PDALookup{ Name: "PerChainRateLimit", PublicKey: chainwriter.AccountConstant{ @@ -131,13 +124,37 @@ func TestGetAddresses(t *testing.T) { IsSigner: false, IsWritable: false, }, + { + Name: "RegistryTokenState", + Accounts: chainwriter.PDALookups{ + Name: "RegistryTokenState", + PublicKey: chainwriter.AccountConstant{ + Address: registryAddress, + IsSigner: false, + IsWritable: false, + }, + AddressSeeds: nil, + ValueSeeds: []chainwriter.ValueLookup{ + {Location: "Message.TokenAmounts.DestTokenAddress"}, + }, + IsSigner: false, + IsWritable: false, + }, + } + // Lookup Table content - Get all the accounts from a lookup table + chainWriter.AccountsFromLookupTable: { // Just include all the accounts within the RegistryTokenState lookup table. + LookupTablesName: "RegistryTokenState", + IncludeIndexes: [1,4] // WE DON"T NEED THIS RIGHT NOW + }, + // Account Lookup - Based on data from input parameters chainwriter.AccountLookup{ Name: "TokenAccount", Location: "Message.TokenAmounts.DestTokenAddress", IsSigner: false, IsWritable: false, }, - chainwriter.PDALookup{ + // PDA Account Lookup - + chainwriter.PDALookups{ Name: "ReceiverAssociatedTokenAccount", PublicKey: chainwriter.AccountLookup{ Name: "TokenAccount", @@ -174,6 +191,7 @@ func TestGetAddresses(t *testing.T) { IsSigner: false, IsWritable: false, }, + // Account constant chainwriter.AccountConstant{ Name: "RouterProgram", Address: routerProgramAddress, From 7ba862d67ec00968e91b31b41805311f9bda0167 Mon Sep 17 00:00:00 2001 From: Silas Lenihan Date: Wed, 30 Oct 2024 14:31:44 -0400 Subject: [PATCH 08/22] Cleaned up exec config and added comments --- pkg/solana/chainwriter/chain_writer.go | 54 +++--- pkg/solana/chainwriter/chain_writer_test.go | 176 +++++++++++--------- 2 files changed, 133 insertions(+), 97 deletions(-) diff --git a/pkg/solana/chainwriter/chain_writer.go b/pkg/solana/chainwriter/chain_writer.go index bdd413bfa..98fe74ae6 100644 --- a/pkg/solana/chainwriter/chain_writer.go +++ b/pkg/solana/chainwriter/chain_writer.go @@ -39,14 +39,13 @@ type ProgramConfig struct { } type MethodConfig struct { - InputModifications commoncodec.ModifiersConfig - EncodedTypeIDL string - DataType reflect.Type - DecodedTypeName string - ChainSpecificName string - DerivedLookupTables []DerivedLookupTable - Accounts []Lookup - LookupTables []string + InputModifications commoncodec.ModifiersConfig + EncodedTypeIDL string + DataType reflect.Type + DecodedTypeName string + ChainSpecificName string + Accounts []Lookup + LookupTables LookupTables // Location in the decoded data where the debug ID is stored DebugIDLocation string } @@ -54,6 +53,7 @@ type MethodConfig struct { type Lookup interface { } +// AccountConstant represents a fixed address, provided in Base58 format, converted into a `solana.PublicKey`. type AccountConstant struct { Name string Address string @@ -61,6 +61,7 @@ type AccountConstant struct { IsWritable bool } +// AccountLookup dynamically derives an account address from decoded data using a specified location path. type AccountLookup struct { Name string Location string @@ -68,7 +69,8 @@ type AccountLookup struct { IsWritable bool } -type PDALookup struct { +// PDALookups generates Program Derived Addresses (PDA) by combining a derived public key with one or more seeds. +type PDALookups struct { Name string // The public key of the PDA to be combined with seeds. If there are multiple PublicKeys // there will be multiple PDAs generated by combining each PublicKey with the seeds. @@ -85,14 +87,26 @@ type ValueLookup struct { Location string } +// LookupTables represents a list of lookup tables that are used to derive addresses for a program. +type LookupTables struct { + DerivedLookupTables []DerivedLookupTable + StaticLookupTables []string +} + +// DerivedLookupTable represents a lookup table that is used to derive addresses for a program. type DerivedLookupTable struct { Name string - Identifier Lookup + Accounts Lookup EncodedTypeIDL string - Locations []AccountLookup DecodedType reflect.Type } +// AccountsFromLookupTable extracts accounts from a lookup table that was previously read and stored in memory. +type AccountsFromLookupTable struct { + LookupTablesName string + IncludeIndexes []int +} + func NewSolanaChainWriterService(reader client.Reader, txm txm.Txm, ge fees.Estimator, config ChainWriterConfig) *SolanaChainWriterService { return &SolanaChainWriterService{ reader: reader, @@ -125,7 +139,7 @@ for Solana transactions. It handles constant addresses, dynamic lookups, program - Dynamically derived from decoded data using a specified location path (e.g., `user.walletAddress`). - If the lookup table is pre-loaded, the address is fetched from `derivedTableMap`. -3. **PDALookup**: +3. **PDALookups**: - Generates Program Derived Addresses (PDA) by combining a derived public key with one or more seeds. - Seeds can be `AddressSeeds` (public keys from the decoded data) or `ValueSeeds` (byte arrays). - Ensures there is only one public key if multiple seeds are provided. @@ -154,7 +168,7 @@ func (s *SolanaChainWriterService) getAccountMeta(ctx context.Context, decoded a return s.handleAccountConstant(lookup, debugID) case AccountLookup: return s.handleAccountLookup(decoded, lookup, derivedTableMap, debugID) - case PDALookup: + case PDALookups: return s.handlePDALookup(ctx, decoded, lookup, derivedTableMap, debugID) default: return nil, errorWithDebugID(fmt.Errorf("unsupported account type: %T", lookup), debugID) @@ -198,24 +212,24 @@ func (s *SolanaChainWriterService) handleAccountLookup(decoded any, lookup Accou return metas, nil } -// handlePDALookup processes a `PDALookup` by resolving seeds and generating the PDA address. -func (s *SolanaChainWriterService) handlePDALookup(ctx context.Context, decoded any, lookup PDALookup, derivedTableMap map[string][]*solana.AccountMeta, debugID string) ([]*solana.AccountMeta, error) { +// handlePDALookup processes a `PDALookups` by resolving seeds and generating the PDA address. +func (s *SolanaChainWriterService) handlePDALookup(ctx context.Context, decoded any, lookup PDALookups, derivedTableMap map[string][]*solana.AccountMeta, debugID string) ([]*solana.AccountMeta, error) { publicKeys, err := s.GetAddresses(ctx, decoded, []Lookup{lookup.PublicKey}, derivedTableMap, debugID) if err != nil { - return nil, errorWithDebugID(fmt.Errorf("error getting public key for PDALookup: %w", err), debugID) + return nil, errorWithDebugID(fmt.Errorf("error getting public key for PDALookups: %w", err), debugID) } seeds, err := s.getSeedBytes(ctx, lookup, decoded, derivedTableMap, debugID) if err != nil { - return nil, errorWithDebugID(fmt.Errorf("error getting seeds for PDALookup: %w", err), debugID) + return nil, errorWithDebugID(fmt.Errorf("error getting seeds for PDALookups: %w", err), debugID) } return s.generatePDAs(publicKeys, seeds, lookup, debugID) } -// getSeedBytes extracts the seeds for the PDALookup. +// getSeedBytes extracts the seeds for the PDALookups. // It handles both AddressSeeds (which are public keys) and ValueSeeds (which are byte arrays from decoded data). -func (s *SolanaChainWriterService) getSeedBytes(ctx context.Context, lookup PDALookup, decoded any, derivedTableMap map[string][]*solana.AccountMeta, debugID string) ([][]byte, error) { +func (s *SolanaChainWriterService) getSeedBytes(ctx context.Context, lookup PDALookups, decoded any, derivedTableMap map[string][]*solana.AccountMeta, debugID string) ([][]byte, error) { var seedBytes [][]byte // Process AddressSeeds first (e.g., public keys) @@ -248,7 +262,7 @@ func (s *SolanaChainWriterService) getSeedBytes(ctx context.Context, lookup PDAL } // generatePDAs generates program-derived addresses (PDAs) from public keys and seeds. -func (s *SolanaChainWriterService) generatePDAs(publicKeys []*solana.AccountMeta, seeds [][]byte, lookup PDALookup, debugID string) ([]*solana.AccountMeta, error) { +func (s *SolanaChainWriterService) generatePDAs(publicKeys []*solana.AccountMeta, seeds [][]byte, lookup PDALookups, debugID string) ([]*solana.AccountMeta, error) { if len(seeds) > 1 && len(publicKeys) > 1 { return nil, errorWithDebugID(fmt.Errorf("multiple public keys and multiple seeds are not allowed"), debugID) } diff --git a/pkg/solana/chainwriter/chain_writer_test.go b/pkg/solana/chainwriter/chain_writer_test.go index 4fb2fd79d..94f179cac 100644 --- a/pkg/solana/chainwriter/chain_writer_test.go +++ b/pkg/solana/chainwriter/chain_writer_test.go @@ -5,6 +5,7 @@ import ( "reflect" "testing" + commoncodec "github.com/smartcontractkit/chainlink-common/pkg/codec" "github.com/smartcontractkit/chainlink-solana/pkg/solana/chainwriter" ) @@ -44,26 +45,8 @@ type RegistryTokenState struct { PoolAssociatedTokenAccount [32]byte `json:"pool_associated_token_account"` } -inputParams := []{ - generatedReports, - rs, - vs, - .. -} - -inputParams := []{ - merketRoots, - generatedReports, - rs, - vs, - .. -} - -CW.SubmitTransaction(address, "router", "executeReport", inputParams, ...) - -SubmitReport([]report, ...) - func TestGetAddresses(t *testing.T) { + // Fake constant addresses for the purpose of this example. registryAddress := "4Nn9dsYBcSTzRbK9hg9kzCUdrCSkMZq1UR6Vw1Tkaf6A" routerProgramAddress := "4Nn9dsYBcSTzRbK9hg9kzCUdrCSkMZq1UR6Vw1Tkaf6B" routerAccountConfigAddress := "4Nn9dsYBcSTzRbK9hg9kzCUdrCSkMZq1UR6Vw1Tkaf6C" @@ -75,86 +58,111 @@ func TestGetAddresses(t *testing.T) { routerLookupTable := "4Nn9dsYBcSTzRbK9hg9kzCUdrCSkMZq1UR6Vw1Tkaf6I" executionReportSingleChainIDL := `{"name":"ExecutionReportSingleChain","type":{"kind":"struct","fields":[{"name":"source_chain_selector","type":"u64"},{"name":"message","type":{"defined":"Any2SolanaRampMessage"}},{"name":"root","type":{"array":["u8",32]}},{"name":"proofs","type":{"vec":{"array":["u8",32]}}}]}},{"name":"Any2SolanaRampMessage","type":{"kind":"struct","fields":[{"name":"header","type":{"defined":"RampMessageHeader"}},{"name":"sender","type":{"vec":"u8"}},{"name":"data","type":{"vec":"u8"}},{"name":"receiver","type":{"array":["u8",32]}},{"name":"extra_args","type":{"defined":"SolanaExtraArgs"}}]}},{"name":"RampMessageHeader","type":{"kind":"struct","fields":[{"name":"message_id","type":{"array":["u8",32]}},{"name":"source_chain_selector","type":"u64"},{"name":"dest_chain_selector","type":"u64"},{"name":"sequence_number","type":"u64"},{"name":"nonce","type":"u64"}]}},{"name":"SolanaExtraArgs","type":{"kind":"struct","fields":[{"name":"compute_units","type":"u32"},{"name":"allow_out_of_order_execution","type":"bool"}]}}` - registryTokenStateIDL := `{"name":"RegistryTokenState","type":"struct","fields":[{"name":"pool_program","type":{"array":[{"type":"u8"},32]}},{"name":"pool_config","type":{"array":[{"type":"u8"},32]}},{"name":"token_program","type":{"array":[{"type":"u8"},32]}},{"name":"token_state","type":{"array":[{"type":"u8"},32]}},{"name":"pool_associated_token_account","type":{"array":[{"type":"u8"},32]}}]}` + // registryTokenStateIDL := `{"name":"RegistryTokenState","type":"struct","fields":[{"name":"pool_program","type":{"array":[{"type":"u8"},32]}},{"name":"pool_config","type":{"array":[{"type":"u8"},32]}},{"name":"token_program","type":{"array":[{"type":"u8"},32]}},{"name":"token_state","type":{"array":[{"type":"u8"},32]}},{"name":"pool_associated_token_account","type":{"array":[{"type":"u8"},32]}}]}` executeConfig := chainwriter.MethodConfig{ - InputModifications: nil, - EncodedTypeIDL: executionReportSingleChainIDL, - DataType: reflect.TypeOf(ExecutionReportSingleChain{}), - DecodedTypeName: "ExecutionReportSingleChain", - ChainSpecificName: "execute", - LookupTables: []chainwriter.LookupTable{ - { - Name: "RegistryTokenState", - Accounts: chainwriter.PDALookups{ + InputModifications: commoncodec.ModifiersConfig{ + // remove merkle root since it isn't a part of the on-chain type + &commoncodec.DropModifierConfig{ + Fields: []string{"Message.ExtraArgs.MerkleRoot"}, + }, + }, + EncodedTypeIDL: executionReportSingleChainIDL, + DataType: reflect.TypeOf(ExecutionReportSingleChain{}), + DecodedTypeName: "ExecutionReportSingleChain", + ChainSpecificName: "execute", + // LookupTables are on-chain stores of accounts. They can be used in two ways: + // 1. As a way to store a list of accounts that are all associated together (i.e. Token State registry) + // 2. To compress the transactions in a TX and reduce the size of the TX. (The traditional way) + LookupTables: chainwriter.LookupTables{ + // DerivedLookupTables are useful in both the ways described above. + // a. The user can configure any type of look up to get a list of lookupTables to read from. + // b. The ChainWriter reads from this lookup table and store the internal addresses in memory + // c. Later, in the []Accounts the user can specify which accounts to include in the TX with an AccountsFromLookupTable lookup. + // d. Lastly, the lookup table is used to compress the size of the transaction. + DerivedLookupTables: []chainwriter.DerivedLookupTable{ + { Name: "RegistryTokenState", - PublicKey: chainwriter.AccountConstant{ - Address: registryAddress, + // In this case, the user configured the lookup table accounts to use a PDALookup, which + // generates a list of one of more PDA accounts based on the input parameters. Specifically, + // there will be multple PDA accounts if there are multiple addresses in the message, otherwise, + // there will only be one PDA account to read from. The PDA account corresponds to the lookup table. + Accounts: chainwriter.PDALookups{ + Name: "RegistryTokenState", + PublicKey: chainwriter.AccountConstant{ + Address: registryAddress, + IsSigner: false, + IsWritable: false, + }, + // AddressSeeds would be used if the user needed to look up addresses to use as seeds, which isn't the case here. + AddressSeeds: nil, + // ValueSeeds tells the ChainWriter where to look in the input parameters to get the seeds for the PDA account. + ValueSeeds: []chainwriter.ValueLookup{ + {Location: "Message.TokenAmounts.DestTokenAddress"}, + }, IsSigner: false, IsWritable: false, }, - AddressSeeds: nil, - ValueSeeds: []chainwriter.ValueLookup{ - {Location: "Message.TokenAmounts.DestTokenAddress"}, - }, - IsSigner: false, - IsWritable: false, - } --> ["a", "b", "c"] each being a PDA account for a token which are address lookup table accounts, + }, + }, + // Static lookup tables are the traditional use case (point 2 above) of Lookup tables. These are lookup + // tables which contain commonly used addresses in all CCIP execute transactions. The ChainWriter reads + // these lookup tables and appends them to the transaction to reduce the size of the transaction. + StaticLookupTables: []string{ + commonAddressesLookupTable, + routerLookupTable, }, }, + // The Accounts field is where the user specifies which accounts to include in the transaction. Each Lookup + // resolves to one or more on-chain addresses. Accounts: []chainwriter.Lookup{ - // Account constant - // Account Lookup - Based on data from input parameters - // Lookup Table content - Get all the accounts from a lookup table - // PDA Account Lookup - Based on another account and a seed/s - // Nested PDA Account with seeds from: - // input paramters - // constant - chainwriter.PDALookup{ + // The accounts can be of any of the following types: + // 1. Account constant + // 2. Account Lookup - Based on data from input parameters + // 3. Lookup Table content - Get all the accounts from a lookup table + // 4. PDA Account Lookup - Based on another account and a seed/s + // Nested PDA Account with seeds from: + // -> input paramters + // -> constant + // PDALookups can resolve to multiple addresses if: + // A) The PublicKey lookup resolves to multiple addresses (i.e. multiple token addresses) + // B) The AddressSeeds or ValueSeeds resolve to multiple values + chainwriter.PDALookups{ Name: "PerChainRateLimit", + // PublicKey is a constant account in this case, not a lookup. PublicKey: chainwriter.AccountConstant{ Address: registryAddress, IsSigner: false, IsWritable: false, }, AddressSeeds: nil, + // Similar to the RegistryTokenState above, the user is looking up PDA accounts based on the dest tokens. ValueSeeds: []chainwriter.ValueLookup{ + // If there are multiple tokens within the report, this will result in multiple PDA accounts {Location: "Message.TokenAmounts.DestTokenAddress"}, }, IsSigner: false, IsWritable: false, }, - { - Name: "RegistryTokenState", - Accounts: chainwriter.PDALookups{ - Name: "RegistryTokenState", - PublicKey: chainwriter.AccountConstant{ - Address: registryAddress, - IsSigner: false, - IsWritable: false, - }, - AddressSeeds: nil, - ValueSeeds: []chainwriter.ValueLookup{ - {Location: "Message.TokenAmounts.DestTokenAddress"}, - }, - IsSigner: false, - IsWritable: false, - }, - } - // Lookup Table content - Get all the accounts from a lookup table - chainWriter.AccountsFromLookupTable: { // Just include all the accounts within the RegistryTokenState lookup table. + // Lookup Table content - Get the accounts from the derived lookup table above + chainwriter.AccountsFromLookupTable{ LookupTablesName: "RegistryTokenState", - IncludeIndexes: [1,4] // WE DON"T NEED THIS RIGHT NOW + IncludeIndexes: []int{1, 4}, // If left empty, all addresses will be included. }, // Account Lookup - Based on data from input parameters + // In this case, the user wants to add the destination token addresses to the transaction. + // Once again, this can be one or multiple addresses. chainwriter.AccountLookup{ Name: "TokenAccount", Location: "Message.TokenAmounts.DestTokenAddress", IsSigner: false, IsWritable: false, }, - // PDA Account Lookup - + // PDA Account Lookup - Based on an account lookup and an address lookup chainwriter.PDALookups{ + // In this case, the token address is the public key, and the receiver is the seed. + // Again, there could be multiple token addresses, in which case this would resolve to + // multiple PDA accounts. Name: "ReceiverAssociatedTokenAccount", PublicKey: chainwriter.AccountLookup{ Name: "TokenAccount", @@ -162,6 +170,7 @@ func TestGetAddresses(t *testing.T) { IsSigner: false, IsWritable: false, }, + // The seed is the receiver address. AddressSeeds: []chainwriter.Lookup{ chainwriter.AccountLookup{ Name: "Receiver", @@ -171,20 +180,24 @@ func TestGetAddresses(t *testing.T) { }, }, }, + // Account constant chainwriter.AccountConstant{ Name: "Registry", Address: registryAddress, IsSigner: false, IsWritable: false, }, - chainwriter.PDALookup{ + // PDA Lookup for the RegistryTokenConfig. + chainwriter.PDALookups{ Name: "RegistryTokenConfig", + // constant public key PublicKey: chainwriter.AccountConstant{ Address: registryAddress, IsSigner: false, IsWritable: false, }, AddressSeeds: nil, + // The seed, once again, is the destination token address. ValueSeeds: []chainwriter.ValueLookup{ {Location: "Message.TokenAmounts.DestTokenAddress"}, }, @@ -198,14 +211,17 @@ func TestGetAddresses(t *testing.T) { IsSigner: false, IsWritable: false, }, + // Account constant chainwriter.AccountConstant{ Name: "RouterAccountConfig", Address: routerAccountConfigAddress, IsSigner: false, IsWritable: false, }, - chainwriter.PDALookup{ + // PDA lookup to get the Router Report Accounts. + chainwriter.PDALookups{ Name: "RouterReportAccount", + // The public key is a constant Router address. PublicKey: chainwriter.AccountConstant{ Address: routerProgramAddress, IsSigner: false, @@ -213,43 +229,51 @@ func TestGetAddresses(t *testing.T) { }, AddressSeeds: nil, ValueSeeds: []chainwriter.ValueLookup{ - // TBD - need to clarify how merkle roots are handled - {Location: "Message.ExtraArgs.MerkleRoot"}, + // The seed is the merkle root of the report, as passed into the input params. + {Location: "args.MerkleRoot"}, }, IsSigner: false, IsWritable: false, }, - chainwriter.PDALookup{ + // PDA lookup to get UserNoncePerChain + chainwriter.PDALookups{ Name: "UserNoncePerChain", + // The public key is a constant Router address. PublicKey: chainwriter.AccountConstant{ Address: routerProgramAddress, IsSigner: false, IsWritable: false, }, AddressSeeds: nil, + // In this case, the user configured multiple seeds. These will be used in conjunction + // with the public key to generate one or multiple PDA accounts. ValueSeeds: []chainwriter.ValueLookup{ {Location: "Message.Receiver"}, {Location: "Message.DestChainSelector"}, }, }, + // Account constant chainwriter.AccountConstant{ Name: "CPISigner", Address: cpiSignerAddress, IsSigner: true, IsWritable: false, }, + // Account constant chainwriter.AccountConstant{ Name: "SystemProgram", Address: systemProgramAddress, IsSigner: true, IsWritable: false, }, + // Account constant chainwriter.AccountConstant{ Name: "ComputeBudgetProgram", Address: computeBudgetProgramAddress, IsSigner: true, IsWritable: false, }, + // Account constant chainwriter.AccountConstant{ Name: "SysvarProgram", Address: sysvarProgramAddress, @@ -257,12 +281,9 @@ func TestGetAddresses(t *testing.T) { IsWritable: false, }, }, - LookupTables: []string{ - commonAddressesLookupTable, - routerLookupTable, - }, // TBD where this will be in the report - DebugIDLocation: "Message.ExtraArgs.DebugID", + // This will be appended to every error message (after args are decoded). + DebugIDLocation: "Message.MessageID", } chainWriterConfig := chainwriter.ChainWriterConfig{ @@ -271,6 +292,7 @@ func TestGetAddresses(t *testing.T) { Methods: map[string]chainwriter.MethodConfig{ "execute": executeConfig, }, + IDL: "ccip-router", }, }, } From e9fe535ad261c96c6cc22a3231660c23c4977cdb Mon Sep 17 00:00:00 2001 From: Silas Lenihan Date: Mon, 4 Nov 2024 11:24:36 -0500 Subject: [PATCH 09/22] Removed ValueSeeds and consolidated into a single Seeds array --- pkg/solana/chainwriter/chain_writer.go | 4 +-- pkg/solana/chainwriter/chain_writer_test.go | 39 +++++++++------------ 2 files changed, 18 insertions(+), 25 deletions(-) diff --git a/pkg/solana/chainwriter/chain_writer.go b/pkg/solana/chainwriter/chain_writer.go index 98fe74ae6..28618ff93 100644 --- a/pkg/solana/chainwriter/chain_writer.go +++ b/pkg/solana/chainwriter/chain_writer.go @@ -76,9 +76,7 @@ type PDALookups struct { // there will be multiple PDAs generated by combining each PublicKey with the seeds. PublicKey Lookup // Seeds to be derived from an additional lookup - AddressSeeds []Lookup - // Seeds to be derived from a value in the decoded data - ValueSeeds []ValueLookup + Seeds []Lookup IsSigner bool IsWritable bool } diff --git a/pkg/solana/chainwriter/chain_writer_test.go b/pkg/solana/chainwriter/chain_writer_test.go index 94f179cac..655294dac 100644 --- a/pkg/solana/chainwriter/chain_writer_test.go +++ b/pkg/solana/chainwriter/chain_writer_test.go @@ -94,11 +94,9 @@ func TestGetAddresses(t *testing.T) { IsSigner: false, IsWritable: false, }, - // AddressSeeds would be used if the user needed to look up addresses to use as seeds, which isn't the case here. - AddressSeeds: nil, - // ValueSeeds tells the ChainWriter where to look in the input parameters to get the seeds for the PDA account. - ValueSeeds: []chainwriter.ValueLookup{ - {Location: "Message.TokenAmounts.DestTokenAddress"}, + // Seeds would be used if the user needed to look up addresses to use as seeds, which isn't the case here. + Seeds: []chainwriter.Lookup{ + chainwriter.ValueLookup{Location: "Message.TokenAmounts.DestTokenAddress"}, }, IsSigner: false, IsWritable: false, @@ -126,7 +124,7 @@ func TestGetAddresses(t *testing.T) { // -> constant // PDALookups can resolve to multiple addresses if: // A) The PublicKey lookup resolves to multiple addresses (i.e. multiple token addresses) - // B) The AddressSeeds or ValueSeeds resolve to multiple values + // B) The Seeds or ValueSeeds resolve to multiple values chainwriter.PDALookups{ Name: "PerChainRateLimit", // PublicKey is a constant account in this case, not a lookup. @@ -135,11 +133,9 @@ func TestGetAddresses(t *testing.T) { IsSigner: false, IsWritable: false, }, - AddressSeeds: nil, // Similar to the RegistryTokenState above, the user is looking up PDA accounts based on the dest tokens. - ValueSeeds: []chainwriter.ValueLookup{ - // If there are multiple tokens within the report, this will result in multiple PDA accounts - {Location: "Message.TokenAmounts.DestTokenAddress"}, + Seeds: []chainwriter.Lookup{ + chainwriter.ValueLookup{Location: "Message.TokenAmounts.DestTokenAddress"}, }, IsSigner: false, IsWritable: false, @@ -171,7 +167,7 @@ func TestGetAddresses(t *testing.T) { IsWritable: false, }, // The seed is the receiver address. - AddressSeeds: []chainwriter.Lookup{ + Seeds: []chainwriter.Lookup{ chainwriter.AccountLookup{ Name: "Receiver", Location: "Message.Receiver", @@ -196,10 +192,9 @@ func TestGetAddresses(t *testing.T) { IsSigner: false, IsWritable: false, }, - AddressSeeds: nil, // The seed, once again, is the destination token address. - ValueSeeds: []chainwriter.ValueLookup{ - {Location: "Message.TokenAmounts.DestTokenAddress"}, + Seeds: []chainwriter.Lookup{ + chainwriter.ValueLookup{Location: "Message.TokenAmounts.DestTokenAddress"}, }, IsSigner: false, IsWritable: false, @@ -227,10 +222,11 @@ func TestGetAddresses(t *testing.T) { IsSigner: false, IsWritable: false, }, - AddressSeeds: nil, - ValueSeeds: []chainwriter.ValueLookup{ - // The seed is the merkle root of the report, as passed into the input params. - {Location: "args.MerkleRoot"}, + Seeds: []chainwriter.Lookup{ + chainwriter.ValueLookup{ + // The seed is the merkle root of the report, as passed into the input params. + Location: "args.MerkleRoot", + }, }, IsSigner: false, IsWritable: false, @@ -244,12 +240,11 @@ func TestGetAddresses(t *testing.T) { IsSigner: false, IsWritable: false, }, - AddressSeeds: nil, // In this case, the user configured multiple seeds. These will be used in conjunction // with the public key to generate one or multiple PDA accounts. - ValueSeeds: []chainwriter.ValueLookup{ - {Location: "Message.Receiver"}, - {Location: "Message.DestChainSelector"}, + Seeds: []chainwriter.Lookup{ + chainwriter.ValueLookup{Location: "Message.Receiver"}, + chainwriter.ValueLookup{Location: "Message.DestChainSelector"}, }, }, // Account constant From ed8ca3bd744e444c757d498cf977717fadaecbf8 Mon Sep 17 00:00:00 2001 From: Silas Lenihan Date: Mon, 4 Nov 2024 11:48:07 -0500 Subject: [PATCH 10/22] Added decode location --- pkg/solana/chainwriter/chain_writer.go | 3 ++- pkg/solana/chainwriter/chain_writer_test.go | 2 ++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/pkg/solana/chainwriter/chain_writer.go b/pkg/solana/chainwriter/chain_writer.go index 28618ff93..fe338dc02 100644 --- a/pkg/solana/chainwriter/chain_writer.go +++ b/pkg/solana/chainwriter/chain_writer.go @@ -41,6 +41,7 @@ type ProgramConfig struct { type MethodConfig struct { InputModifications commoncodec.ModifiersConfig EncodedTypeIDL string + DecodeLocation string DataType reflect.Type DecodedTypeName string ChainSpecificName string @@ -76,7 +77,7 @@ type PDALookups struct { // there will be multiple PDAs generated by combining each PublicKey with the seeds. PublicKey Lookup // Seeds to be derived from an additional lookup - Seeds []Lookup + Seeds []Lookup IsSigner bool IsWritable bool } diff --git a/pkg/solana/chainwriter/chain_writer_test.go b/pkg/solana/chainwriter/chain_writer_test.go index 655294dac..7250a1f26 100644 --- a/pkg/solana/chainwriter/chain_writer_test.go +++ b/pkg/solana/chainwriter/chain_writer_test.go @@ -68,6 +68,8 @@ func TestGetAddresses(t *testing.T) { }, }, EncodedTypeIDL: executionReportSingleChainIDL, + // Location in the args where the object to decode is located. + DecodeLocation: "Report", DataType: reflect.TypeOf(ExecutionReportSingleChain{}), DecodedTypeName: "ExecutionReportSingleChain", ChainSpecificName: "execute", From 9648729b78d0c3bdabc12f636a0f43b30e77c966 Mon Sep 17 00:00:00 2001 From: Silas Lenihan Date: Mon, 4 Nov 2024 13:03:37 -0500 Subject: [PATCH 11/22] Added commit report config example --- pkg/solana/chainwriter/chain_writer.go | 41 +++++--- pkg/solana/chainwriter/chain_writer_test.go | 109 ++++++++++++++------ 2 files changed, 105 insertions(+), 45 deletions(-) diff --git a/pkg/solana/chainwriter/chain_writer.go b/pkg/solana/chainwriter/chain_writer.go index fe338dc02..f3d185fcd 100644 --- a/pkg/solana/chainwriter/chain_writer.go +++ b/pkg/solana/chainwriter/chain_writer.go @@ -39,6 +39,7 @@ type ProgramConfig struct { } type MethodConfig struct { + FromAddress string InputModifications commoncodec.ModifiersConfig EncodedTypeIDL string DecodeLocation string @@ -428,15 +429,22 @@ func (s *SolanaChainWriterService) SubmitTransaction(ctx context.Context, contra if err != nil { return fmt.Errorf("error unmarshalling IDL: %w", err) } + // create codec from configured method IDL cwCodec, err := codec.NewIDLAccountCodec(idl, binary.LittleEndian()) if err != nil { return fmt.Errorf("error creating new IDLAccountCodec: %w", err) } + // get inner encoded data from the encoded args + encoded, err := GetValueAtLocation(data, methodConfig.DecodeLocation) + if err != nil { + return fmt.Errorf("error getting value at location: %w", err) + } // Create an instance of the type defined by methodConfig.DataType decoded := reflect.New(methodConfig.DataType).Interface() - err = cwCodec.Decode(ctx, data, decoded, methodConfig.DecodedTypeName) + err = cwCodec.Decode(ctx, encoded[0], decoded, methodConfig.DecodedTypeName) + // Configure debug ID debugID := "" if methodConfig.DebugIDLocation != "" { debugID, err = GetDebugIDAtLocation(decoded, methodConfig.DebugIDLocation) @@ -444,20 +452,20 @@ func (s *SolanaChainWriterService) SubmitTransaction(ctx context.Context, contra return errorWithDebugID(fmt.Errorf("error getting debug ID from decoded data: %w", err), debugID) } } - derivedTableAccounts, lookupTableAddresses, derivedTableMap, err := s.getDerivedTableMap(ctx, methodConfig.DerivedLookupTables, debugID) + + // Read lookup tables from on-chain + lookupTableAddresses, derivedTableMap, err := s.getDerivedTableMap(ctx, methodConfig.DerivedLookupTables, debugID) if err != nil { return errorWithDebugID(fmt.Errorf("error getting readable table map: %w", err), debugID) } + + // Lookup configured account addresses from decoded data accounts, err := s.GetAddresses(ctx, decoded, methodConfig.Accounts, derivedTableMap, debugID) - accounts = append(accounts, derivedTableAccounts...) if err != nil { return errorWithDebugID(fmt.Errorf("error getting addresses from decoded data: %w", err), debugID) } - lookupTables, err := s.GetLookupTables(ctx, append(methodConfig.LookupTables, lookupTableAddresses...), debugID) - if err != nil { - return errorWithDebugID(fmt.Errorf("error getting lookup tables from decoded data: %w", err), debugID) - } + // get current latest blockhash, this can be overwritten by the TXM blockhash, err := s.reader.LatestBlockhash(ctx) programId, err := solana.PublicKeyFromBase58(contractName) @@ -465,17 +473,26 @@ func (s *SolanaChainWriterService) SubmitTransaction(ctx context.Context, contra return errorWithDebugID(fmt.Errorf("Error getting programId: %w", err), debugID) } - // This isn't a real method, TBD how we will get this - feePayer := accounts[0] + // Re-encode payload, apply modifiers and borsh-encode + encodedPayload, err := cwCodec.Encode(ctx, decoded, methodConfig.DecodedTypeName) + if err != nil { + return errorWithDebugID(fmt.Errorf("error encoding data: %w", err), debugID) + } + + feePayer, err := solana.PublicKeyFromBase58(methodConfig.FromAddress) + if err != nil { + return errorWithDebugID(fmt.Errorf("error getting fee payer: %w", err), debugID) + } tx, err := solana.NewTransaction( []solana.Instruction{ - solana.NewInstruction(programId, accounts, data), + solana.NewInstruction(programId, accounts, encodedPayload), }, blockhash.Value.Blockhash, - solana.TransactionPayer(feePayer.PublicKey), - solana.TransactionAddressTables(lookupTables), + solana.TransactionPayer(feePayer), + solana.TransactionAddressTables(lookupTableAddresses), ) + if err != nil { return errorWithDebugID(fmt.Errorf("error creating new transaction: %w", err), debugID) } diff --git a/pkg/solana/chainwriter/chain_writer_test.go b/pkg/solana/chainwriter/chain_writer_test.go index 7250a1f26..3afa2e6e4 100644 --- a/pkg/solana/chainwriter/chain_writer_test.go +++ b/pkg/solana/chainwriter/chain_writer_test.go @@ -7,35 +7,9 @@ import ( commoncodec "github.com/smartcontractkit/chainlink-common/pkg/codec" "github.com/smartcontractkit/chainlink-solana/pkg/solana/chainwriter" -) - -type ExecutionReportSingleChain struct { - SourceChainSelector uint64 `json:"source_chain_selector"` - Message Any2SolanaRampMessage `json:"message"` - Root [32]byte `json:"root"` - Proofs [][]byte `json:"proofs"` -} - -type Any2SolanaRampMessage struct { - Header RampMessageHeader `json:"header"` - Sender []byte `json:"sender"` - Data []byte `json:"data"` - Receiver [32]byte `json:"receiver"` - ExtraArgs SolanaExtraArgs `json:"extra_args"` -} -type RampMessageHeader struct { - MessageID [32]byte `json:"message_id"` - SourceChainSelector uint64 `json:"source_chain_selector"` - DestChainSelector uint64 `json:"dest_chain_selector"` - SequenceNumber uint64 `json:"sequence_number"` - Nonce uint64 `json:"nonce"` -} - -type SolanaExtraArgs struct { - ComputeUnits uint32 `json:"compute_units"` - AllowOutOfOrderExecution bool `json:"allow_out_of_order_execution"` -} + ccipocr3 "github.com/smartcontractkit/chainlink-common/pkg/types/ccipocr3" +) type RegistryTokenState struct { PoolProgram [32]byte `json:"pool_program"` @@ -56,22 +30,24 @@ func TestGetAddresses(t *testing.T) { sysvarProgramAddress := "4Nn9dsYBcSTzRbK9hg9kzCUdrCSkMZq1UR6Vw1Tkaf6G" commonAddressesLookupTable := "4Nn9dsYBcSTzRbK9hg9kzCUdrCSkMZq1UR6Vw1Tkaf6H" routerLookupTable := "4Nn9dsYBcSTzRbK9hg9kzCUdrCSkMZq1UR6Vw1Tkaf6I" + userAddress := "4Nn9dsYBcSTzRbK9hg9kzCUdrCSkMZq1UR6Vw1Tkaf6J" executionReportSingleChainIDL := `{"name":"ExecutionReportSingleChain","type":{"kind":"struct","fields":[{"name":"source_chain_selector","type":"u64"},{"name":"message","type":{"defined":"Any2SolanaRampMessage"}},{"name":"root","type":{"array":["u8",32]}},{"name":"proofs","type":{"vec":{"array":["u8",32]}}}]}},{"name":"Any2SolanaRampMessage","type":{"kind":"struct","fields":[{"name":"header","type":{"defined":"RampMessageHeader"}},{"name":"sender","type":{"vec":"u8"}},{"name":"data","type":{"vec":"u8"}},{"name":"receiver","type":{"array":["u8",32]}},{"name":"extra_args","type":{"defined":"SolanaExtraArgs"}}]}},{"name":"RampMessageHeader","type":{"kind":"struct","fields":[{"name":"message_id","type":{"array":["u8",32]}},{"name":"source_chain_selector","type":"u64"},{"name":"dest_chain_selector","type":"u64"},{"name":"sequence_number","type":"u64"},{"name":"nonce","type":"u64"}]}},{"name":"SolanaExtraArgs","type":{"kind":"struct","fields":[{"name":"compute_units","type":"u32"},{"name":"allow_out_of_order_execution","type":"bool"}]}}` - // registryTokenStateIDL := `{"name":"RegistryTokenState","type":"struct","fields":[{"name":"pool_program","type":{"array":[{"type":"u8"},32]}},{"name":"pool_config","type":{"array":[{"type":"u8"},32]}},{"name":"token_program","type":{"array":[{"type":"u8"},32]}},{"name":"token_state","type":{"array":[{"type":"u8"},32]}},{"name":"pool_associated_token_account","type":{"array":[{"type":"u8"},32]}}]}` + commitInputIDL := `{"name":"CommitInput","type":{"kind":"struct","fields":[{"name":"price_updates","type":{"defined":"PriceUpdates"}},{"name":"merkle_root","type":{"defined":"MerkleRoot"}}]}},{"name":"PriceUpdates","type":{"kind":"struct","fields":[{"name":"token_price_updates","type":{"vec":{"defined":"TokenPriceUpdate"}}},{"name":"gas_price_updates","type":{"vec":{"defined":"GasPriceUpdate"}}}]}},{"name":"TokenPriceUpdate","type":{"kind":"struct","fields":[{"name":"source_token","type":"publicKey"},{"name":"usd_per_token","type":{"array":["u8",28]}}]}},{"name":"GasPriceUpdate","type":{"kind":"struct","fields":[{"name":"dest_chain_selector","type":"u64"},{"name":"usd_per_unit_gas","type":{"array":["u8",28]}}]}},{"name":"MerkleRoot","type":{"kind":"struct","fields":[{"name":"source_chain_selector","type":"u64"},{"name":"on_ramp_address","type":{"vec":"u8"}},{"name":"min_seq_nr","type":"u64"},{"name":"max_seq_nr","type":"u64"},{"name":"merkle_root","type":{"array":["u8",32]}}]}}` executeConfig := chainwriter.MethodConfig{ + FromAddress: userAddress, InputModifications: commoncodec.ModifiersConfig{ // remove merkle root since it isn't a part of the on-chain type &commoncodec.DropModifierConfig{ Fields: []string{"Message.ExtraArgs.MerkleRoot"}, }, }, - EncodedTypeIDL: executionReportSingleChainIDL, + EncodedTypeIDL: executionReportSingleChainIDL, // Location in the args where the object to decode is located. DecodeLocation: "Report", - DataType: reflect.TypeOf(ExecutionReportSingleChain{}), - DecodedTypeName: "ExecutionReportSingleChain", + DataType: reflect.TypeOf(ccipocr3.ExecutePluginReportSingleChain{}), + DecodedTypeName: "ExecutePluginReportSingleChain", ChainSpecificName: "execute", // LookupTables are on-chain stores of accounts. They can be used in two ways: // 1. As a way to store a list of accounts that are all associated together (i.e. Token State registry) @@ -145,7 +121,7 @@ func TestGetAddresses(t *testing.T) { // Lookup Table content - Get the accounts from the derived lookup table above chainwriter.AccountsFromLookupTable{ LookupTablesName: "RegistryTokenState", - IncludeIndexes: []int{1, 4}, // If left empty, all addresses will be included. + IncludeIndexes: []int{}, // If left empty, all addresses will be included. Otherwise, only the specified indexes will be included. }, // Account Lookup - Based on data from input parameters // In this case, the user wants to add the destination token addresses to the transaction. @@ -283,11 +259,78 @@ func TestGetAddresses(t *testing.T) { DebugIDLocation: "Message.MessageID", } + commitConfig := chainwriter.MethodConfig{ + FromAddress: userAddress, + InputModifications: nil, + EncodedTypeIDL: commitInputIDL, + DecodeLocation: "Report", + DataType: reflect.TypeOf(ccipocr3.CommitPluginReport{}), + DecodedTypeName: "CommitPluginReport", + ChainSpecificName: "commit", + LookupTables: chainwriter.LookupTables{ + StaticLookupTables: []string{ + commonAddressesLookupTable, + routerLookupTable, + }, + }, + Accounts: []chainwriter.Lookup{ + + // PDA lookup to get the Router Report Accounts. + chainwriter.PDALookups{ + Name: "RouterReportAccount", + // The public key is a constant Router address. + PublicKey: chainwriter.AccountConstant{ + Address: routerProgramAddress, + IsSigner: false, + IsWritable: false, + }, + Seeds: []chainwriter.Lookup{ + chainwriter.ValueLookup{ + // The seed is the merkle root of the report, as passed into the input params. + Location: "args.MerkleRoots", + }, + }, + IsSigner: false, + IsWritable: false, + }, + // Account constant + chainwriter.AccountConstant{ + Name: "CPISigner", + Address: cpiSignerAddress, + IsSigner: true, + IsWritable: false, + }, + // Account constant + chainwriter.AccountConstant{ + Name: "SystemProgram", + Address: systemProgramAddress, + IsSigner: true, + IsWritable: false, + }, + // Account constant + chainwriter.AccountConstant{ + Name: "ComputeBudgetProgram", + Address: computeBudgetProgramAddress, + IsSigner: true, + IsWritable: false, + }, + // Account constant + chainwriter.AccountConstant{ + Name: "SysvarProgram", + Address: sysvarProgramAddress, + IsSigner: true, + IsWritable: false, + }, + }, + DebugIDLocation: "", + } + chainWriterConfig := chainwriter.ChainWriterConfig{ Programs: map[string]chainwriter.ProgramConfig{ "ccip-router": { Methods: map[string]chainwriter.MethodConfig{ "execute": executeConfig, + "commit": commitConfig, }, IDL: "ccip-router", }, From e48659471db5189c667dc7ccf6e8d732c01c1317 Mon Sep 17 00:00:00 2001 From: Silas Lenihan Date: Wed, 6 Nov 2024 15:16:59 -0500 Subject: [PATCH 12/22] Slight changes to IDL and codec --- pkg/solana/chainwriter/chain_writer.go | 22 +++++---------------- pkg/solana/chainwriter/chain_writer_test.go | 5 +---- 2 files changed, 6 insertions(+), 21 deletions(-) diff --git a/pkg/solana/chainwriter/chain_writer.go b/pkg/solana/chainwriter/chain_writer.go index f3d185fcd..e7a24da68 100644 --- a/pkg/solana/chainwriter/chain_writer.go +++ b/pkg/solana/chainwriter/chain_writer.go @@ -41,13 +41,12 @@ type ProgramConfig struct { type MethodConfig struct { FromAddress string InputModifications commoncodec.ModifiersConfig - EncodedTypeIDL string DecodeLocation string DataType reflect.Type DecodedTypeName string ChainSpecificName string - Accounts []Lookup LookupTables LookupTables + Accounts []Lookup // Location in the decoded data where the debug ID is stored DebugIDLocation string } @@ -423,17 +422,6 @@ func (s *SolanaChainWriterService) SubmitTransaction(ctx context.Context, contra return fmt.Errorf("Unable to convert args to []byte") } - // decode data - var idl codec.IDL - err := json.Unmarshal([]byte(methodConfig.EncodedTypeIDL), &idl) - if err != nil { - return fmt.Errorf("error unmarshalling IDL: %w", err) - } - // create codec from configured method IDL - cwCodec, err := codec.NewIDLAccountCodec(idl, binary.LittleEndian()) - if err != nil { - return fmt.Errorf("error creating new IDLAccountCodec: %w", err) - } // get inner encoded data from the encoded args encoded, err := GetValueAtLocation(data, methodConfig.DecodeLocation) if err != nil { @@ -442,7 +430,7 @@ func (s *SolanaChainWriterService) SubmitTransaction(ctx context.Context, contra // Create an instance of the type defined by methodConfig.DataType decoded := reflect.New(methodConfig.DataType).Interface() - err = cwCodec.Decode(ctx, encoded[0], decoded, methodConfig.DecodedTypeName) + err = s.codec.Decode(ctx, encoded[0], decoded, methodConfig.DecodedTypeName) // Configure debug ID debugID := "" @@ -473,8 +461,8 @@ func (s *SolanaChainWriterService) SubmitTransaction(ctx context.Context, contra return errorWithDebugID(fmt.Errorf("Error getting programId: %w", err), debugID) } - // Re-encode payload, apply modifiers and borsh-encode - encodedPayload, err := cwCodec.Encode(ctx, decoded, methodConfig.DecodedTypeName) + // Encode payload for chain, apply modifiers and borsh-encode + encodedPayload, err := s.codec.Encode(ctx, args, codec.WrapItemType(contract, method, true)) if err != nil { return errorWithDebugID(fmt.Errorf("error encoding data: %w", err), debugID) } @@ -497,7 +485,7 @@ func (s *SolanaChainWriterService) SubmitTransaction(ctx context.Context, contra return errorWithDebugID(fmt.Errorf("error creating new transaction: %w", err), debugID) } - if err = s.txm.Enqueue(ctx, accounts[0].PublicKey.String(), tx); err != nil { + if err = s.txm.Enqueue(ctx, accounts[0].PublicKey.String(), tx, transactionID); err != nil { return errorWithDebugID(fmt.Errorf("error on sending trasnaction to TXM: %w", err), debugID) } return nil diff --git a/pkg/solana/chainwriter/chain_writer_test.go b/pkg/solana/chainwriter/chain_writer_test.go index 3afa2e6e4..1b986a361 100644 --- a/pkg/solana/chainwriter/chain_writer_test.go +++ b/pkg/solana/chainwriter/chain_writer_test.go @@ -33,7 +33,6 @@ func TestGetAddresses(t *testing.T) { userAddress := "4Nn9dsYBcSTzRbK9hg9kzCUdrCSkMZq1UR6Vw1Tkaf6J" executionReportSingleChainIDL := `{"name":"ExecutionReportSingleChain","type":{"kind":"struct","fields":[{"name":"source_chain_selector","type":"u64"},{"name":"message","type":{"defined":"Any2SolanaRampMessage"}},{"name":"root","type":{"array":["u8",32]}},{"name":"proofs","type":{"vec":{"array":["u8",32]}}}]}},{"name":"Any2SolanaRampMessage","type":{"kind":"struct","fields":[{"name":"header","type":{"defined":"RampMessageHeader"}},{"name":"sender","type":{"vec":"u8"}},{"name":"data","type":{"vec":"u8"}},{"name":"receiver","type":{"array":["u8",32]}},{"name":"extra_args","type":{"defined":"SolanaExtraArgs"}}]}},{"name":"RampMessageHeader","type":{"kind":"struct","fields":[{"name":"message_id","type":{"array":["u8",32]}},{"name":"source_chain_selector","type":"u64"},{"name":"dest_chain_selector","type":"u64"},{"name":"sequence_number","type":"u64"},{"name":"nonce","type":"u64"}]}},{"name":"SolanaExtraArgs","type":{"kind":"struct","fields":[{"name":"compute_units","type":"u32"},{"name":"allow_out_of_order_execution","type":"bool"}]}}` - commitInputIDL := `{"name":"CommitInput","type":{"kind":"struct","fields":[{"name":"price_updates","type":{"defined":"PriceUpdates"}},{"name":"merkle_root","type":{"defined":"MerkleRoot"}}]}},{"name":"PriceUpdates","type":{"kind":"struct","fields":[{"name":"token_price_updates","type":{"vec":{"defined":"TokenPriceUpdate"}}},{"name":"gas_price_updates","type":{"vec":{"defined":"GasPriceUpdate"}}}]}},{"name":"TokenPriceUpdate","type":{"kind":"struct","fields":[{"name":"source_token","type":"publicKey"},{"name":"usd_per_token","type":{"array":["u8",28]}}]}},{"name":"GasPriceUpdate","type":{"kind":"struct","fields":[{"name":"dest_chain_selector","type":"u64"},{"name":"usd_per_unit_gas","type":{"array":["u8",28]}}]}},{"name":"MerkleRoot","type":{"kind":"struct","fields":[{"name":"source_chain_selector","type":"u64"},{"name":"on_ramp_address","type":{"vec":"u8"}},{"name":"min_seq_nr","type":"u64"},{"name":"max_seq_nr","type":"u64"},{"name":"merkle_root","type":{"array":["u8",32]}}]}}` executeConfig := chainwriter.MethodConfig{ FromAddress: userAddress, @@ -43,7 +42,6 @@ func TestGetAddresses(t *testing.T) { Fields: []string{"Message.ExtraArgs.MerkleRoot"}, }, }, - EncodedTypeIDL: executionReportSingleChainIDL, // Location in the args where the object to decode is located. DecodeLocation: "Report", DataType: reflect.TypeOf(ccipocr3.ExecutePluginReportSingleChain{}), @@ -262,7 +260,6 @@ func TestGetAddresses(t *testing.T) { commitConfig := chainwriter.MethodConfig{ FromAddress: userAddress, InputModifications: nil, - EncodedTypeIDL: commitInputIDL, DecodeLocation: "Report", DataType: reflect.TypeOf(ccipocr3.CommitPluginReport{}), DecodedTypeName: "CommitPluginReport", @@ -332,7 +329,7 @@ func TestGetAddresses(t *testing.T) { "execute": executeConfig, "commit": commitConfig, }, - IDL: "ccip-router", + IDL: executionReportSingleChainIDL, }, }, } From ecb1629c9d8e262ee6f9b8b03ecd6f6cd0d9d548 Mon Sep 17 00:00:00 2001 From: Silas Lenihan Date: Fri, 15 Nov 2024 14:08:56 -0500 Subject: [PATCH 13/22] Updated ChainWriter implementation to reflect new design changes --- pkg/solana/chainwriter/chain_writer.go | 379 +++++++++++--------- pkg/solana/chainwriter/chain_writer_test.go | 26 +- pkg/solana/chainwriter/helpers.go | 12 +- 3 files changed, 214 insertions(+), 203 deletions(-) diff --git a/pkg/solana/chainwriter/chain_writer.go b/pkg/solana/chainwriter/chain_writer.go index e7a24da68..51aefed50 100644 --- a/pkg/solana/chainwriter/chain_writer.go +++ b/pkg/solana/chainwriter/chain_writer.go @@ -2,16 +2,13 @@ package chainwriter import ( "context" - "encoding/json" "fmt" "math/big" - "reflect" "github.com/gagliardetto/solana-go" "github.com/gagliardetto/solana-go/rpc" commoncodec "github.com/smartcontractkit/chainlink-common/pkg/codec" - "github.com/smartcontractkit/chainlink-common/pkg/codec/encodings/binary" "github.com/smartcontractkit/chainlink-common/pkg/services" "github.com/smartcontractkit/chainlink-common/pkg/types" @@ -41,17 +38,15 @@ type ProgramConfig struct { type MethodConfig struct { FromAddress string InputModifications commoncodec.ModifiersConfig - DecodeLocation string - DataType reflect.Type - DecodedTypeName string ChainSpecificName string LookupTables LookupTables Accounts []Lookup - // Location in the decoded data where the debug ID is stored + // Location in the args where the debug ID is stored DebugIDLocation string } type Lookup interface { + Resolve(ctx context.Context, args any, derivedTableMap map[string]map[string][]*solana.AccountMeta, debugID string) ([]*solana.AccountMeta, error) } // AccountConstant represents a fixed address, provided in Base58 format, converted into a `solana.PublicKey`. @@ -62,7 +57,7 @@ type AccountConstant struct { IsWritable bool } -// AccountLookup dynamically derives an account address from decoded data using a specified location path. +// AccountLookup dynamically derives an account address from args using a specified location path. type AccountLookup struct { Name string Location string @@ -94,10 +89,8 @@ type LookupTables struct { // DerivedLookupTable represents a lookup table that is used to derive addresses for a program. type DerivedLookupTable struct { - Name string - Accounts Lookup - EncodedTypeIDL string - DecodedType reflect.Type + Name string + Accounts Lookup } // AccountsFromLookupTable extracts accounts from a lookup table that was previously read and stored in memory. @@ -121,7 +114,7 @@ for Solana transactions. It handles constant addresses, dynamic lookups, program ### Parameters: - `ctx`: Context for request lifecycle management. -- `decoded`: Decoded data used for dynamic lookups. +- `args`: Input arguments used for dynamic lookups. - `accounts`: List of `Lookup` configurations specifying how addresses are derived. - `derivedTableMap`: Map of pre-loaded lookup table addresses. - `debugID`: Debug identifier for tracing errors. @@ -135,12 +128,12 @@ for Solana transactions. It handles constant addresses, dynamic lookups, program - Example: A pre-defined fee payer or system account. 2. **AccountLookup**: - - Dynamically derived from decoded data using a specified location path (e.g., `user.walletAddress`). + - Dynamically derived from input args using a specified location path (e.g., `user.walletAddress`). - If the lookup table is pre-loaded, the address is fetched from `derivedTableMap`. 3. **PDALookups**: - Generates Program Derived Addresses (PDA) by combining a derived public key with one or more seeds. - - Seeds can be `AddressSeeds` (public keys from the decoded data) or `ValueSeeds` (byte arrays). + - Seeds can be `AddressSeeds` (public keys from the input args) or `ValueSeeds` (byte arrays). - Ensures there is only one public key if multiple seeds are provided. ### Error Handling: @@ -148,10 +141,10 @@ for Solana transactions. It handles constant addresses, dynamic lookups, program */ // GetAddresses resolves account addresses from various `Lookup` configurations to build the required `solana.AccountMeta` list // for Solana transactions. -func (s *SolanaChainWriterService) GetAddresses(ctx context.Context, decoded any, accounts []Lookup, derivedTableMap map[string][]*solana.AccountMeta, debugID string) ([]*solana.AccountMeta, error) { +func GetAddresses(ctx context.Context, args any, accounts []Lookup, derivedTableMap map[string]map[string][]*solana.AccountMeta, debugID string) ([]*solana.AccountMeta, error) { var addresses []*solana.AccountMeta for _, accountConfig := range accounts { - meta, err := s.getAccountMeta(ctx, decoded, accountConfig, derivedTableMap, debugID) + meta, err := accountConfig.Resolve(ctx, args, derivedTableMap, debugID) if err != nil { return nil, err } @@ -160,42 +153,22 @@ func (s *SolanaChainWriterService) GetAddresses(ctx context.Context, decoded any return addresses, nil } -// getAccountMeta processes a single account configuration and returns the corresponding `solana.AccountMeta` slice. -func (s *SolanaChainWriterService) getAccountMeta(ctx context.Context, decoded any, accountConfig Lookup, derivedTableMap map[string][]*solana.AccountMeta, debugID string) ([]*solana.AccountMeta, error) { - switch lookup := accountConfig.(type) { - case AccountConstant: - return s.handleAccountConstant(lookup, debugID) - case AccountLookup: - return s.handleAccountLookup(decoded, lookup, derivedTableMap, debugID) - case PDALookups: - return s.handlePDALookup(ctx, decoded, lookup, derivedTableMap, debugID) - default: - return nil, errorWithDebugID(fmt.Errorf("unsupported account type: %T", lookup), debugID) - } -} - -// handleAccountConstant processes an `AccountConstant` and returns the corresponding `solana.AccountMeta`. -func (s *SolanaChainWriterService) handleAccountConstant(lookup AccountConstant, debugID string) ([]*solana.AccountMeta, error) { - address, err := solana.PublicKeyFromBase58(lookup.Address) +func (ac AccountConstant) Resolve(_ context.Context, _ any, _ map[string]map[string][]*solana.AccountMeta, debugID string) ([]*solana.AccountMeta, error) { + address, err := solana.PublicKeyFromBase58(ac.Address) if err != nil { return nil, errorWithDebugID(fmt.Errorf("error getting account from constant: %w", err), debugID) } return []*solana.AccountMeta{ { PublicKey: address, - IsSigner: lookup.IsSigner, - IsWritable: lookup.IsWritable, + IsSigner: ac.IsSigner, + IsWritable: ac.IsWritable, }, }, nil } -// handleAccountLookup processes an `AccountLookup` by either fetching from the lookup table or dynamically deriving the address. -func (s *SolanaChainWriterService) handleAccountLookup(decoded any, lookup AccountLookup, derivedTableMap map[string][]*solana.AccountMeta, debugID string) ([]*solana.AccountMeta, error) { - if derivedAddresses, ok := derivedTableMap[lookup.Name]; ok { - return derivedAddresses, nil - } - - derivedAddresses, err := GetAddressAtLocation(decoded, lookup.Location, debugID) +func (al AccountLookup) Resolve(_ context.Context, args any, _ map[string]map[string][]*solana.AccountMeta, debugID string) ([]*solana.AccountMeta, error) { + derivedAddresses, err := GetAddressAtLocation(args, al.Location, debugID) if err != nil { return nil, errorWithDebugID(fmt.Errorf("error getting account from lookup: %w", err), debugID) } @@ -204,37 +177,66 @@ func (s *SolanaChainWriterService) handleAccountLookup(decoded any, lookup Accou for _, address := range derivedAddresses { metas = append(metas, &solana.AccountMeta{ PublicKey: address, - IsSigner: lookup.IsSigner, - IsWritable: lookup.IsWritable, + IsSigner: al.IsSigner, + IsWritable: al.IsWritable, }) } return metas, nil } -// handlePDALookup processes a `PDALookups` by resolving seeds and generating the PDA address. -func (s *SolanaChainWriterService) handlePDALookup(ctx context.Context, decoded any, lookup PDALookups, derivedTableMap map[string][]*solana.AccountMeta, debugID string) ([]*solana.AccountMeta, error) { - publicKeys, err := s.GetAddresses(ctx, decoded, []Lookup{lookup.PublicKey}, derivedTableMap, debugID) +func (alt AccountsFromLookupTable) Resolve(_ context.Context, _ any, derivedTableMap map[string]map[string][]*solana.AccountMeta, debugID string) ([]*solana.AccountMeta, error) { + // Fetch the inner map for the specified lookup table name + innerMap, ok := derivedTableMap[alt.LookupTablesName] + if !ok { + return nil, errorWithDebugID(fmt.Errorf("lookup table not found: %s", alt.LookupTablesName), debugID) + } + + var result []*solana.AccountMeta + + // If no indices are specified, include all addresses + if len(alt.IncludeIndexes) == 0 { + for _, metas := range innerMap { + result = append(result, metas...) + } + return result, nil + } + + // Otherwise, include only addresses at the specified indices + for publicKey, metas := range innerMap { + for _, index := range alt.IncludeIndexes { + if index < 0 || index >= len(metas) { + return nil, errorWithDebugID(fmt.Errorf("invalid index %d for account %s in lookup table %s", index, publicKey, alt.LookupTablesName), debugID) + } + result = append(result, metas[index]) + } + } + + return result, nil +} + +func (pda PDALookups) Resolve(ctx context.Context, args any, derivedTableMap map[string]map[string][]*solana.AccountMeta, debugID string) ([]*solana.AccountMeta, error) { + publicKeys, err := GetAddresses(ctx, args, []Lookup{pda.PublicKey}, derivedTableMap, debugID) if err != nil { return nil, errorWithDebugID(fmt.Errorf("error getting public key for PDALookups: %w", err), debugID) } - seeds, err := s.getSeedBytes(ctx, lookup, decoded, derivedTableMap, debugID) + seeds, err := getSeedBytes(ctx, pda, args, derivedTableMap, debugID) if err != nil { return nil, errorWithDebugID(fmt.Errorf("error getting seeds for PDALookups: %w", err), debugID) } - return s.generatePDAs(publicKeys, seeds, lookup, debugID) + return generatePDAs(publicKeys, seeds, pda, debugID) } // getSeedBytes extracts the seeds for the PDALookups. -// It handles both AddressSeeds (which are public keys) and ValueSeeds (which are byte arrays from decoded data). -func (s *SolanaChainWriterService) getSeedBytes(ctx context.Context, lookup PDALookups, decoded any, derivedTableMap map[string][]*solana.AccountMeta, debugID string) ([][]byte, error) { +// It handles both AddressSeeds (which are public keys) and ValueSeeds (which are byte arrays from input args). +func getSeedBytes(ctx context.Context, lookup PDALookups, args any, derivedTableMap map[string]map[string][]*solana.AccountMeta, debugID string) ([][]byte, error) { var seedBytes [][]byte // Process AddressSeeds first (e.g., public keys) - for _, seed := range lookup.AddressSeeds { + for _, seed := range lookup.Seeds { // Get the address(es) at the seed location - seedAddresses, err := s.GetAddresses(ctx, decoded, []Lookup{seed}, derivedTableMap, debugID) + seedAddresses, err := GetAddresses(ctx, args, []Lookup{seed}, derivedTableMap, debugID) if err != nil { return nil, errorWithDebugID(fmt.Errorf("error getting address seed: %w", err), debugID) } @@ -245,23 +247,11 @@ func (s *SolanaChainWriterService) getSeedBytes(ctx context.Context, lookup PDAL } } - // Process ValueSeeds (e.g., raw byte values found in decoded data) - for _, valueSeed := range lookup.ValueSeeds { - // Get the byte array value at the seed location - values, err := GetValueAtLocation(decoded, valueSeed.Location) - if err != nil { - return nil, errorWithDebugID(fmt.Errorf("error getting value seed: %w", err), debugID) - } - - // Add each value seed (which is a byte array) - seedBytes = append(seedBytes, values...) - } - return seedBytes, nil } // generatePDAs generates program-derived addresses (PDAs) from public keys and seeds. -func (s *SolanaChainWriterService) generatePDAs(publicKeys []*solana.AccountMeta, seeds [][]byte, lookup PDALookups, debugID string) ([]*solana.AccountMeta, error) { +func generatePDAs(publicKeys []*solana.AccountMeta, seeds [][]byte, lookup PDALookups, debugID string) ([]*solana.AccountMeta, error) { if len(seeds) > 1 && len(publicKeys) > 1 { return nil, errorWithDebugID(fmt.Errorf("multiple public keys and multiple seeds are not allowed"), debugID) } @@ -281,120 +271,104 @@ func (s *SolanaChainWriterService) generatePDAs(publicKeys []*solana.AccountMeta return addresses, nil } -func (s *SolanaChainWriterService) getDerivedTableMap(ctx context.Context, lookupTables []DerivedLookupTable, debugID string) ([]*solana.AccountMeta, []string, map[string][]*solana.AccountMeta, error) { - var accounts []*solana.AccountMeta - var lookupTableAddresses []string - var addressMap = make(map[string][]*solana.AccountMeta) +func (s *SolanaChainWriterService) getDerivedTableMap(ctx context.Context, lookupTables LookupTables, debugID string) (map[string]map[string][]*solana.AccountMeta, map[solana.PublicKey]solana.PublicKeySlice, error) { + derivedTableMap := make(map[string]map[string][]*solana.AccountMeta) + staticTableMap := make(map[solana.PublicKey]solana.PublicKeySlice) - for _, lookup := range lookupTables { - lookupTableMap, tableAddresses, err := s.LoadTable(lookup, ctx, s.reader, addressMap, debugID) + // Read derived lookup tables + for _, derivedLookup := range lookupTables.DerivedLookupTables { + lookupTableMap, _, err := s.LoadTable(derivedLookup, ctx, s.reader, derivedTableMap, debugID) if err != nil { - return nil, nil, nil, errorWithDebugID(fmt.Errorf("error loading lookup table: %w", err), debugID) - } - for _, address := range tableAddresses { - lookupTableAddresses = append(lookupTableAddresses, address.PublicKey.String()) + return nil, nil, errorWithDebugID(fmt.Errorf("error loading derived lookup table: %w", err), debugID) } - for name, addressList := range lookupTableMap { - for _, address := range addressList { - accounts = append(accounts, address) + + // Merge the loaded table map into the result + for tableName, innerMap := range lookupTableMap { + if derivedTableMap[tableName] == nil { + derivedTableMap[tableName] = make(map[string][]*solana.AccountMeta) + } + for accountKey, metas := range innerMap { + derivedTableMap[tableName][accountKey] = metas } - addressMap[name] = addressList } } - return accounts, lookupTableAddresses, addressMap, nil -} -// LoadTable fetches addresses specified by Identifier, loads data for each, and decodes it into solana.PublicKey slices. -func (s *SolanaChainWriterService) LoadTable(rlt DerivedLookupTable, ctx context.Context, reader client.Reader, derivedTableMap map[string][]*solana.AccountMeta, debugID string) (map[string][]*solana.AccountMeta, []*solana.AccountMeta, error) { - // Use GetAddresses to resolve all addresses specified by Identifier. - lookupTableAddresses, err := s.GetAddresses(ctx, nil, []Lookup{rlt.Identifier}, derivedTableMap, debugID) - if err != nil { - return nil, nil, errorWithDebugID(fmt.Errorf("error resolving addresses for lookup table: %w", err), debugID) - } + // Read static lookup tables + for _, staticTable := range lookupTables.StaticLookupTables { + // Parse the static table address + tableAddress, err := solana.PublicKeyFromBase58(staticTable) + if err != nil { + return nil, nil, errorWithDebugID(fmt.Errorf("invalid static lookup table address: %s, error: %w", staticTable, err), debugID) + } - // Map to store address metadata grouped by location. - resultMap := make(map[string][]*solana.AccountMeta) - for _, addressMeta := range lookupTableAddresses { - // Fetch account data for each resolved address. - accountInfo, err := reader.GetAccountInfoWithOpts(ctx, addressMeta.PublicKey, &rpc.GetAccountInfoOpts{ + // Fetch the account info for the static table + accountInfo, err := s.reader.GetAccountInfoWithOpts(ctx, tableAddress, &rpc.GetAccountInfoOpts{ Encoding: "base64", Commitment: rpc.CommitmentConfirmed, }) if err != nil || accountInfo == nil || accountInfo.Value == nil { - return nil, nil, errorWithDebugID(fmt.Errorf("error fetching account info for address %s: %w", addressMeta.PublicKey.String(), err), debugID) + return nil, nil, errorWithDebugID(fmt.Errorf("error fetching account info for static table: %s, error: %w", staticTable, err), debugID) } - // Decode account data based on the IDL specified in EncodedTypeIDL. - decodedData, err := rlt.decodeAccountData(accountInfo.Value.Data.GetBinary(), debugID) + // Decode the account data into an array of public keys + addresses, err := decodeLookupTable(accountInfo.Value.Data.GetBinary()) if err != nil { - return nil, nil, errorWithDebugID(fmt.Errorf("error decoding data for address %s: %w", addressMeta.PublicKey.String(), err), debugID) - } - - // Get derived addresses from the decoded data for each location specified. - for _, location := range rlt.Locations { - derivedAddresses, err := s.GetAddresses(ctx, decodedData, []Lookup{location}, derivedTableMap, debugID) - if err != nil { - return nil, nil, errorWithDebugID(fmt.Errorf("error resolving derived addresses: %w", err), debugID) - } - resultMap[fmt.Sprintf("%s.%s", rlt.Name, location.Name)] = derivedAddresses + return nil, nil, errorWithDebugID(fmt.Errorf("error decoding static lookup table data for %s: %w", staticTable, err), debugID) } - } - return resultMap, lookupTableAddresses, nil -} -// Decode account data for the DerivedLookupTable based on its EncodedTypeIDL. -func (rlt *DerivedLookupTable) decodeAccountData(data []byte, debugID string) (any, error) { - var idl codec.IDL - err := json.Unmarshal([]byte(rlt.EncodedTypeIDL), &idl) - if err != nil { - return nil, errorWithDebugID(fmt.Errorf("error unmarshalling IDL: %w", err), debugID) + // Add the static lookup table to the map + staticTableMap[tableAddress] = addresses } - cwCodec, err := codec.NewIDLAccountCodec(idl, binary.LittleEndian()) - if err != nil { - return nil, errorWithDebugID(fmt.Errorf("error creating IDLAccountCodec: %w", err), debugID) - } + return derivedTableMap, staticTableMap, nil +} - decoded := reflect.New(rlt.DecodedType).Interface() - err = cwCodec.Decode(nil, data, decoded, "") +func (s *SolanaChainWriterService) LoadTable(rlt DerivedLookupTable, ctx context.Context, reader client.Reader, derivedTableMap map[string]map[string][]*solana.AccountMeta, debugID string) (map[string]map[string][]*solana.AccountMeta, []*solana.AccountMeta, error) { + // Resolve all addresses specified by the identifier + lookupTableAddresses, err := GetAddresses(ctx, nil, []Lookup{rlt.Accounts}, nil, debugID) if err != nil { - return nil, errorWithDebugID(fmt.Errorf("error decoding account data: %w", err), debugID) + return nil, nil, errorWithDebugID(fmt.Errorf("error resolving addresses for lookup table: %w", err), debugID) } - return decoded, nil -} -func (s *SolanaChainWriterService) GetLookupTables(ctx context.Context, lookupTables []string, debugID string) (map[solana.PublicKey]solana.PublicKeySlice, error) { - tables := make(map[solana.PublicKey]solana.PublicKeySlice) - - for _, addressStr := range lookupTables { - // Convert the string address to solana.PublicKey - tableAddress, err := solana.PublicKeyFromBase58(addressStr) - if err != nil { - return nil, errorWithDebugID(fmt.Errorf("invalid lookup table address: %s, error: %w", addressStr, err), debugID) - } + resultMap := make(map[string]map[string][]*solana.AccountMeta) + var lookupTableMetas []*solana.AccountMeta - // Fetch the lookup table data from the blockchain - accountInfo, err := s.reader.GetAccountInfoWithOpts(ctx, tableAddress, &rpc.GetAccountInfoOpts{ + // Iterate over each address of the lookup table + for _, addressMeta := range lookupTableAddresses { + // Fetch account info + accountInfo, err := reader.GetAccountInfoWithOpts(ctx, addressMeta.PublicKey, &rpc.GetAccountInfoOpts{ Encoding: "base64", Commitment: rpc.CommitmentConfirmed, }) - if err != nil { - return nil, errorWithDebugID(fmt.Errorf("error fetching account info for lookup table %s: %w", addressStr, err), debugID) - } - if accountInfo == nil || accountInfo.Value == nil { - return nil, errorWithDebugID(fmt.Errorf("no data found for lookup table at address: %s", addressStr), debugID) + if err != nil || accountInfo == nil || accountInfo.Value == nil { + return nil, nil, errorWithDebugID(fmt.Errorf("error fetching account info for address %s: %w", addressMeta.PublicKey.String(), err), debugID) } - // Decode and extract public keys within the lookup table + // Decode the account data into an array of public keys addresses, err := decodeLookupTable(accountInfo.Value.Data.GetBinary()) if err != nil { - return nil, errorWithDebugID(fmt.Errorf("error decoding lookup table data for %s: %w", addressStr, err), debugID) + return nil, nil, errorWithDebugID(fmt.Errorf("error decoding lookup table data for address %s: %w", addressMeta.PublicKey.String(), err), debugID) + } + + // Create the inner map for this lookup table + if resultMap[rlt.Name] == nil { + resultMap[rlt.Name] = make(map[string][]*solana.AccountMeta) } - // Add the addresses to the lookup table map - tables[tableAddress] = addresses + // Populate the inner map (keyed by the account public key) + for _, addr := range addresses { + resultMap[rlt.Name][addr.String()] = append(resultMap[rlt.Name][addr.String()], &solana.AccountMeta{ + PublicKey: addr, + IsSigner: false, + IsWritable: false, + }) + } + + // Add the current lookup table address to the list of metas + lookupTableMetas = append(lookupTableMetas, addressMeta) } - return tables, nil + + return resultMap, lookupTableMetas, nil } func decodeLookupTable(data []byte) (solana.PublicKeySlice, error) { @@ -413,63 +387,109 @@ func decodeLookupTable(data []byte) (solana.PublicKeySlice, error) { return addresses, nil } -func (s *SolanaChainWriterService) SubmitTransaction(ctx context.Context, contractName, method string, args any, transactionID string, toAddress string, meta *types.TxMeta, value *big.Int) error { - programConfig := s.config.Programs[contractName] - methodConfig := programConfig.Methods[method] +func (s *SolanaChainWriterService) FilterLookupTableAddresses( + accounts []*solana.AccountMeta, + derivedTableMap map[string]map[string][]*solana.AccountMeta, + staticTableMap map[solana.PublicKey]solana.PublicKeySlice, + debugID string, +) map[solana.PublicKey]solana.PublicKeySlice { + filteredLookupTables := make(map[solana.PublicKey]solana.PublicKeySlice) - data, ok := args.([]byte) - if !ok { - return fmt.Errorf("Unable to convert args to []byte") + // Build a hash set of account public keys for fast lookup + usedAccounts := make(map[string]struct{}) + for _, account := range accounts { + usedAccounts[account.PublicKey.String()] = struct{}{} } - // get inner encoded data from the encoded args - encoded, err := GetValueAtLocation(data, methodConfig.DecodeLocation) - if err != nil { - return fmt.Errorf("error getting value at location: %w", err) + // Filter derived lookup tables + for _, innerMap := range derivedTableMap { + for innerIdentifier, metas := range innerMap { + tableKey, err := solana.PublicKeyFromBase58(innerIdentifier) + if err != nil { + errorWithDebugID(fmt.Errorf("error parsing lookup table key: %w", err), debugID) + } + + // Collect public keys that are actually used + var usedAddresses solana.PublicKeySlice + for _, meta := range metas { + if _, exists := usedAccounts[meta.PublicKey.String()]; exists { + usedAddresses = append(usedAddresses, meta.PublicKey) + } + } + + // Add to the filtered map if there are any used addresses + if len(usedAddresses) > 0 { + filteredLookupTables[tableKey] = usedAddresses + } + } + } + + // Filter static lookup tables + for tableKey, addresses := range staticTableMap { + var usedAddresses solana.PublicKeySlice + for _, staticAddress := range addresses { + if _, exists := usedAccounts[staticAddress.String()]; exists { + usedAddresses = append(usedAddresses, staticAddress) + } + } + + // Add to the filtered map if there are any used addresses + if len(usedAddresses) > 0 { + filteredLookupTables[tableKey] = usedAddresses + } } - // Create an instance of the type defined by methodConfig.DataType - decoded := reflect.New(methodConfig.DataType).Interface() - err = s.codec.Decode(ctx, encoded[0], decoded, methodConfig.DecodedTypeName) + return filteredLookupTables +} + +func (s *SolanaChainWriterService) SubmitTransaction(ctx context.Context, contractName, method string, args any, transactionID string, toAddress string, meta *types.TxMeta, value *big.Int) error { + programConfig := s.config.Programs[contractName] + methodConfig := programConfig.Methods[method] // Configure debug ID debugID := "" if methodConfig.DebugIDLocation != "" { - debugID, err = GetDebugIDAtLocation(decoded, methodConfig.DebugIDLocation) + debugID, err := GetDebugIDAtLocation(args, methodConfig.DebugIDLocation) if err != nil { - return errorWithDebugID(fmt.Errorf("error getting debug ID from decoded data: %w", err), debugID) + return errorWithDebugID(fmt.Errorf("error getting debug ID from input args: %w", err), debugID) } } - // Read lookup tables from on-chain - lookupTableAddresses, derivedTableMap, err := s.getDerivedTableMap(ctx, methodConfig.DerivedLookupTables, debugID) + // Fetch derived and static table maps + derivedTableMap, staticTableMap, err := s.getDerivedTableMap(ctx, methodConfig.LookupTables, debugID) if err != nil { - return errorWithDebugID(fmt.Errorf("error getting readable table map: %w", err), debugID) + return errorWithDebugID(fmt.Errorf("error getting lookup tables: %w", err), debugID) } - // Lookup configured account addresses from decoded data - accounts, err := s.GetAddresses(ctx, decoded, methodConfig.Accounts, derivedTableMap, debugID) + // Resolve account metas + accounts, err := GetAddresses(ctx, args, methodConfig.Accounts, derivedTableMap, debugID) if err != nil { - return errorWithDebugID(fmt.Errorf("error getting addresses from decoded data: %w", err), debugID) + return errorWithDebugID(fmt.Errorf("error resolving account addresses: %w", err), debugID) } - // get current latest blockhash, this can be overwritten by the TXM + // Filter the lookup table addresses based on which accounts are actually used + filteredLookupTableMap := s.FilterLookupTableAddresses(accounts, derivedTableMap, staticTableMap, debugID) + + // Fetch latest blockhash blockhash, err := s.reader.LatestBlockhash(ctx) + if err != nil { + return errorWithDebugID(fmt.Errorf("error fetching latest blockhash: %w", err), debugID) + } + // Prepare transaction programId, err := solana.PublicKeyFromBase58(contractName) if err != nil { - return errorWithDebugID(fmt.Errorf("Error getting programId: %w", err), debugID) + return errorWithDebugID(fmt.Errorf("error parsing program ID: %w", err), debugID) } - // Encode payload for chain, apply modifiers and borsh-encode - encodedPayload, err := s.codec.Encode(ctx, args, codec.WrapItemType(contract, method, true)) + feePayer, err := solana.PublicKeyFromBase58(methodConfig.FromAddress) if err != nil { - return errorWithDebugID(fmt.Errorf("error encoding data: %w", err), debugID) + return errorWithDebugID(fmt.Errorf("error parsing fee payer address: %w", err), debugID) } - feePayer, err := solana.PublicKeyFromBase58(methodConfig.FromAddress) + encodedPayload, err := s.codec.Encode(ctx, args, codec.WrapItemType(contractName, method, true)) if err != nil { - return errorWithDebugID(fmt.Errorf("error getting fee payer: %w", err), debugID) + return errorWithDebugID(fmt.Errorf("error encoding transaction payload: %w", err), debugID) } tx, err := solana.NewTransaction( @@ -478,16 +498,17 @@ func (s *SolanaChainWriterService) SubmitTransaction(ctx context.Context, contra }, blockhash.Value.Blockhash, solana.TransactionPayer(feePayer), - solana.TransactionAddressTables(lookupTableAddresses), + solana.TransactionAddressTables(filteredLookupTableMap), ) - if err != nil { - return errorWithDebugID(fmt.Errorf("error creating new transaction: %w", err), debugID) + return errorWithDebugID(fmt.Errorf("error constructing transaction: %w", err), debugID) } + // Enqueue transaction if err = s.txm.Enqueue(ctx, accounts[0].PublicKey.String(), tx, transactionID); err != nil { - return errorWithDebugID(fmt.Errorf("error on sending trasnaction to TXM: %w", err), debugID) + return errorWithDebugID(fmt.Errorf("error enqueuing transaction: %w", err), debugID) } + return nil } diff --git a/pkg/solana/chainwriter/chain_writer_test.go b/pkg/solana/chainwriter/chain_writer_test.go index 1b986a361..dc2a06654 100644 --- a/pkg/solana/chainwriter/chain_writer_test.go +++ b/pkg/solana/chainwriter/chain_writer_test.go @@ -2,13 +2,10 @@ package chainwriter_test import ( "fmt" - "reflect" "testing" commoncodec "github.com/smartcontractkit/chainlink-common/pkg/codec" "github.com/smartcontractkit/chainlink-solana/pkg/solana/chainwriter" - - ccipocr3 "github.com/smartcontractkit/chainlink-common/pkg/types/ccipocr3" ) type RegistryTokenState struct { @@ -42,10 +39,6 @@ func TestGetAddresses(t *testing.T) { Fields: []string{"Message.ExtraArgs.MerkleRoot"}, }, }, - // Location in the args where the object to decode is located. - DecodeLocation: "Report", - DataType: reflect.TypeOf(ccipocr3.ExecutePluginReportSingleChain{}), - DecodedTypeName: "ExecutePluginReportSingleChain", ChainSpecificName: "execute", // LookupTables are on-chain stores of accounts. They can be used in two ways: // 1. As a way to store a list of accounts that are all associated together (i.e. Token State registry) @@ -72,7 +65,7 @@ func TestGetAddresses(t *testing.T) { }, // Seeds would be used if the user needed to look up addresses to use as seeds, which isn't the case here. Seeds: []chainwriter.Lookup{ - chainwriter.ValueLookup{Location: "Message.TokenAmounts.DestTokenAddress"}, + chainwriter.AccountLookup{Location: "Message.TokenAmounts.DestTokenAddress"}, }, IsSigner: false, IsWritable: false, @@ -111,7 +104,7 @@ func TestGetAddresses(t *testing.T) { }, // Similar to the RegistryTokenState above, the user is looking up PDA accounts based on the dest tokens. Seeds: []chainwriter.Lookup{ - chainwriter.ValueLookup{Location: "Message.TokenAmounts.DestTokenAddress"}, + chainwriter.AccountLookup{Location: "Message.TokenAmounts.DestTokenAddress"}, }, IsSigner: false, IsWritable: false, @@ -170,7 +163,7 @@ func TestGetAddresses(t *testing.T) { }, // The seed, once again, is the destination token address. Seeds: []chainwriter.Lookup{ - chainwriter.ValueLookup{Location: "Message.TokenAmounts.DestTokenAddress"}, + chainwriter.AccountLookup{Location: "Message.TokenAmounts.DestTokenAddress"}, }, IsSigner: false, IsWritable: false, @@ -199,7 +192,7 @@ func TestGetAddresses(t *testing.T) { IsWritable: false, }, Seeds: []chainwriter.Lookup{ - chainwriter.ValueLookup{ + chainwriter.AccountLookup{ // The seed is the merkle root of the report, as passed into the input params. Location: "args.MerkleRoot", }, @@ -219,8 +212,8 @@ func TestGetAddresses(t *testing.T) { // In this case, the user configured multiple seeds. These will be used in conjunction // with the public key to generate one or multiple PDA accounts. Seeds: []chainwriter.Lookup{ - chainwriter.ValueLookup{Location: "Message.Receiver"}, - chainwriter.ValueLookup{Location: "Message.DestChainSelector"}, + chainwriter.AccountLookup{Location: "Message.Receiver"}, + chainwriter.AccountLookup{Location: "Message.DestChainSelector"}, }, }, // Account constant @@ -253,16 +246,13 @@ func TestGetAddresses(t *testing.T) { }, }, // TBD where this will be in the report - // This will be appended to every error message (after args are decoded). + // This will be appended to every error message DebugIDLocation: "Message.MessageID", } commitConfig := chainwriter.MethodConfig{ FromAddress: userAddress, InputModifications: nil, - DecodeLocation: "Report", - DataType: reflect.TypeOf(ccipocr3.CommitPluginReport{}), - DecodedTypeName: "CommitPluginReport", ChainSpecificName: "commit", LookupTables: chainwriter.LookupTables{ StaticLookupTables: []string{ @@ -282,7 +272,7 @@ func TestGetAddresses(t *testing.T) { IsWritable: false, }, Seeds: []chainwriter.Lookup{ - chainwriter.ValueLookup{ + chainwriter.AccountLookup{ // The seed is the merkle root of the report, as passed into the input params. Location: "args.MerkleRoots", }, diff --git a/pkg/solana/chainwriter/helpers.go b/pkg/solana/chainwriter/helpers.go index 369ca69fe..256d10c25 100644 --- a/pkg/solana/chainwriter/helpers.go +++ b/pkg/solana/chainwriter/helpers.go @@ -10,12 +10,12 @@ import ( ) // GetAddressAtLocation parses through nested types and arrays to find all address locations. -func GetAddressAtLocation(decoded any, location string, debugID string) ([]solana.PublicKey, error) { +func GetAddressAtLocation(args any, location string, debugID string) ([]solana.PublicKey, error) { var addresses []solana.PublicKey path := strings.Split(location, ".") - addressList, err := traversePath(decoded, path) + addressList, err := traversePath(args, path) if err != nil { return nil, err } @@ -31,8 +31,8 @@ func GetAddressAtLocation(decoded any, location string, debugID string) ([]solan return addresses, nil } -func GetDebugIDAtLocation(decoded any, location string) (string, error) { - debugIDList, err := GetValueAtLocation(decoded, location) +func GetDebugIDAtLocation(args any, location string) (string, error) { + debugIDList, err := GetValueAtLocation(args, location) if err != nil { return "", err } @@ -43,10 +43,10 @@ func GetDebugIDAtLocation(decoded any, location string) (string, error) { return debugID, nil } -func GetValueAtLocation(decoded any, location string) ([][]byte, error) { +func GetValueAtLocation(args any, location string) ([][]byte, error) { path := strings.Split(location, ".") - valueList, err := traversePath(decoded, path) + valueList, err := traversePath(args, path) if err != nil { return nil, err } From 87191d2f997f04bd782acb2933768983da640519 Mon Sep 17 00:00:00 2001 From: Silas Lenihan Date: Mon, 18 Nov 2024 11:16:04 -0500 Subject: [PATCH 14/22] Added codec implementation --- pkg/solana/chainwriter/chain_writer.go | 54 ++++++++++++++++++++++++-- pkg/solana/chainwriter/lookups.go | 2 + 2 files changed, 52 insertions(+), 4 deletions(-) create mode 100644 pkg/solana/chainwriter/lookups.go diff --git a/pkg/solana/chainwriter/chain_writer.go b/pkg/solana/chainwriter/chain_writer.go index 51aefed50..a8714c715 100644 --- a/pkg/solana/chainwriter/chain_writer.go +++ b/pkg/solana/chainwriter/chain_writer.go @@ -2,6 +2,7 @@ package chainwriter import ( "context" + "encoding/json" "fmt" "math/big" @@ -9,6 +10,7 @@ import ( "github.com/gagliardetto/solana-go/rpc" commoncodec "github.com/smartcontractkit/chainlink-common/pkg/codec" + "github.com/smartcontractkit/chainlink-common/pkg/codec/encodings/binary" "github.com/smartcontractkit/chainlink-common/pkg/services" "github.com/smartcontractkit/chainlink-common/pkg/types" @@ -22,8 +24,8 @@ type SolanaChainWriterService struct { reader client.Reader txm txm.Txm ge fees.Estimator - codec types.Codec config ChainWriterConfig + codecs map[string]types.Codec } type ChainWriterConfig struct { @@ -99,13 +101,56 @@ type AccountsFromLookupTable struct { IncludeIndexes []int } -func NewSolanaChainWriterService(reader client.Reader, txm txm.Txm, ge fees.Estimator, config ChainWriterConfig) *SolanaChainWriterService { +func NewSolanaChainWriterService(reader client.Reader, txm txm.Txm, ge fees.Estimator, config ChainWriterConfig) (*SolanaChainWriterService, error) { + codecs, err := parseIDLCodecs(config) + if err != nil { + return nil, fmt.Errorf("failed to parse IDL codecs: %w", err) + } + return &SolanaChainWriterService{ reader: reader, txm: txm, ge: ge, config: config, + codecs: codecs, + }, nil +} + +func parseIDLCodecs(config ChainWriterConfig) (map[string]types.Codec, error) { + codecs := make(map[string]types.Codec) + for program, programConfig := range config.Programs { + var idl codec.IDL + if err := json.Unmarshal([]byte(programConfig.IDL), &idl); err != nil { + return nil, fmt.Errorf("failed to unmarshal IDL: %w", err) + } + idlCodec, err := codec.NewIDLAccountCodec(idl, binary.LittleEndian()) + if err != nil { + return nil, fmt.Errorf("failed to create codec from IDL: %w", err) + } + for method, methodConfig := range programConfig.Methods { + if methodConfig.InputModifications != nil { + modConfig, err := methodConfig.InputModifications.ToModifier(codec.DecoderHooks...) + if err != nil { + return nil, fmt.Errorf("failed to create input modifications: %w", err) + } + // add mods to codec + idlCodec, err = codec.NewNamedModifierCodec(idlCodec, WrapItemType(program, method, true), modConfig) + if err != nil { + return nil, fmt.Errorf("failed to create named codec: %w", err) + } + } + } + codecs[program] = idlCodec } + return codecs, nil +} + +func WrapItemType(programName, itemType string, isParams bool) string { + if isParams { + return fmt.Sprintf("params.%s.%s", programName, itemType) + } + + return fmt.Sprintf("return.%s.%s", programName, itemType) } /* @@ -487,7 +532,8 @@ func (s *SolanaChainWriterService) SubmitTransaction(ctx context.Context, contra return errorWithDebugID(fmt.Errorf("error parsing fee payer address: %w", err), debugID) } - encodedPayload, err := s.codec.Encode(ctx, args, codec.WrapItemType(contractName, method, true)) + codec := s.codecs[contractName] + encodedPayload, err := codec.Encode(ctx, args, WrapItemType(contractName, method, true)) if err != nil { return errorWithDebugID(fmt.Errorf("error encoding transaction payload: %w", err), debugID) } @@ -505,7 +551,7 @@ func (s *SolanaChainWriterService) SubmitTransaction(ctx context.Context, contra } // Enqueue transaction - if err = s.txm.Enqueue(ctx, accounts[0].PublicKey.String(), tx, transactionID); err != nil { + if err = s.txm.Enqueue(ctx, accounts[0].PublicKey.String(), tx, &transactionID); err != nil { return errorWithDebugID(fmt.Errorf("error enqueuing transaction: %w", err), debugID) } diff --git a/pkg/solana/chainwriter/lookups.go b/pkg/solana/chainwriter/lookups.go new file mode 100644 index 000000000..313c18c45 --- /dev/null +++ b/pkg/solana/chainwriter/lookups.go @@ -0,0 +1,2 @@ +package chainwriter + From aeb45bda501138c831342f0ef74aea7e0368776b Mon Sep 17 00:00:00 2001 From: Silas Lenihan Date: Tue, 19 Nov 2024 14:57:20 -0500 Subject: [PATCH 15/22] updated CCIP example --- .../actions/projectserum_version/action.yml | 1 - pkg/solana/chainwriter/chain_writer_test.go | 51 ++++++++++++------- 2 files changed, 33 insertions(+), 19 deletions(-) diff --git a/.github/actions/projectserum_version/action.yml b/.github/actions/projectserum_version/action.yml index 9bc91323a..02e0406e8 100644 --- a/.github/actions/projectserum_version/action.yml +++ b/.github/actions/projectserum_version/action.yml @@ -14,4 +14,3 @@ runs: run: | PSVERSION=$(make projectserum_version) echo "PSVERSION=${PSVERSION}" >>$GITHUB_OUTPUT -EVM2AnyRampMessage \ No newline at end of file diff --git a/pkg/solana/chainwriter/chain_writer_test.go b/pkg/solana/chainwriter/chain_writer_test.go index dc2a06654..85b05b12b 100644 --- a/pkg/solana/chainwriter/chain_writer_test.go +++ b/pkg/solana/chainwriter/chain_writer_test.go @@ -8,14 +8,6 @@ import ( "github.com/smartcontractkit/chainlink-solana/pkg/solana/chainwriter" ) -type RegistryTokenState struct { - PoolProgram [32]byte `json:"pool_program"` - PoolConfig [32]byte `json:"pool_config"` - TokenProgram [32]byte `json:"token_program"` - TokenState [32]byte `json:"token_state"` - PoolAssociatedTokenAccount [32]byte `json:"pool_associated_token_account"` -} - func TestGetAddresses(t *testing.T) { // Fake constant addresses for the purpose of this example. registryAddress := "4Nn9dsYBcSTzRbK9hg9kzCUdrCSkMZq1UR6Vw1Tkaf6A" @@ -95,7 +87,7 @@ func TestGetAddresses(t *testing.T) { // A) The PublicKey lookup resolves to multiple addresses (i.e. multiple token addresses) // B) The Seeds or ValueSeeds resolve to multiple values chainwriter.PDALookups{ - Name: "PerChainRateLimit", + Name: "PerChainConfig", // PublicKey is a constant account in this case, not a lookup. PublicKey: chainwriter.AccountConstant{ Address: registryAddress, @@ -105,6 +97,7 @@ func TestGetAddresses(t *testing.T) { // Similar to the RegistryTokenState above, the user is looking up PDA accounts based on the dest tokens. Seeds: []chainwriter.Lookup{ chainwriter.AccountLookup{Location: "Message.TokenAmounts.DestTokenAddress"}, + chainwriter.AccountLookup{Location: "Message.Header.DestChainSelector"}, }, IsSigner: false, IsWritable: false, @@ -182,6 +175,22 @@ func TestGetAddresses(t *testing.T) { IsSigner: false, IsWritable: false, }, + // PDA lookup to get the Router Chain Config + chainwriter.PDALookups{ + Name: "RouterChainConfig", + // The public key is a constant Router address. + PublicKey: chainwriter.AccountConstant{ + Address: routerProgramAddress, + IsSigner: false, + IsWritable: false, + }, + Seeds: []chainwriter.Lookup{ + chainwriter.AccountLookup{Location: "Message.Header.DestChainSelector"}, + chainwriter.AccountLookup{Location: "Message.Header.SourceChainSelector"}, + }, + IsSigner: false, + IsWritable: false, + }, // PDA lookup to get the Router Report Accounts. chainwriter.PDALookups{ Name: "RouterReportAccount", @@ -213,7 +222,7 @@ func TestGetAddresses(t *testing.T) { // with the public key to generate one or multiple PDA accounts. Seeds: []chainwriter.Lookup{ chainwriter.AccountLookup{Location: "Message.Receiver"}, - chainwriter.AccountLookup{Location: "Message.DestChainSelector"}, + chainwriter.AccountLookup{Location: "Message.Header.DestChainSelector"}, }, }, // Account constant @@ -261,7 +270,20 @@ func TestGetAddresses(t *testing.T) { }, }, Accounts: []chainwriter.Lookup{ - + // Account constant + chainwriter.AccountConstant{ + Name: "RouterProgram", + Address: routerProgramAddress, + IsSigner: false, + IsWritable: false, + }, + // Account constant + chainwriter.AccountConstant{ + Name: "RouterAccountConfig", + Address: routerAccountConfigAddress, + IsSigner: false, + IsWritable: false, + }, // PDA lookup to get the Router Report Accounts. chainwriter.PDALookups{ Name: "RouterReportAccount", @@ -281,13 +303,6 @@ func TestGetAddresses(t *testing.T) { IsWritable: false, }, // Account constant - chainwriter.AccountConstant{ - Name: "CPISigner", - Address: cpiSignerAddress, - IsSigner: true, - IsWritable: false, - }, - // Account constant chainwriter.AccountConstant{ Name: "SystemProgram", Address: systemProgramAddress, From 4ab6a8c5513db895e979be85d5528f451be13121 Mon Sep 17 00:00:00 2001 From: Silas Lenihan Date: Wed, 20 Nov 2024 10:35:39 -0500 Subject: [PATCH 16/22] Moved lookups logic to separate file --- pkg/solana/chainwriter/chain_writer.go | 289 ------------------------ pkg/solana/chainwriter/lookups.go | 296 +++++++++++++++++++++++++ 2 files changed, 296 insertions(+), 289 deletions(-) diff --git a/pkg/solana/chainwriter/chain_writer.go b/pkg/solana/chainwriter/chain_writer.go index a8714c715..93be11058 100644 --- a/pkg/solana/chainwriter/chain_writer.go +++ b/pkg/solana/chainwriter/chain_writer.go @@ -7,7 +7,6 @@ import ( "math/big" "github.com/gagliardetto/solana-go" - "github.com/gagliardetto/solana-go/rpc" commoncodec "github.com/smartcontractkit/chainlink-common/pkg/codec" "github.com/smartcontractkit/chainlink-common/pkg/codec/encodings/binary" @@ -47,60 +46,6 @@ type MethodConfig struct { DebugIDLocation string } -type Lookup interface { - Resolve(ctx context.Context, args any, derivedTableMap map[string]map[string][]*solana.AccountMeta, debugID string) ([]*solana.AccountMeta, error) -} - -// AccountConstant represents a fixed address, provided in Base58 format, converted into a `solana.PublicKey`. -type AccountConstant struct { - Name string - Address string - IsSigner bool - IsWritable bool -} - -// AccountLookup dynamically derives an account address from args using a specified location path. -type AccountLookup struct { - Name string - Location string - IsSigner bool - IsWritable bool -} - -// PDALookups generates Program Derived Addresses (PDA) by combining a derived public key with one or more seeds. -type PDALookups struct { - Name string - // The public key of the PDA to be combined with seeds. If there are multiple PublicKeys - // there will be multiple PDAs generated by combining each PublicKey with the seeds. - PublicKey Lookup - // Seeds to be derived from an additional lookup - Seeds []Lookup - IsSigner bool - IsWritable bool -} - -type ValueLookup struct { - Location string -} - -// LookupTables represents a list of lookup tables that are used to derive addresses for a program. -type LookupTables struct { - DerivedLookupTables []DerivedLookupTable - StaticLookupTables []string -} - -// DerivedLookupTable represents a lookup table that is used to derive addresses for a program. -type DerivedLookupTable struct { - Name string - Accounts Lookup -} - -// AccountsFromLookupTable extracts accounts from a lookup table that was previously read and stored in memory. -type AccountsFromLookupTable struct { - LookupTablesName string - IncludeIndexes []int -} - func NewSolanaChainWriterService(reader client.Reader, txm txm.Txm, ge fees.Estimator, config ChainWriterConfig) (*SolanaChainWriterService, error) { codecs, err := parseIDLCodecs(config) if err != nil { @@ -198,240 +143,6 @@ func GetAddresses(ctx context.Context, args any, accounts []Lookup, derivedTable return addresses, nil } -func (ac AccountConstant) Resolve(_ context.Context, _ any, _ map[string]map[string][]*solana.AccountMeta, debugID string) ([]*solana.AccountMeta, error) { - address, err := solana.PublicKeyFromBase58(ac.Address) - if err != nil { - return nil, errorWithDebugID(fmt.Errorf("error getting account from constant: %w", err), debugID) - } - return []*solana.AccountMeta{ - { - PublicKey: address, - IsSigner: ac.IsSigner, - IsWritable: ac.IsWritable, - }, - }, nil -} - -func (al AccountLookup) Resolve(_ context.Context, args any, _ map[string]map[string][]*solana.AccountMeta, debugID string) ([]*solana.AccountMeta, error) { - derivedAddresses, err := GetAddressAtLocation(args, al.Location, debugID) - if err != nil { - return nil, errorWithDebugID(fmt.Errorf("error getting account from lookup: %w", err), debugID) - } - - var metas []*solana.AccountMeta - for _, address := range derivedAddresses { - metas = append(metas, &solana.AccountMeta{ - PublicKey: address, - IsSigner: al.IsSigner, - IsWritable: al.IsWritable, - }) - } - return metas, nil -} - -func (alt AccountsFromLookupTable) Resolve(_ context.Context, _ any, derivedTableMap map[string]map[string][]*solana.AccountMeta, debugID string) ([]*solana.AccountMeta, error) { - // Fetch the inner map for the specified lookup table name - innerMap, ok := derivedTableMap[alt.LookupTablesName] - if !ok { - return nil, errorWithDebugID(fmt.Errorf("lookup table not found: %s", alt.LookupTablesName), debugID) - } - - var result []*solana.AccountMeta - - // If no indices are specified, include all addresses - if len(alt.IncludeIndexes) == 0 { - for _, metas := range innerMap { - result = append(result, metas...) - } - return result, nil - } - - // Otherwise, include only addresses at the specified indices - for publicKey, metas := range innerMap { - for _, index := range alt.IncludeIndexes { - if index < 0 || index >= len(metas) { - return nil, errorWithDebugID(fmt.Errorf("invalid index %d for account %s in lookup table %s", index, publicKey, alt.LookupTablesName), debugID) - } - result = append(result, metas[index]) - } - } - - return result, nil -} - -func (pda PDALookups) Resolve(ctx context.Context, args any, derivedTableMap map[string]map[string][]*solana.AccountMeta, debugID string) ([]*solana.AccountMeta, error) { - publicKeys, err := GetAddresses(ctx, args, []Lookup{pda.PublicKey}, derivedTableMap, debugID) - if err != nil { - return nil, errorWithDebugID(fmt.Errorf("error getting public key for PDALookups: %w", err), debugID) - } - - seeds, err := getSeedBytes(ctx, pda, args, derivedTableMap, debugID) - if err != nil { - return nil, errorWithDebugID(fmt.Errorf("error getting seeds for PDALookups: %w", err), debugID) - } - - return generatePDAs(publicKeys, seeds, pda, debugID) -} - -// getSeedBytes extracts the seeds for the PDALookups. -// It handles both AddressSeeds (which are public keys) and ValueSeeds (which are byte arrays from input args). -func getSeedBytes(ctx context.Context, lookup PDALookups, args any, derivedTableMap map[string]map[string][]*solana.AccountMeta, debugID string) ([][]byte, error) { - var seedBytes [][]byte - - // Process AddressSeeds first (e.g., public keys) - for _, seed := range lookup.Seeds { - // Get the address(es) at the seed location - seedAddresses, err := GetAddresses(ctx, args, []Lookup{seed}, derivedTableMap, debugID) - if err != nil { - return nil, errorWithDebugID(fmt.Errorf("error getting address seed: %w", err), debugID) - } - - // Add each address seed as bytes - for _, address := range seedAddresses { - seedBytes = append(seedBytes, address.PublicKey.Bytes()) - } - } - - return seedBytes, nil -} - -// generatePDAs generates program-derived addresses (PDAs) from public keys and seeds. -func generatePDAs(publicKeys []*solana.AccountMeta, seeds [][]byte, lookup PDALookups, debugID string) ([]*solana.AccountMeta, error) { - if len(seeds) > 1 && len(publicKeys) > 1 { - return nil, errorWithDebugID(fmt.Errorf("multiple public keys and multiple seeds are not allowed"), debugID) - } - - var addresses []*solana.AccountMeta - for _, publicKeyMeta := range publicKeys { - address, _, err := solana.FindProgramAddress(seeds, publicKeyMeta.PublicKey) - if err != nil { - return nil, errorWithDebugID(fmt.Errorf("error finding program address: %w", err), debugID) - } - addresses = append(addresses, &solana.AccountMeta{ - PublicKey: address, - IsSigner: lookup.IsSigner, - IsWritable: lookup.IsWritable, - }) - } - return addresses, nil -} - -func (s *SolanaChainWriterService) getDerivedTableMap(ctx context.Context, lookupTables LookupTables, debugID string) (map[string]map[string][]*solana.AccountMeta, map[solana.PublicKey]solana.PublicKeySlice, error) { - derivedTableMap := make(map[string]map[string][]*solana.AccountMeta) - staticTableMap := make(map[solana.PublicKey]solana.PublicKeySlice) - - // Read derived lookup tables - for _, derivedLookup := range lookupTables.DerivedLookupTables { - lookupTableMap, _, err := s.LoadTable(derivedLookup, ctx, s.reader, derivedTableMap, debugID) - if err != nil { - return nil, nil, errorWithDebugID(fmt.Errorf("error loading derived lookup table: %w", err), debugID) - } - - // Merge the loaded table map into the result - for tableName, innerMap := range lookupTableMap { - if derivedTableMap[tableName] == nil { - derivedTableMap[tableName] = make(map[string][]*solana.AccountMeta) - } - for accountKey, metas := range innerMap { - derivedTableMap[tableName][accountKey] = metas - } - } - } - - // Read static lookup tables - for _, staticTable := range lookupTables.StaticLookupTables { - // Parse the static table address - tableAddress, err := solana.PublicKeyFromBase58(staticTable) - if err != nil { - return nil, nil, errorWithDebugID(fmt.Errorf("invalid static lookup table address: %s, error: %w", staticTable, err), debugID) - } - - // Fetch the account info for the static table - accountInfo, err := s.reader.GetAccountInfoWithOpts(ctx, tableAddress, &rpc.GetAccountInfoOpts{ - Encoding: "base64", - Commitment: rpc.CommitmentConfirmed, - }) - if err != nil || accountInfo == nil || accountInfo.Value == nil { - return nil, nil, errorWithDebugID(fmt.Errorf("error fetching account info for static table: %s, error: %w", staticTable, err), debugID) - } - - // Decode the account data into an array of public keys - addresses, err := decodeLookupTable(accountInfo.Value.Data.GetBinary()) - if err != nil { - return nil, nil, errorWithDebugID(fmt.Errorf("error decoding static lookup table data for %s: %w", staticTable, err), debugID) - } - - // Add the static lookup table to the map - staticTableMap[tableAddress] = addresses - } - - return derivedTableMap, staticTableMap, nil -} - -func (s *SolanaChainWriterService) LoadTable(rlt DerivedLookupTable, ctx context.Context, reader client.Reader, derivedTableMap map[string]map[string][]*solana.AccountMeta, debugID string) (map[string]map[string][]*solana.AccountMeta, []*solana.AccountMeta, error) { - // Resolve all addresses specified by the identifier - lookupTableAddresses, err := GetAddresses(ctx, nil, []Lookup{rlt.Accounts}, nil, debugID) - if err != nil { - return nil, nil, errorWithDebugID(fmt.Errorf("error resolving addresses for lookup table: %w", err), debugID) - } - - resultMap := make(map[string]map[string][]*solana.AccountMeta) - var lookupTableMetas []*solana.AccountMeta - - // Iterate over each address of the lookup table - for _, addressMeta := range lookupTableAddresses { - // Fetch account info - accountInfo, err := reader.GetAccountInfoWithOpts(ctx, addressMeta.PublicKey, &rpc.GetAccountInfoOpts{ - Encoding: "base64", - Commitment: rpc.CommitmentConfirmed, - }) - if err != nil || accountInfo == nil || accountInfo.Value == nil { - return nil, nil, errorWithDebugID(fmt.Errorf("error fetching account info for address %s: %w", addressMeta.PublicKey.String(), err), debugID) - } - - // Decode the account data into an array of public keys - addresses, err := decodeLookupTable(accountInfo.Value.Data.GetBinary()) - if err != nil { - return nil, nil, errorWithDebugID(fmt.Errorf("error decoding lookup table data for address %s: %w", addressMeta.PublicKey.String(), err), debugID) - } - - // Create the inner map for this lookup table - if resultMap[rlt.Name] == nil { - resultMap[rlt.Name] = make(map[string][]*solana.AccountMeta) - } - - // Populate the inner map (keyed by the account public key) - for _, addr := range addresses { - resultMap[rlt.Name][addr.String()] = append(resultMap[rlt.Name][addr.String()], &solana.AccountMeta{ - PublicKey: addr, - IsSigner: false, - IsWritable: false, - }) - } - - // Add the current lookup table address to the list of metas - lookupTableMetas = append(lookupTableMetas, addressMeta) - } - - return resultMap, lookupTableMetas, nil -} - -func decodeLookupTable(data []byte) (solana.PublicKeySlice, error) { - // Example logic to decode lookup table data; you may need to adjust based on the actual format of the data. - var addresses solana.PublicKeySlice - - // Assuming the data is a list of 32-byte public keys in binary format: - for i := 0; i < len(data); i += solana.PublicKeyLength { - if i+solana.PublicKeyLength > len(data) { - return nil, fmt.Errorf("invalid lookup table data length") - } - address := solana.PublicKeyFromBytes(data[i : i+solana.PublicKeyLength]) - addresses = append(addresses, address) - } - - return addresses, nil -} - func (s *SolanaChainWriterService) FilterLookupTableAddresses( accounts []*solana.AccountMeta, derivedTableMap map[string]map[string][]*solana.AccountMeta, diff --git a/pkg/solana/chainwriter/lookups.go b/pkg/solana/chainwriter/lookups.go index 313c18c45..520345104 100644 --- a/pkg/solana/chainwriter/lookups.go +++ b/pkg/solana/chainwriter/lookups.go @@ -1,2 +1,298 @@ package chainwriter +import ( + "context" + "fmt" + + "github.com/gagliardetto/solana-go" + "github.com/gagliardetto/solana-go/rpc" + "github.com/smartcontractkit/chainlink-solana/pkg/solana/client" +) + +type Lookup interface { + Resolve(ctx context.Context, args any, derivedTableMap map[string]map[string][]*solana.AccountMeta, debugID string) ([]*solana.AccountMeta, error) +} + +// AccountConstant represents a fixed address, provided in Base58 format, converted into a `solana.PublicKey`. +type AccountConstant struct { + Name string + Address string + IsSigner bool + IsWritable bool +} + +// AccountLookup dynamically derives an account address from args using a specified location path. +type AccountLookup struct { + Name string + Location string + IsSigner bool + IsWritable bool +} + +// PDALookups generates Program Derived Addresses (PDA) by combining a derived public key with one or more seeds. +type PDALookups struct { + Name string + // The public key of the PDA to be combined with seeds. If there are multiple PublicKeys + // there will be multiple PDAs generated by combining each PublicKey with the seeds. + PublicKey Lookup + // Seeds to be derived from an additional lookup + Seeds []Lookup + IsSigner bool + IsWritable bool +} + +type ValueLookup struct { + Location string +} + +// LookupTables represents a list of lookup tables that are used to derive addresses for a program. +type LookupTables struct { + DerivedLookupTables []DerivedLookupTable + StaticLookupTables []string +} + +// DerivedLookupTable represents a lookup table that is used to derive addresses for a program. +type DerivedLookupTable struct { + Name string + Accounts Lookup +} + +// AccountsFromLookupTable extracts accounts from a lookup table that was previously read and stored in memory. +type AccountsFromLookupTable struct { + LookupTablesName string + IncludeIndexes []int +} + +func (ac AccountConstant) Resolve(_ context.Context, _ any, _ map[string]map[string][]*solana.AccountMeta, debugID string) ([]*solana.AccountMeta, error) { + address, err := solana.PublicKeyFromBase58(ac.Address) + if err != nil { + return nil, errorWithDebugID(fmt.Errorf("error getting account from constant: %w", err), debugID) + } + return []*solana.AccountMeta{ + { + PublicKey: address, + IsSigner: ac.IsSigner, + IsWritable: ac.IsWritable, + }, + }, nil +} + +func (al AccountLookup) Resolve(_ context.Context, args any, _ map[string]map[string][]*solana.AccountMeta, debugID string) ([]*solana.AccountMeta, error) { + derivedAddresses, err := GetAddressAtLocation(args, al.Location, debugID) + if err != nil { + return nil, errorWithDebugID(fmt.Errorf("error getting account from lookup: %w", err), debugID) + } + + var metas []*solana.AccountMeta + for _, address := range derivedAddresses { + metas = append(metas, &solana.AccountMeta{ + PublicKey: address, + IsSigner: al.IsSigner, + IsWritable: al.IsWritable, + }) + } + return metas, nil +} + +func (alt AccountsFromLookupTable) Resolve(_ context.Context, _ any, derivedTableMap map[string]map[string][]*solana.AccountMeta, debugID string) ([]*solana.AccountMeta, error) { + // Fetch the inner map for the specified lookup table name + innerMap, ok := derivedTableMap[alt.LookupTablesName] + if !ok { + return nil, errorWithDebugID(fmt.Errorf("lookup table not found: %s", alt.LookupTablesName), debugID) + } + + var result []*solana.AccountMeta + + // If no indices are specified, include all addresses + if len(alt.IncludeIndexes) == 0 { + for _, metas := range innerMap { + result = append(result, metas...) + } + return result, nil + } + + // Otherwise, include only addresses at the specified indices + for publicKey, metas := range innerMap { + for _, index := range alt.IncludeIndexes { + if index < 0 || index >= len(metas) { + return nil, errorWithDebugID(fmt.Errorf("invalid index %d for account %s in lookup table %s", index, publicKey, alt.LookupTablesName), debugID) + } + result = append(result, metas[index]) + } + } + + return result, nil +} + +func (pda PDALookups) Resolve(ctx context.Context, args any, derivedTableMap map[string]map[string][]*solana.AccountMeta, debugID string) ([]*solana.AccountMeta, error) { + publicKeys, err := GetAddresses(ctx, args, []Lookup{pda.PublicKey}, derivedTableMap, debugID) + if err != nil { + return nil, errorWithDebugID(fmt.Errorf("error getting public key for PDALookups: %w", err), debugID) + } + + seeds, err := getSeedBytes(ctx, pda, args, derivedTableMap, debugID) + if err != nil { + return nil, errorWithDebugID(fmt.Errorf("error getting seeds for PDALookups: %w", err), debugID) + } + + return generatePDAs(publicKeys, seeds, pda, debugID) +} + +// getSeedBytes extracts the seeds for the PDALookups. +// It handles both AddressSeeds (which are public keys) and ValueSeeds (which are byte arrays from input args). +func getSeedBytes(ctx context.Context, lookup PDALookups, args any, derivedTableMap map[string]map[string][]*solana.AccountMeta, debugID string) ([][]byte, error) { + var seedBytes [][]byte + + // Process AddressSeeds first (e.g., public keys) + for _, seed := range lookup.Seeds { + // Get the address(es) at the seed location + seedAddresses, err := GetAddresses(ctx, args, []Lookup{seed}, derivedTableMap, debugID) + if err != nil { + return nil, errorWithDebugID(fmt.Errorf("error getting address seed: %w", err), debugID) + } + + // Add each address seed as bytes + for _, address := range seedAddresses { + seedBytes = append(seedBytes, address.PublicKey.Bytes()) + } + } + + return seedBytes, nil +} + +// generatePDAs generates program-derived addresses (PDAs) from public keys and seeds. +func generatePDAs(publicKeys []*solana.AccountMeta, seeds [][]byte, lookup PDALookups, debugID string) ([]*solana.AccountMeta, error) { + if len(seeds) > 1 && len(publicKeys) > 1 { + return nil, errorWithDebugID(fmt.Errorf("multiple public keys and multiple seeds are not allowed"), debugID) + } + + var addresses []*solana.AccountMeta + for _, publicKeyMeta := range publicKeys { + address, _, err := solana.FindProgramAddress(seeds, publicKeyMeta.PublicKey) + if err != nil { + return nil, errorWithDebugID(fmt.Errorf("error finding program address: %w", err), debugID) + } + addresses = append(addresses, &solana.AccountMeta{ + PublicKey: address, + IsSigner: lookup.IsSigner, + IsWritable: lookup.IsWritable, + }) + } + return addresses, nil +} + +func (s *SolanaChainWriterService) getDerivedTableMap(ctx context.Context, lookupTables LookupTables, debugID string) (map[string]map[string][]*solana.AccountMeta, map[solana.PublicKey]solana.PublicKeySlice, error) { + derivedTableMap := make(map[string]map[string][]*solana.AccountMeta) + staticTableMap := make(map[solana.PublicKey]solana.PublicKeySlice) + + // Read derived lookup tables + for _, derivedLookup := range lookupTables.DerivedLookupTables { + lookupTableMap, _, err := s.LoadTable(derivedLookup, ctx, s.reader, derivedTableMap, debugID) + if err != nil { + return nil, nil, errorWithDebugID(fmt.Errorf("error loading derived lookup table: %w", err), debugID) + } + + // Merge the loaded table map into the result + for tableName, innerMap := range lookupTableMap { + if derivedTableMap[tableName] == nil { + derivedTableMap[tableName] = make(map[string][]*solana.AccountMeta) + } + for accountKey, metas := range innerMap { + derivedTableMap[tableName][accountKey] = metas + } + } + } + + // Read static lookup tables + for _, staticTable := range lookupTables.StaticLookupTables { + // Parse the static table address + tableAddress, err := solana.PublicKeyFromBase58(staticTable) + if err != nil { + return nil, nil, errorWithDebugID(fmt.Errorf("invalid static lookup table address: %s, error: %w", staticTable, err), debugID) + } + + // Fetch the account info for the static table + accountInfo, err := s.reader.GetAccountInfoWithOpts(ctx, tableAddress, &rpc.GetAccountInfoOpts{ + Encoding: "base64", + Commitment: rpc.CommitmentConfirmed, + }) + if err != nil || accountInfo == nil || accountInfo.Value == nil { + return nil, nil, errorWithDebugID(fmt.Errorf("error fetching account info for static table: %s, error: %w", staticTable, err), debugID) + } + + // Decode the account data into an array of public keys + addresses, err := decodeLookupTable(accountInfo.Value.Data.GetBinary()) + if err != nil { + return nil, nil, errorWithDebugID(fmt.Errorf("error decoding static lookup table data for %s: %w", staticTable, err), debugID) + } + + // Add the static lookup table to the map + staticTableMap[tableAddress] = addresses + } + + return derivedTableMap, staticTableMap, nil +} + +func (s *SolanaChainWriterService) LoadTable(rlt DerivedLookupTable, ctx context.Context, reader client.Reader, derivedTableMap map[string]map[string][]*solana.AccountMeta, debugID string) (map[string]map[string][]*solana.AccountMeta, []*solana.AccountMeta, error) { + // Resolve all addresses specified by the identifier + lookupTableAddresses, err := GetAddresses(ctx, nil, []Lookup{rlt.Accounts}, nil, debugID) + if err != nil { + return nil, nil, errorWithDebugID(fmt.Errorf("error resolving addresses for lookup table: %w", err), debugID) + } + + resultMap := make(map[string]map[string][]*solana.AccountMeta) + var lookupTableMetas []*solana.AccountMeta + + // Iterate over each address of the lookup table + for _, addressMeta := range lookupTableAddresses { + // Fetch account info + accountInfo, err := reader.GetAccountInfoWithOpts(ctx, addressMeta.PublicKey, &rpc.GetAccountInfoOpts{ + Encoding: "base64", + Commitment: rpc.CommitmentConfirmed, + }) + if err != nil || accountInfo == nil || accountInfo.Value == nil { + return nil, nil, errorWithDebugID(fmt.Errorf("error fetching account info for address %s: %w", addressMeta.PublicKey.String(), err), debugID) + } + + // Decode the account data into an array of public keys + addresses, err := decodeLookupTable(accountInfo.Value.Data.GetBinary()) + if err != nil { + return nil, nil, errorWithDebugID(fmt.Errorf("error decoding lookup table data for address %s: %w", addressMeta.PublicKey.String(), err), debugID) + } + + // Create the inner map for this lookup table + if resultMap[rlt.Name] == nil { + resultMap[rlt.Name] = make(map[string][]*solana.AccountMeta) + } + + // Populate the inner map (keyed by the account public key) + for _, addr := range addresses { + resultMap[rlt.Name][addr.String()] = append(resultMap[rlt.Name][addr.String()], &solana.AccountMeta{ + PublicKey: addr, + IsSigner: false, + IsWritable: false, + }) + } + + // Add the current lookup table address to the list of metas + lookupTableMetas = append(lookupTableMetas, addressMeta) + } + + return resultMap, lookupTableMetas, nil +} + +func decodeLookupTable(data []byte) (solana.PublicKeySlice, error) { + // Example logic to decode lookup table data; you may need to adjust based on the actual format of the data. + var addresses solana.PublicKeySlice + + // Assuming the data is a list of 32-byte public keys in binary format: + for i := 0; i < len(data); i += solana.PublicKeyLength { + if i+solana.PublicKeyLength > len(data) { + return nil, fmt.Errorf("invalid lookup table data length") + } + address := solana.PublicKeyFromBytes(data[i : i+solana.PublicKeyLength]) + addresses = append(addresses, address) + } + + return addresses, nil +} From 7baa5016722cbfdbfb979303c3e0a033b20fd0ce Mon Sep 17 00:00:00 2001 From: Silas Lenihan Date: Thu, 21 Nov 2024 09:48:39 -0500 Subject: [PATCH 17/22] unit tests for lookups --- go.mod | 1 + gotest.log | 72 +++++ pkg/solana/chainwriter/chain_writer.go | 2 +- pkg/solana/chainwriter/lookups.go | 62 ++-- pkg/solana/chainwriter/lookups_test.go | 374 +++++++++++++++++++++++++ 5 files changed, 468 insertions(+), 43 deletions(-) create mode 100644 gotest.log create mode 100644 pkg/solana/chainwriter/lookups_test.go diff --git a/go.mod b/go.mod index 57024c04c..80489d3db 100644 --- a/go.mod +++ b/go.mod @@ -20,6 +20,7 @@ require ( github.com/smartcontractkit/chainlink-common v0.3.1-0.20241112140826-0e2daed34ef6 github.com/smartcontractkit/libocr v0.0.0-20241007185508-adbe57025f12 github.com/stretchr/testify v1.9.0 + github.com/test-go/testify v1.1.4 go.uber.org/zap v1.27.0 golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 golang.org/x/sync v0.8.0 diff --git a/gotest.log b/gotest.log new file mode 100644 index 000000000..2589c5f6a --- /dev/null +++ b/gotest.log @@ -0,0 +1,72 @@ +📦 github.com/smartcontractkit/chainlink-solana/pkg/solana/chainwriter +exit status 1 + ❌ TestLookupTables (30.07s) + ports.go:37: found open port: 54520 + ports.go:37: found open port: 8536 + test_helpers.go:50: API server not ready yet (attempt 1) + test_helpers.go:50: API server not ready yet (attempt 2) + test_helpers.go:50: API server not ready yet (attempt 3) + test_helpers.go:50: API server not ready yet (attempt 4) + test_helpers.go:50: API server not ready yet (attempt 5) + test_helpers.go:50: API server not ready yet (attempt 6) + test_helpers.go:50: API server not ready yet (attempt 7) + test_helpers.go:50: API server not ready yet (attempt 8) + test_helpers.go:50: API server not ready yet (attempt 9) + test_helpers.go:50: API server not ready yet (attempt 10) + test_helpers.go:50: API server not ready yet (attempt 11) + test_helpers.go:50: API server not ready yet (attempt 12) + test_helpers.go:50: API server not ready yet (attempt 13) + test_helpers.go:50: API server not ready yet (attempt 14) + test_helpers.go:50: API server not ready yet (attempt 15) + test_helpers.go:50: API server not ready yet (attempt 16) + test_helpers.go:50: API server not ready yet (attempt 17) + test_helpers.go:50: API server not ready yet (attempt 18) + test_helpers.go:50: API server not ready yet (attempt 19) + test_helpers.go:50: API server not ready yet (attempt 20) + test_helpers.go:50: API server not ready yet (attempt 21) + test_helpers.go:50: API server not ready yet (attempt 22) + test_helpers.go:50: API server not ready yet (attempt 23) + test_helpers.go:50: API server not ready yet (attempt 24) + test_helpers.go:50: API server not ready yet (attempt 25) + test_helpers.go:50: API server not ready yet (attempt 26) + test_helpers.go:50: API server not ready yet (attempt 27) + test_helpers.go:50: API server not ready yet (attempt 28) + test_helpers.go:50: API server not ready yet (attempt 29) + test_helpers.go:50: API server not ready yet (attempt 30) + test_helpers.go:57: Cmd output: + Notice! No wallet available. `solana airdrop` localnet SOL after creating one + + Ledger location: /var/folders/p4/jlx3pf896blgl6tcj0xbkhvm0000gn/T/TestLookupTables940285115/001 + Log: /var/folders/p4/jlx3pf896blgl6tcj0xbkhvm0000gn/T/TestLookupTables940285115/001/validator.log + Initializing... + Error: failed to start validator: Failed to create ledger at /var/folders/p4/jlx3pf896blgl6tcj0xbkhvm0000gn/T/TestLookupTables940285115/001: blockstore error + + Cmd error: + test_helpers.go:59: + Error Trace: /Users/silaslenihan/Desktop/repos/chainlink-solana/pkg/solana/client/test_helpers.go:59 + /Users/silaslenihan/Desktop/repos/chainlink-solana/pkg/solana/chainwriter/lookups_test.go:148 + Error: Should be true + Test: TestLookupTables + test_helpers.go:37: + Error Trace: /Users/silaslenihan/Desktop/repos/chainlink-solana/pkg/solana/client/test_helpers.go:37 + /Users/silaslenihan/go/pkg/mod/golang.org/toolchain@v0.0.1-go1.23.1.darwin-arm64/src/testing/testing.go:1176 + /Users/silaslenihan/go/pkg/mod/golang.org/toolchain@v0.0.1-go1.23.1.darwin-arm64/src/testing/testing.go:1354 + /Users/silaslenihan/go/pkg/mod/golang.org/toolchain@v0.0.1-go1.23.1.darwin-arm64/src/testing/testing.go:1684 + /Users/silaslenihan/go/pkg/mod/golang.org/toolchain@v0.0.1-go1.23.1.darwin-arm64/src/runtime/panic.go:629 + /Users/silaslenihan/go/pkg/mod/golang.org/toolchain@v0.0.1-go1.23.1.darwin-arm64/src/testing/testing.go:1006 + /Users/silaslenihan/Desktop/repos/chainlink-solana/pkg/solana/client/test_helpers.go:59 + /Users/silaslenihan/Desktop/repos/chainlink-solana/pkg/solana/chainwriter/lookups_test.go:148 + Error: "exit status 1" does not contain "signal: killed" + Test: TestLookupTables + Messages: exit status 1 + test_helpers.go:38: solana-test-validator + stdout: + Notice! No wallet available. `solana airdrop` localnet SOL after creating one + + Ledger location: /var/folders/p4/jlx3pf896blgl6tcj0xbkhvm0000gn/T/TestLookupTables940285115/001 + Log: /var/folders/p4/jlx3pf896blgl6tcj0xbkhvm0000gn/T/TestLookupTables940285115/001/validator.log + Initializing... + Error: failed to start validator: Failed to create ledger at /var/folders/p4/jlx3pf896blgl6tcj0xbkhvm0000gn/T/TestLookupTables940285115/001: blockstore error + + stderr: + diff --git a/pkg/solana/chainwriter/chain_writer.go b/pkg/solana/chainwriter/chain_writer.go index 93be11058..ef5e3eeff 100644 --- a/pkg/solana/chainwriter/chain_writer.go +++ b/pkg/solana/chainwriter/chain_writer.go @@ -212,7 +212,7 @@ func (s *SolanaChainWriterService) SubmitTransaction(ctx context.Context, contra } // Fetch derived and static table maps - derivedTableMap, staticTableMap, err := s.getDerivedTableMap(ctx, methodConfig.LookupTables, debugID) + derivedTableMap, staticTableMap, err := s.ResolveLookupTables(ctx, methodConfig.LookupTables, debugID) if err != nil { return errorWithDebugID(fmt.Errorf("error getting lookup tables: %w", err), debugID) } diff --git a/pkg/solana/chainwriter/lookups.go b/pkg/solana/chainwriter/lookups.go index 520345104..5aed4b486 100644 --- a/pkg/solana/chainwriter/lookups.go +++ b/pkg/solana/chainwriter/lookups.go @@ -5,6 +5,7 @@ import ( "fmt" "github.com/gagliardetto/solana-go" + addresslookuptable "github.com/gagliardetto/solana-go/programs/address-lookup-table" "github.com/gagliardetto/solana-go/rpc" "github.com/smartcontractkit/chainlink-solana/pkg/solana/client" ) @@ -181,7 +182,7 @@ func generatePDAs(publicKeys []*solana.AccountMeta, seeds [][]byte, lookup PDALo return addresses, nil } -func (s *SolanaChainWriterService) getDerivedTableMap(ctx context.Context, lookupTables LookupTables, debugID string) (map[string]map[string][]*solana.AccountMeta, map[solana.PublicKey]solana.PublicKeySlice, error) { +func (s *SolanaChainWriterService) ResolveLookupTables(ctx context.Context, lookupTables LookupTables, debugID string) (map[string]map[string][]*solana.AccountMeta, map[solana.PublicKey]solana.PublicKeySlice, error) { derivedTableMap := make(map[string]map[string][]*solana.AccountMeta) staticTableMap := make(map[solana.PublicKey]solana.PublicKeySlice) @@ -211,23 +212,8 @@ func (s *SolanaChainWriterService) getDerivedTableMap(ctx context.Context, looku return nil, nil, errorWithDebugID(fmt.Errorf("invalid static lookup table address: %s, error: %w", staticTable, err), debugID) } - // Fetch the account info for the static table - accountInfo, err := s.reader.GetAccountInfoWithOpts(ctx, tableAddress, &rpc.GetAccountInfoOpts{ - Encoding: "base64", - Commitment: rpc.CommitmentConfirmed, - }) - if err != nil || accountInfo == nil || accountInfo.Value == nil { - return nil, nil, errorWithDebugID(fmt.Errorf("error fetching account info for static table: %s, error: %w", staticTable, err), debugID) - } - - // Decode the account data into an array of public keys - addresses, err := decodeLookupTable(accountInfo.Value.Data.GetBinary()) - if err != nil { - return nil, nil, errorWithDebugID(fmt.Errorf("error decoding static lookup table data for %s: %w", staticTable, err), debugID) - } - - // Add the static lookup table to the map - staticTableMap[tableAddress] = addresses + addressses, err := getLookupTableAddress(ctx, s.reader, tableAddress, debugID) + staticTableMap[tableAddress] = addressses } return derivedTableMap, staticTableMap, nil @@ -246,18 +232,9 @@ func (s *SolanaChainWriterService) LoadTable(rlt DerivedLookupTable, ctx context // Iterate over each address of the lookup table for _, addressMeta := range lookupTableAddresses { // Fetch account info - accountInfo, err := reader.GetAccountInfoWithOpts(ctx, addressMeta.PublicKey, &rpc.GetAccountInfoOpts{ - Encoding: "base64", - Commitment: rpc.CommitmentConfirmed, - }) - if err != nil || accountInfo == nil || accountInfo.Value == nil { - return nil, nil, errorWithDebugID(fmt.Errorf("error fetching account info for address %s: %w", addressMeta.PublicKey.String(), err), debugID) - } - - // Decode the account data into an array of public keys - addresses, err := decodeLookupTable(accountInfo.Value.Data.GetBinary()) + addresses, err := getLookupTableAddress(ctx, reader, addressMeta.PublicKey, debugID) if err != nil { - return nil, nil, errorWithDebugID(fmt.Errorf("error decoding lookup table data for address %s: %w", addressMeta.PublicKey.String(), err), debugID) + return nil, nil, errorWithDebugID(fmt.Errorf("error fetching lookup table address: %w", err), debugID) } // Create the inner map for this lookup table @@ -267,7 +244,7 @@ func (s *SolanaChainWriterService) LoadTable(rlt DerivedLookupTable, ctx context // Populate the inner map (keyed by the account public key) for _, addr := range addresses { - resultMap[rlt.Name][addr.String()] = append(resultMap[rlt.Name][addr.String()], &solana.AccountMeta{ + resultMap[rlt.Name][addressMeta.PublicKey.String()] = append(resultMap[rlt.Name][addressMeta.PublicKey.String()], &solana.AccountMeta{ PublicKey: addr, IsSigner: false, IsWritable: false, @@ -281,18 +258,19 @@ func (s *SolanaChainWriterService) LoadTable(rlt DerivedLookupTable, ctx context return resultMap, lookupTableMetas, nil } -func decodeLookupTable(data []byte) (solana.PublicKeySlice, error) { - // Example logic to decode lookup table data; you may need to adjust based on the actual format of the data. - var addresses solana.PublicKeySlice +func getLookupTableAddress(ctx context.Context, reader client.Reader, tableAddress solana.PublicKey, debugID string) (solana.PublicKeySlice, error) { + // Fetch the account info for the static table + accountInfo, err := reader.GetAccountInfoWithOpts(ctx, tableAddress, &rpc.GetAccountInfoOpts{ + Encoding: "base64", + Commitment: rpc.CommitmentConfirmed, + }) - // Assuming the data is a list of 32-byte public keys in binary format: - for i := 0; i < len(data); i += solana.PublicKeyLength { - if i+solana.PublicKeyLength > len(data) { - return nil, fmt.Errorf("invalid lookup table data length") - } - address := solana.PublicKeyFromBytes(data[i : i+solana.PublicKeyLength]) - addresses = append(addresses, address) + if err != nil || accountInfo == nil || accountInfo.Value == nil { + return nil, errorWithDebugID(fmt.Errorf("error fetching account info for table: %s, error: %w", tableAddress.String(), err), debugID) } - - return addresses, nil + alt, err := addresslookuptable.DecodeAddressLookupTableState(accountInfo.GetBinary()) + if err != nil { + return nil, errorWithDebugID(fmt.Errorf("error decoding address lookup table state: %w", err), debugID) + } + return alt.Addresses, nil } diff --git a/pkg/solana/chainwriter/lookups_test.go b/pkg/solana/chainwriter/lookups_test.go new file mode 100644 index 000000000..1e5ae2d7c --- /dev/null +++ b/pkg/solana/chainwriter/lookups_test.go @@ -0,0 +1,374 @@ +package chainwriter_test + +import ( + "context" + "fmt" + "testing" + "time" + + "encoding/binary" + + "github.com/gagliardetto/solana-go" + "github.com/gagliardetto/solana-go/rpc" + "github.com/smartcontractkit/chainlink-common/pkg/logger" + "github.com/smartcontractkit/chainlink-common/pkg/utils/tests" + "github.com/smartcontractkit/chainlink-solana/pkg/solana/chainwriter" + "github.com/smartcontractkit/chainlink-solana/pkg/solana/client" + "github.com/smartcontractkit/chainlink-solana/pkg/solana/config" + "github.com/smartcontractkit/chainlink-solana/pkg/solana/txm" + keyMocks "github.com/smartcontractkit/chainlink-solana/pkg/solana/txm/mocks" + + "github.com/smartcontractkit/chainlink-common/pkg/utils" + "github.com/test-go/testify/require" +) + +type TestArgs struct { + Inner []InnerArgs +} + +type InnerArgs struct { + Address []byte +} + +func TestAccountContant(t *testing.T) { + + t.Run("AccountConstant resolves valid address", func(t *testing.T) { + expectedAddr := "4Nn9dsYBcSTzRbK9hg9kzCUdrCSkMZq1UR6Vw1Tkaf6M" + expectedMeta := []*solana.AccountMeta{ + { + PublicKey: solana.MustPublicKeyFromBase58(expectedAddr), + IsSigner: true, + IsWritable: true, + }, + } + constantConfig := chainwriter.AccountConstant{ + Name: "TestAccount", + Address: expectedAddr, + IsSigner: true, + IsWritable: true, + } + result, err := constantConfig.Resolve(nil, nil, nil, "") + require.NoError(t, err) + require.Equal(t, expectedMeta, result) + }) +} +func TestAccountLookups(t *testing.T) { + t.Run("AccountLookup resolves valid address with just one address", func(t *testing.T) { + expectedAddr := "4Nn9dsYBcSTzRbK9hg9kzCUdrCSkMZq1UR6Vw1Tkaf6M" + testArgs := TestArgs{ + Inner: []InnerArgs{ + {Address: solana.MustPublicKeyFromBase58(expectedAddr).Bytes()}, + }, + } + expectedMeta := []*solana.AccountMeta{ + { + PublicKey: solana.MustPublicKeyFromBase58(expectedAddr), + IsSigner: true, + IsWritable: true, + }, + } + + lookupConfig := chainwriter.AccountLookup{ + Name: "TestAccount", + Location: "Inner.Address", + IsSigner: true, + IsWritable: true, + } + result, err := lookupConfig.Resolve(nil, testArgs, nil, "") + require.NoError(t, err) + require.Equal(t, expectedMeta, result) + }) + + t.Run("AccountLookup resolves valid address with just multiple addresses", func(t *testing.T) { + expectedAddr1 := "4Nn9dsYBcSTzRbK9hg9kzCUdrCSkMZq1UR6Vw1Tkaf6M" + expectedAddr2 := "4Nn9dsYBcSTzRbK9hg9kzCUdrCSkMZq1UR6Vw1Tkaf6N" + testArgs := TestArgs{ + Inner: []InnerArgs{ + {Address: solana.MustPublicKeyFromBase58(expectedAddr1).Bytes()}, + {Address: solana.MustPublicKeyFromBase58(expectedAddr2).Bytes()}, + }, + } + expectedMeta := []*solana.AccountMeta{ + { + PublicKey: solana.MustPublicKeyFromBase58(expectedAddr1), + IsSigner: true, + IsWritable: true, + }, + { + PublicKey: solana.MustPublicKeyFromBase58(expectedAddr2), + IsSigner: true, + IsWritable: true, + }, + } + + lookupConfig := chainwriter.AccountLookup{ + Name: "TestAccount", + Location: "Inner.Address", + IsSigner: true, + IsWritable: true, + } + result, err := lookupConfig.Resolve(nil, testArgs, nil, "") + require.NoError(t, err) + for i, meta := range result { + require.Equal(t, expectedMeta[i], meta) + } + }) + + t.Run("AccountLookup fails when address isn't in args", func(t *testing.T) { + expectedAddr := "4Nn9dsYBcSTzRbK9hg9kzCUdrCSkMZq1UR6Vw1Tkaf6M" + testArgs := TestArgs{ + Inner: []InnerArgs{ + {Address: solana.MustPublicKeyFromBase58(expectedAddr).Bytes()}, + }, + } + lookupConfig := chainwriter.AccountLookup{ + Name: "InvalidAccount", + Location: "Invalid.Directory", + IsSigner: true, + IsWritable: true, + } + _, err := lookupConfig.Resolve(nil, testArgs, nil, "") + require.Error(t, err) + }) +} + +func TestPDALookups(t *testing.T) { + // TODO: May require deploying a program to test + // t.Run("PDALookup resolves valid address", func(t *testing.T) { + // expectedAddr := "4Nn9dsYBcSTzRbK9hg9kzCUdrCSkMZq1UR6Vw1Tkaf6M" + // expectedMeta := []*solana.AccountMeta{ + // { + // PublicKey: solana.MustPublicKeyFromBase58(expectedAddr), + // IsSigner: true, + // IsWritable: true, + // }, + // } + // lookupConfig := chainwriter.PDALookups{ + // Name: "TestAccount", + // PublicKey: + // } + + // }) +} + +func TestLookupTables(t *testing.T) { + ctx := tests.Context(t) + url := client.SetupLocalSolNode(t) + c := rpc.New(url) + + sender, err := solana.NewRandomPrivateKey() + require.NoError(t, err) + client.FundTestAccounts(t, []solana.PublicKey{sender.PublicKey()}, url) + + cfg := config.NewDefault() + solanaClient, err := client.NewClient(url, cfg, 5*time.Second, nil) + + loader := utils.NewLazyLoad(func() (client.ReaderWriter, error) { return solanaClient, nil }) + mkey := keyMocks.NewSimpleKeystore(t) + lggr := logger.Test(t) + + txm := txm.NewTxm("localnet", loader, nil, cfg, mkey, lggr) + + chainWriter, err := chainwriter.NewSolanaChainWriterService(solanaClient, *txm, nil, chainwriter.ChainWriterConfig{}) + + t.Run("StaticLookup table resolves properly", func(t *testing.T) { + pubKeys := createTestPubKeys(t, 8) + table := CreateTestLookupTable(t, ctx, c, sender, pubKeys) + lookupConfig := chainwriter.LookupTables{ + DerivedLookupTables: nil, + StaticLookupTables: []string{table.String()}, + } + _, staticTableMap, err := chainWriter.ResolveLookupTables(ctx, lookupConfig, "test-debug-id") + require.NoError(t, err) + require.Equal(t, pubKeys, staticTableMap[table]) + }) + + t.Run("Derived lookup table resovles properly with constant address", func(t *testing.T) { + pubKeys := createTestPubKeys(t, 8) + table := CreateTestLookupTable(t, ctx, c, sender, pubKeys) + lookupConfig := chainwriter.LookupTables{ + DerivedLookupTables: []chainwriter.DerivedLookupTable{ + { + Name: "DerivedTable", + Accounts: chainwriter.AccountConstant{ + Name: "TestLookupTable", + Address: table.String(), + IsSigner: true, + IsWritable: true, + }, + }, + }, + StaticLookupTables: nil, + } + derivedTableMap, _, err := chainWriter.ResolveLookupTables(ctx, lookupConfig, "test-debug-id") + require.NoError(t, err) + + addresses, ok := derivedTableMap["DerivedTable"][table.String()] + require.True(t, ok) + for i, address := range addresses { + require.Equal(t, pubKeys[i], address.PublicKey) + } + }) +} + +func createTestPubKeys(t *testing.T, num int) solana.PublicKeySlice { + addresses := make([]solana.PublicKey, num) + for i := 0; i < num; i++ { + privKey, err := solana.NewRandomPrivateKey() + require.NoError(t, err) + addresses[i] = privKey.PublicKey() + } + return addresses +} + +func CreateTestLookupTable(t *testing.T, ctx context.Context, c *rpc.Client, sender solana.PrivateKey, addresses []solana.PublicKey) solana.PublicKey { + // Create lookup tables + slot, serr := c.GetSlot(ctx, rpc.CommitmentFinalized) + fmt.Println("SLOT: ", slot) + require.NoError(t, serr) + table, instruction, ierr := NewCreateLookupTableInstruction( + sender.PublicKey(), + sender.PublicKey(), + slot, + ) + require.NoError(t, ierr) + SendAndConfirm(ctx, t, c, []solana.Instruction{instruction}, sender, rpc.CommitmentConfirmed) + + // add entries to lookup table + SendAndConfirm(ctx, t, c, []solana.Instruction{ + NewExtendLookupTableInstruction( + table, sender.PublicKey(), sender.PublicKey(), + addresses, + ), + }, sender, rpc.CommitmentConfirmed) + + return table +} + +// TxModifier is a dynamic function used to flexibly add components to a transaction such as additional signers, and compute budget parameters +type TxModifier func(tx *solana.Transaction, signers map[solana.PublicKey]solana.PrivateKey) error + +func SendAndConfirm(ctx context.Context, t *testing.T, rpcClient *rpc.Client, instructions []solana.Instruction, + signer solana.PrivateKey, commitment rpc.CommitmentType, opts ...TxModifier) *rpc.GetTransactionResult { + txres := sendTransaction(ctx, rpcClient, t, instructions, signer, commitment, false, opts...) // do not skipPreflight when expected to pass, preflight can help debug + + require.NotNil(t, txres.Meta) + require.Nil(t, txres.Meta.Err, fmt.Sprintf("tx failed with: %+v", txres.Meta)) // tx should not err, print meta if it does (contains logs) + return txres +} + +func sendTransaction(ctx context.Context, rpcClient *rpc.Client, t *testing.T, instructions []solana.Instruction, + signerAndPayer solana.PrivateKey, commitment rpc.CommitmentType, skipPreflight bool, opts ...TxModifier) *rpc.GetTransactionResult { + hashRes, err := rpcClient.GetLatestBlockhash(ctx, rpc.CommitmentFinalized) + require.NoError(t, err) + + tx, err := solana.NewTransaction( + instructions, + hashRes.Value.Blockhash, + solana.TransactionPayer(signerAndPayer.PublicKey()), + ) + require.NoError(t, err) + + // build signers map + signers := map[solana.PublicKey]solana.PrivateKey{} + signers[signerAndPayer.PublicKey()] = signerAndPayer + + // set options before signing transaction + for _, o := range opts { + require.NoError(t, o(tx, signers)) + } + + _, err = tx.Sign(func(pub solana.PublicKey) *solana.PrivateKey { + priv, ok := signers[pub] + require.True(t, ok, fmt.Sprintf("Missing signer private key for %s", pub)) + return &priv + }) + require.NoError(t, err) + + txsig, err := rpcClient.SendTransactionWithOpts(ctx, tx, rpc.TransactionOpts{SkipPreflight: skipPreflight, PreflightCommitment: rpc.CommitmentProcessed}) + require.NoError(t, err) + + var txStatus rpc.ConfirmationStatusType + count := 0 + for txStatus != rpc.ConfirmationStatusConfirmed && txStatus != rpc.ConfirmationStatusFinalized { + count++ + statusRes, sigErr := rpcClient.GetSignatureStatuses(ctx, true, txsig) + require.NoError(t, sigErr) + if statusRes != nil && len(statusRes.Value) > 0 && statusRes.Value[0] != nil { + txStatus = statusRes.Value[0].ConfirmationStatus + } + time.Sleep(100 * time.Millisecond) + if count > 50 { + require.NoError(t, fmt.Errorf("unable to find transaction within timeout")) + } + } + + txres, err := rpcClient.GetTransaction(ctx, txsig, &rpc.GetTransactionOpts{ + Commitment: commitment, + }) + require.NoError(t, err) + return txres +} + +var ( + AddressLookupTableProgram = solana.MustPublicKeyFromBase58("AddressLookupTab1e1111111111111111111111111") +) + +const ( + InstructionCreateLookupTable uint32 = iota + InstructionFreezeLookupTable + InstructionExtendLookupTable + InstructionDeactiveLookupTable + InstructionCloseLookupTable +) + +func NewCreateLookupTableInstruction( + authority, funder solana.PublicKey, + slot uint64, +) (solana.PublicKey, solana.Instruction, error) { + // https://github.com/solana-labs/solana-web3.js/blob/c1c98715b0c7900ce37c59bffd2056fa0037213d/src/programs/address-lookup-table/index.ts#L274 + slotLE := make([]byte, 8) + binary.LittleEndian.PutUint64(slotLE, slot) + account, bumpSeed, err := solana.FindProgramAddress([][]byte{authority.Bytes(), slotLE}, AddressLookupTableProgram) + if err != nil { + return solana.PublicKey{}, nil, err + } + + data := binary.LittleEndian.AppendUint32([]byte{}, InstructionCreateLookupTable) + data = binary.LittleEndian.AppendUint64(data, slot) + data = append(data, bumpSeed) + return account, solana.NewInstruction( + AddressLookupTableProgram, + solana.AccountMetaSlice{ + solana.Meta(account).WRITE(), + solana.Meta(authority).SIGNER(), + solana.Meta(funder).SIGNER().WRITE(), + solana.Meta(solana.SystemProgramID), + }, + data, + ), nil +} + +func NewExtendLookupTableInstruction( + table, authority, funder solana.PublicKey, + accounts []solana.PublicKey, +) solana.Instruction { + // https://github.com/solana-labs/solana-web3.js/blob/c1c98715b0c7900ce37c59bffd2056fa0037213d/src/programs/address-lookup-table/index.ts#L113 + + data := binary.LittleEndian.AppendUint32([]byte{}, InstructionExtendLookupTable) + data = binary.LittleEndian.AppendUint64(data, uint64(len(accounts))) // note: this is usually u32 + 8 byte buffer + for _, a := range accounts { + data = append(data, a.Bytes()...) + } + + return solana.NewInstruction( + AddressLookupTableProgram, + solana.AccountMetaSlice{ + solana.Meta(table).WRITE(), + solana.Meta(authority).SIGNER(), + solana.Meta(funder).SIGNER().WRITE(), + solana.Meta(solana.SystemProgramID), + }, + data, + ) +} From 7012b9aa407ed94ad408cbe8c82fa35270a441ec Mon Sep 17 00:00:00 2001 From: Silas Lenihan Date: Thu, 21 Nov 2024 16:45:54 -0500 Subject: [PATCH 18/22] Added utils to their own package --- gotest.log | 72 ---------- pkg/solana/chainwriter/chain_writer.go | 2 +- pkg/solana/chainwriter/lookups.go | 8 +- pkg/solana/chainwriter/lookups_test.go | 185 ++++++------------------- pkg/solana/utils.go | 5 - pkg/solana/utils/utils.go | 178 ++++++++++++++++++++++++ pkg/solana/{ => utils}/utils_test.go | 5 +- 7 files changed, 229 insertions(+), 226 deletions(-) delete mode 100644 gotest.log delete mode 100644 pkg/solana/utils.go create mode 100644 pkg/solana/utils/utils.go rename pkg/solana/{ => utils}/utils_test.go (71%) diff --git a/gotest.log b/gotest.log deleted file mode 100644 index 2589c5f6a..000000000 --- a/gotest.log +++ /dev/null @@ -1,72 +0,0 @@ -📦 github.com/smartcontractkit/chainlink-solana/pkg/solana/chainwriter -exit status 1 - ❌ TestLookupTables (30.07s) - ports.go:37: found open port: 54520 - ports.go:37: found open port: 8536 - test_helpers.go:50: API server not ready yet (attempt 1) - test_helpers.go:50: API server not ready yet (attempt 2) - test_helpers.go:50: API server not ready yet (attempt 3) - test_helpers.go:50: API server not ready yet (attempt 4) - test_helpers.go:50: API server not ready yet (attempt 5) - test_helpers.go:50: API server not ready yet (attempt 6) - test_helpers.go:50: API server not ready yet (attempt 7) - test_helpers.go:50: API server not ready yet (attempt 8) - test_helpers.go:50: API server not ready yet (attempt 9) - test_helpers.go:50: API server not ready yet (attempt 10) - test_helpers.go:50: API server not ready yet (attempt 11) - test_helpers.go:50: API server not ready yet (attempt 12) - test_helpers.go:50: API server not ready yet (attempt 13) - test_helpers.go:50: API server not ready yet (attempt 14) - test_helpers.go:50: API server not ready yet (attempt 15) - test_helpers.go:50: API server not ready yet (attempt 16) - test_helpers.go:50: API server not ready yet (attempt 17) - test_helpers.go:50: API server not ready yet (attempt 18) - test_helpers.go:50: API server not ready yet (attempt 19) - test_helpers.go:50: API server not ready yet (attempt 20) - test_helpers.go:50: API server not ready yet (attempt 21) - test_helpers.go:50: API server not ready yet (attempt 22) - test_helpers.go:50: API server not ready yet (attempt 23) - test_helpers.go:50: API server not ready yet (attempt 24) - test_helpers.go:50: API server not ready yet (attempt 25) - test_helpers.go:50: API server not ready yet (attempt 26) - test_helpers.go:50: API server not ready yet (attempt 27) - test_helpers.go:50: API server not ready yet (attempt 28) - test_helpers.go:50: API server not ready yet (attempt 29) - test_helpers.go:50: API server not ready yet (attempt 30) - test_helpers.go:57: Cmd output: - Notice! No wallet available. `solana airdrop` localnet SOL after creating one - - Ledger location: /var/folders/p4/jlx3pf896blgl6tcj0xbkhvm0000gn/T/TestLookupTables940285115/001 - Log: /var/folders/p4/jlx3pf896blgl6tcj0xbkhvm0000gn/T/TestLookupTables940285115/001/validator.log - Initializing... - Error: failed to start validator: Failed to create ledger at /var/folders/p4/jlx3pf896blgl6tcj0xbkhvm0000gn/T/TestLookupTables940285115/001: blockstore error - - Cmd error: - test_helpers.go:59: - Error Trace: /Users/silaslenihan/Desktop/repos/chainlink-solana/pkg/solana/client/test_helpers.go:59 - /Users/silaslenihan/Desktop/repos/chainlink-solana/pkg/solana/chainwriter/lookups_test.go:148 - Error: Should be true - Test: TestLookupTables - test_helpers.go:37: - Error Trace: /Users/silaslenihan/Desktop/repos/chainlink-solana/pkg/solana/client/test_helpers.go:37 - /Users/silaslenihan/go/pkg/mod/golang.org/toolchain@v0.0.1-go1.23.1.darwin-arm64/src/testing/testing.go:1176 - /Users/silaslenihan/go/pkg/mod/golang.org/toolchain@v0.0.1-go1.23.1.darwin-arm64/src/testing/testing.go:1354 - /Users/silaslenihan/go/pkg/mod/golang.org/toolchain@v0.0.1-go1.23.1.darwin-arm64/src/testing/testing.go:1684 - /Users/silaslenihan/go/pkg/mod/golang.org/toolchain@v0.0.1-go1.23.1.darwin-arm64/src/runtime/panic.go:629 - /Users/silaslenihan/go/pkg/mod/golang.org/toolchain@v0.0.1-go1.23.1.darwin-arm64/src/testing/testing.go:1006 - /Users/silaslenihan/Desktop/repos/chainlink-solana/pkg/solana/client/test_helpers.go:59 - /Users/silaslenihan/Desktop/repos/chainlink-solana/pkg/solana/chainwriter/lookups_test.go:148 - Error: "exit status 1" does not contain "signal: killed" - Test: TestLookupTables - Messages: exit status 1 - test_helpers.go:38: solana-test-validator - stdout: - Notice! No wallet available. `solana airdrop` localnet SOL after creating one - - Ledger location: /var/folders/p4/jlx3pf896blgl6tcj0xbkhvm0000gn/T/TestLookupTables940285115/001 - Log: /var/folders/p4/jlx3pf896blgl6tcj0xbkhvm0000gn/T/TestLookupTables940285115/001/validator.log - Initializing... - Error: failed to start validator: Failed to create ledger at /var/folders/p4/jlx3pf896blgl6tcj0xbkhvm0000gn/T/TestLookupTables940285115/001: blockstore error - - stderr: - diff --git a/pkg/solana/chainwriter/chain_writer.go b/pkg/solana/chainwriter/chain_writer.go index ef5e3eeff..7dea709aa 100644 --- a/pkg/solana/chainwriter/chain_writer.go +++ b/pkg/solana/chainwriter/chain_writer.go @@ -212,7 +212,7 @@ func (s *SolanaChainWriterService) SubmitTransaction(ctx context.Context, contra } // Fetch derived and static table maps - derivedTableMap, staticTableMap, err := s.ResolveLookupTables(ctx, methodConfig.LookupTables, debugID) + derivedTableMap, staticTableMap, err := s.ResolveLookupTables(ctx, args, methodConfig.LookupTables, debugID) if err != nil { return errorWithDebugID(fmt.Errorf("error getting lookup tables: %w", err), debugID) } diff --git a/pkg/solana/chainwriter/lookups.go b/pkg/solana/chainwriter/lookups.go index 5aed4b486..72ee99e90 100644 --- a/pkg/solana/chainwriter/lookups.go +++ b/pkg/solana/chainwriter/lookups.go @@ -182,13 +182,13 @@ func generatePDAs(publicKeys []*solana.AccountMeta, seeds [][]byte, lookup PDALo return addresses, nil } -func (s *SolanaChainWriterService) ResolveLookupTables(ctx context.Context, lookupTables LookupTables, debugID string) (map[string]map[string][]*solana.AccountMeta, map[solana.PublicKey]solana.PublicKeySlice, error) { +func (s *SolanaChainWriterService) ResolveLookupTables(ctx context.Context, args any, lookupTables LookupTables, debugID string) (map[string]map[string][]*solana.AccountMeta, map[solana.PublicKey]solana.PublicKeySlice, error) { derivedTableMap := make(map[string]map[string][]*solana.AccountMeta) staticTableMap := make(map[solana.PublicKey]solana.PublicKeySlice) // Read derived lookup tables for _, derivedLookup := range lookupTables.DerivedLookupTables { - lookupTableMap, _, err := s.LoadTable(derivedLookup, ctx, s.reader, derivedTableMap, debugID) + lookupTableMap, _, err := s.LoadTable(ctx, args, derivedLookup, s.reader, derivedTableMap, debugID) if err != nil { return nil, nil, errorWithDebugID(fmt.Errorf("error loading derived lookup table: %w", err), debugID) } @@ -219,9 +219,9 @@ func (s *SolanaChainWriterService) ResolveLookupTables(ctx context.Context, look return derivedTableMap, staticTableMap, nil } -func (s *SolanaChainWriterService) LoadTable(rlt DerivedLookupTable, ctx context.Context, reader client.Reader, derivedTableMap map[string]map[string][]*solana.AccountMeta, debugID string) (map[string]map[string][]*solana.AccountMeta, []*solana.AccountMeta, error) { +func (s *SolanaChainWriterService) LoadTable(ctx context.Context, args any, rlt DerivedLookupTable, reader client.Reader, derivedTableMap map[string]map[string][]*solana.AccountMeta, debugID string) (map[string]map[string][]*solana.AccountMeta, []*solana.AccountMeta, error) { // Resolve all addresses specified by the identifier - lookupTableAddresses, err := GetAddresses(ctx, nil, []Lookup{rlt.Accounts}, nil, debugID) + lookupTableAddresses, err := GetAddresses(ctx, args, []Lookup{rlt.Accounts}, nil, debugID) if err != nil { return nil, nil, errorWithDebugID(fmt.Errorf("error resolving addresses for lookup table: %w", err), debugID) } diff --git a/pkg/solana/chainwriter/lookups_test.go b/pkg/solana/chainwriter/lookups_test.go index 1e5ae2d7c..6658750ff 100644 --- a/pkg/solana/chainwriter/lookups_test.go +++ b/pkg/solana/chainwriter/lookups_test.go @@ -2,12 +2,9 @@ package chainwriter_test import ( "context" - "fmt" "testing" "time" - "encoding/binary" - "github.com/gagliardetto/solana-go" "github.com/gagliardetto/solana-go/rpc" "github.com/smartcontractkit/chainlink-common/pkg/logger" @@ -17,8 +14,9 @@ import ( "github.com/smartcontractkit/chainlink-solana/pkg/solana/config" "github.com/smartcontractkit/chainlink-solana/pkg/solana/txm" keyMocks "github.com/smartcontractkit/chainlink-solana/pkg/solana/txm/mocks" + "github.com/smartcontractkit/chainlink-solana/pkg/solana/utils" - "github.com/smartcontractkit/chainlink-common/pkg/utils" + commonutils "github.com/smartcontractkit/chainlink-common/pkg/utils" "github.com/test-go/testify/require" ) @@ -158,12 +156,12 @@ func TestLookupTables(t *testing.T) { sender, err := solana.NewRandomPrivateKey() require.NoError(t, err) - client.FundTestAccounts(t, []solana.PublicKey{sender.PublicKey()}, url) + utils.FundAccounts(ctx, []solana.PrivateKey{sender}, c, t) cfg := config.NewDefault() solanaClient, err := client.NewClient(url, cfg, 5*time.Second, nil) - loader := utils.NewLazyLoad(func() (client.ReaderWriter, error) { return solanaClient, nil }) + loader := commonutils.NewLazyLoad(func() (client.ReaderWriter, error) { return solanaClient, nil }) mkey := keyMocks.NewSimpleKeystore(t) lggr := logger.Test(t) @@ -178,11 +176,10 @@ func TestLookupTables(t *testing.T) { DerivedLookupTables: nil, StaticLookupTables: []string{table.String()}, } - _, staticTableMap, err := chainWriter.ResolveLookupTables(ctx, lookupConfig, "test-debug-id") + _, staticTableMap, err := chainWriter.ResolveLookupTables(ctx, nil, lookupConfig, "test-debug-id") require.NoError(t, err) require.Equal(t, pubKeys, staticTableMap[table]) }) - t.Run("Derived lookup table resovles properly with constant address", func(t *testing.T) { pubKeys := createTestPubKeys(t, 8) table := CreateTestLookupTable(t, ctx, c, sender, pubKeys) @@ -200,7 +197,40 @@ func TestLookupTables(t *testing.T) { }, StaticLookupTables: nil, } - derivedTableMap, _, err := chainWriter.ResolveLookupTables(ctx, lookupConfig, "test-debug-id") + derivedTableMap, _, err := chainWriter.ResolveLookupTables(ctx, nil, lookupConfig, "test-debug-id") + require.NoError(t, err) + + addresses, ok := derivedTableMap["DerivedTable"][table.String()] + require.True(t, ok) + for i, address := range addresses { + require.Equal(t, pubKeys[i], address.PublicKey) + } + }) + + t.Run("Derived lookup table resolves properly with account lookup address", func(t *testing.T) { + pubKeys := createTestPubKeys(t, 8) + table := CreateTestLookupTable(t, ctx, c, sender, pubKeys) + lookupConfig := chainwriter.LookupTables{ + DerivedLookupTables: []chainwriter.DerivedLookupTable{ + { + Name: "DerivedTable", + Accounts: chainwriter.AccountLookup{ + Name: "TestLookupTable", + Location: "Inner.Address", + IsSigner: true, + }, + }, + }, + StaticLookupTables: nil, + } + + testArgs := TestArgs{ + Inner: []InnerArgs{ + {Address: table.Bytes()}, + }, + } + + derivedTableMap, _, err := chainWriter.ResolveLookupTables(ctx, testArgs, lookupConfig, "test-debug-id") require.NoError(t, err) addresses, ok := derivedTableMap["DerivedTable"][table.String()] @@ -224,19 +254,18 @@ func createTestPubKeys(t *testing.T, num int) solana.PublicKeySlice { func CreateTestLookupTable(t *testing.T, ctx context.Context, c *rpc.Client, sender solana.PrivateKey, addresses []solana.PublicKey) solana.PublicKey { // Create lookup tables slot, serr := c.GetSlot(ctx, rpc.CommitmentFinalized) - fmt.Println("SLOT: ", slot) require.NoError(t, serr) - table, instruction, ierr := NewCreateLookupTableInstruction( + table, instruction, ierr := utils.NewCreateLookupTableInstruction( sender.PublicKey(), sender.PublicKey(), slot, ) require.NoError(t, ierr) - SendAndConfirm(ctx, t, c, []solana.Instruction{instruction}, sender, rpc.CommitmentConfirmed) + utils.SendAndConfirm(ctx, t, c, []solana.Instruction{instruction}, sender, rpc.CommitmentConfirmed) // add entries to lookup table - SendAndConfirm(ctx, t, c, []solana.Instruction{ - NewExtendLookupTableInstruction( + utils.SendAndConfirm(ctx, t, c, []solana.Instruction{ + utils.NewExtendLookupTableInstruction( table, sender.PublicKey(), sender.PublicKey(), addresses, ), @@ -244,131 +273,3 @@ func CreateTestLookupTable(t *testing.T, ctx context.Context, c *rpc.Client, sen return table } - -// TxModifier is a dynamic function used to flexibly add components to a transaction such as additional signers, and compute budget parameters -type TxModifier func(tx *solana.Transaction, signers map[solana.PublicKey]solana.PrivateKey) error - -func SendAndConfirm(ctx context.Context, t *testing.T, rpcClient *rpc.Client, instructions []solana.Instruction, - signer solana.PrivateKey, commitment rpc.CommitmentType, opts ...TxModifier) *rpc.GetTransactionResult { - txres := sendTransaction(ctx, rpcClient, t, instructions, signer, commitment, false, opts...) // do not skipPreflight when expected to pass, preflight can help debug - - require.NotNil(t, txres.Meta) - require.Nil(t, txres.Meta.Err, fmt.Sprintf("tx failed with: %+v", txres.Meta)) // tx should not err, print meta if it does (contains logs) - return txres -} - -func sendTransaction(ctx context.Context, rpcClient *rpc.Client, t *testing.T, instructions []solana.Instruction, - signerAndPayer solana.PrivateKey, commitment rpc.CommitmentType, skipPreflight bool, opts ...TxModifier) *rpc.GetTransactionResult { - hashRes, err := rpcClient.GetLatestBlockhash(ctx, rpc.CommitmentFinalized) - require.NoError(t, err) - - tx, err := solana.NewTransaction( - instructions, - hashRes.Value.Blockhash, - solana.TransactionPayer(signerAndPayer.PublicKey()), - ) - require.NoError(t, err) - - // build signers map - signers := map[solana.PublicKey]solana.PrivateKey{} - signers[signerAndPayer.PublicKey()] = signerAndPayer - - // set options before signing transaction - for _, o := range opts { - require.NoError(t, o(tx, signers)) - } - - _, err = tx.Sign(func(pub solana.PublicKey) *solana.PrivateKey { - priv, ok := signers[pub] - require.True(t, ok, fmt.Sprintf("Missing signer private key for %s", pub)) - return &priv - }) - require.NoError(t, err) - - txsig, err := rpcClient.SendTransactionWithOpts(ctx, tx, rpc.TransactionOpts{SkipPreflight: skipPreflight, PreflightCommitment: rpc.CommitmentProcessed}) - require.NoError(t, err) - - var txStatus rpc.ConfirmationStatusType - count := 0 - for txStatus != rpc.ConfirmationStatusConfirmed && txStatus != rpc.ConfirmationStatusFinalized { - count++ - statusRes, sigErr := rpcClient.GetSignatureStatuses(ctx, true, txsig) - require.NoError(t, sigErr) - if statusRes != nil && len(statusRes.Value) > 0 && statusRes.Value[0] != nil { - txStatus = statusRes.Value[0].ConfirmationStatus - } - time.Sleep(100 * time.Millisecond) - if count > 50 { - require.NoError(t, fmt.Errorf("unable to find transaction within timeout")) - } - } - - txres, err := rpcClient.GetTransaction(ctx, txsig, &rpc.GetTransactionOpts{ - Commitment: commitment, - }) - require.NoError(t, err) - return txres -} - -var ( - AddressLookupTableProgram = solana.MustPublicKeyFromBase58("AddressLookupTab1e1111111111111111111111111") -) - -const ( - InstructionCreateLookupTable uint32 = iota - InstructionFreezeLookupTable - InstructionExtendLookupTable - InstructionDeactiveLookupTable - InstructionCloseLookupTable -) - -func NewCreateLookupTableInstruction( - authority, funder solana.PublicKey, - slot uint64, -) (solana.PublicKey, solana.Instruction, error) { - // https://github.com/solana-labs/solana-web3.js/blob/c1c98715b0c7900ce37c59bffd2056fa0037213d/src/programs/address-lookup-table/index.ts#L274 - slotLE := make([]byte, 8) - binary.LittleEndian.PutUint64(slotLE, slot) - account, bumpSeed, err := solana.FindProgramAddress([][]byte{authority.Bytes(), slotLE}, AddressLookupTableProgram) - if err != nil { - return solana.PublicKey{}, nil, err - } - - data := binary.LittleEndian.AppendUint32([]byte{}, InstructionCreateLookupTable) - data = binary.LittleEndian.AppendUint64(data, slot) - data = append(data, bumpSeed) - return account, solana.NewInstruction( - AddressLookupTableProgram, - solana.AccountMetaSlice{ - solana.Meta(account).WRITE(), - solana.Meta(authority).SIGNER(), - solana.Meta(funder).SIGNER().WRITE(), - solana.Meta(solana.SystemProgramID), - }, - data, - ), nil -} - -func NewExtendLookupTableInstruction( - table, authority, funder solana.PublicKey, - accounts []solana.PublicKey, -) solana.Instruction { - // https://github.com/solana-labs/solana-web3.js/blob/c1c98715b0c7900ce37c59bffd2056fa0037213d/src/programs/address-lookup-table/index.ts#L113 - - data := binary.LittleEndian.AppendUint32([]byte{}, InstructionExtendLookupTable) - data = binary.LittleEndian.AppendUint64(data, uint64(len(accounts))) // note: this is usually u32 + 8 byte buffer - for _, a := range accounts { - data = append(data, a.Bytes()...) - } - - return solana.NewInstruction( - AddressLookupTableProgram, - solana.AccountMetaSlice{ - solana.Meta(table).WRITE(), - solana.Meta(authority).SIGNER(), - solana.Meta(funder).SIGNER().WRITE(), - solana.Meta(solana.SystemProgramID), - }, - data, - ) -} diff --git a/pkg/solana/utils.go b/pkg/solana/utils.go deleted file mode 100644 index a4387aea8..000000000 --- a/pkg/solana/utils.go +++ /dev/null @@ -1,5 +0,0 @@ -package solana - -import "github.com/smartcontractkit/chainlink-solana/pkg/solana/internal" - -func LamportsToSol(lamports uint64) float64 { return internal.LamportsToSol(lamports) } diff --git a/pkg/solana/utils/utils.go b/pkg/solana/utils/utils.go new file mode 100644 index 000000000..3ce1f788e --- /dev/null +++ b/pkg/solana/utils/utils.go @@ -0,0 +1,178 @@ +package utils + +import ( + "context" + "encoding/binary" + "fmt" + "testing" + "time" + + "github.com/gagliardetto/solana-go" + "github.com/gagliardetto/solana-go/rpc" + "github.com/smartcontractkit/chainlink-solana/pkg/solana/internal" + "github.com/test-go/testify/require" +) + +func LamportsToSol(lamports uint64) float64 { return internal.LamportsToSol(lamports) } + +// TxModifier is a dynamic function used to flexibly add components to a transaction such as additional signers, and compute budget parameters +type TxModifier func(tx *solana.Transaction, signers map[solana.PublicKey]solana.PrivateKey) error + +func SendAndConfirm(ctx context.Context, t *testing.T, rpcClient *rpc.Client, instructions []solana.Instruction, + signer solana.PrivateKey, commitment rpc.CommitmentType, opts ...TxModifier) *rpc.GetTransactionResult { + txres := sendTransaction(ctx, rpcClient, t, instructions, signer, commitment, false, opts...) // do not skipPreflight when expected to pass, preflight can help debug + + require.NotNil(t, txres.Meta) + require.Nil(t, txres.Meta.Err, fmt.Sprintf("tx failed with: %+v", txres.Meta)) // tx should not err, print meta if it does (contains logs) + return txres +} + +func sendTransaction(ctx context.Context, rpcClient *rpc.Client, t *testing.T, instructions []solana.Instruction, + signerAndPayer solana.PrivateKey, commitment rpc.CommitmentType, skipPreflight bool, opts ...TxModifier) *rpc.GetTransactionResult { + hashRes, err := rpcClient.GetLatestBlockhash(ctx, rpc.CommitmentFinalized) + require.NoError(t, err) + + tx, err := solana.NewTransaction( + instructions, + hashRes.Value.Blockhash, + solana.TransactionPayer(signerAndPayer.PublicKey()), + ) + require.NoError(t, err) + + // build signers map + signers := map[solana.PublicKey]solana.PrivateKey{} + signers[signerAndPayer.PublicKey()] = signerAndPayer + + // set options before signing transaction + for _, o := range opts { + require.NoError(t, o(tx, signers)) + } + + _, err = tx.Sign(func(pub solana.PublicKey) *solana.PrivateKey { + priv, ok := signers[pub] + require.True(t, ok, fmt.Sprintf("Missing signer private key for %s", pub)) + return &priv + }) + require.NoError(t, err) + + txsig, err := rpcClient.SendTransactionWithOpts(ctx, tx, rpc.TransactionOpts{SkipPreflight: skipPreflight, PreflightCommitment: rpc.CommitmentProcessed}) + require.NoError(t, err) + + var txStatus rpc.ConfirmationStatusType + count := 0 + for txStatus != rpc.ConfirmationStatusConfirmed && txStatus != rpc.ConfirmationStatusFinalized { + count++ + statusRes, sigErr := rpcClient.GetSignatureStatuses(ctx, true, txsig) + require.NoError(t, sigErr) + if statusRes != nil && len(statusRes.Value) > 0 && statusRes.Value[0] != nil { + txStatus = statusRes.Value[0].ConfirmationStatus + } + time.Sleep(100 * time.Millisecond) + if count > 50 { + require.NoError(t, fmt.Errorf("unable to find transaction within timeout")) + } + } + + txres, err := rpcClient.GetTransaction(ctx, txsig, &rpc.GetTransactionOpts{ + Commitment: commitment, + }) + require.NoError(t, err) + return txres +} + +var ( + AddressLookupTableProgram = solana.MustPublicKeyFromBase58("AddressLookupTab1e1111111111111111111111111") +) + +const ( + InstructionCreateLookupTable uint32 = iota + InstructionFreezeLookupTable + InstructionExtendLookupTable + InstructionDeactiveLookupTable + InstructionCloseLookupTable +) + +func NewCreateLookupTableInstruction( + authority, funder solana.PublicKey, + slot uint64, +) (solana.PublicKey, solana.Instruction, error) { + // https://github.com/solana-labs/solana-web3.js/blob/c1c98715b0c7900ce37c59bffd2056fa0037213d/src/programs/address-lookup-table/index.ts#L274 + slotLE := make([]byte, 8) + binary.LittleEndian.PutUint64(slotLE, slot) + account, bumpSeed, err := solana.FindProgramAddress([][]byte{authority.Bytes(), slotLE}, AddressLookupTableProgram) + if err != nil { + return solana.PublicKey{}, nil, err + } + + data := binary.LittleEndian.AppendUint32([]byte{}, InstructionCreateLookupTable) + data = binary.LittleEndian.AppendUint64(data, slot) + data = append(data, bumpSeed) + return account, solana.NewInstruction( + AddressLookupTableProgram, + solana.AccountMetaSlice{ + solana.Meta(account).WRITE(), + solana.Meta(authority).SIGNER(), + solana.Meta(funder).SIGNER().WRITE(), + solana.Meta(solana.SystemProgramID), + }, + data, + ), nil +} + +func NewExtendLookupTableInstruction( + table, authority, funder solana.PublicKey, + accounts []solana.PublicKey, +) solana.Instruction { + // https://github.com/solana-labs/solana-web3.js/blob/c1c98715b0c7900ce37c59bffd2056fa0037213d/src/programs/address-lookup-table/index.ts#L113 + + data := binary.LittleEndian.AppendUint32([]byte{}, InstructionExtendLookupTable) + data = binary.LittleEndian.AppendUint64(data, uint64(len(accounts))) // note: this is usually u32 + 8 byte buffer + for _, a := range accounts { + data = append(data, a.Bytes()...) + } + + return solana.NewInstruction( + AddressLookupTableProgram, + solana.AccountMetaSlice{ + solana.Meta(table).WRITE(), + solana.Meta(authority).SIGNER(), + solana.Meta(funder).SIGNER().WRITE(), + solana.Meta(solana.SystemProgramID), + }, + data, + ) +} + +func FundAccounts(ctx context.Context, accounts []solana.PrivateKey, solanaGoClient *rpc.Client, t *testing.T) { + sigs := []solana.Signature{} + for _, v := range accounts { + sig, err := solanaGoClient.RequestAirdrop(ctx, v.PublicKey(), 1000*solana.LAMPORTS_PER_SOL, rpc.CommitmentFinalized) + require.NoError(t, err) + sigs = append(sigs, sig) + } + + // wait for confirmation so later transactions don't fail + remaining := len(sigs) + count := 0 + for remaining > 0 { + count++ + statusRes, sigErr := solanaGoClient.GetSignatureStatuses(ctx, true, sigs...) + require.NoError(t, sigErr) + require.NotNil(t, statusRes) + require.NotNil(t, statusRes.Value) + + unconfirmedTxCount := 0 + for _, res := range statusRes.Value { + if res == nil || res.ConfirmationStatus == rpc.ConfirmationStatusProcessed || res.ConfirmationStatus == rpc.ConfirmationStatusConfirmed { + unconfirmedTxCount++ + } + } + remaining = unconfirmedTxCount + fmt.Printf("Waiting for finalized funding on %d addresses\n", remaining) + + time.Sleep(500 * time.Millisecond) + if count > 60 { + require.NoError(t, fmt.Errorf("unable to find transaction within timeout")) + } + } +} diff --git a/pkg/solana/utils_test.go b/pkg/solana/utils/utils_test.go similarity index 71% rename from pkg/solana/utils_test.go rename to pkg/solana/utils/utils_test.go index 67efc932b..15a3e47d8 100644 --- a/pkg/solana/utils_test.go +++ b/pkg/solana/utils/utils_test.go @@ -1,8 +1,9 @@ -package solana +package utils_test import ( "testing" + "github.com/smartcontractkit/chainlink-solana/pkg/solana/utils" "github.com/stretchr/testify/assert" ) @@ -19,7 +20,7 @@ func TestLamportsToSol(t *testing.T) { for _, test := range tests { t.Run(test.name, func(t *testing.T) { - assert.Equal(t, test.out, LamportsToSol(test.in)) + assert.Equal(t, test.out, utils.LamportsToSol(test.in)) }) } } From e8fec1baa2f09bf3f097114f8d7e96a4396ba056 Mon Sep 17 00:00:00 2001 From: Silas Lenihan Date: Mon, 25 Nov 2024 11:49:10 -0500 Subject: [PATCH 19/22] Updated lookup tests and helpers --- pkg/solana/chainwriter/helpers.go | 22 +++-- pkg/solana/chainwriter/lookups.go | 32 ++++--- pkg/solana/chainwriter/lookups_test.go | 128 +++++++++++++++++++++---- 3 files changed, 148 insertions(+), 34 deletions(-) diff --git a/pkg/solana/chainwriter/helpers.go b/pkg/solana/chainwriter/helpers.go index 256d10c25..c2a143d98 100644 --- a/pkg/solana/chainwriter/helpers.go +++ b/pkg/solana/chainwriter/helpers.go @@ -9,9 +9,9 @@ import ( "github.com/gagliardetto/solana-go" ) -// GetAddressAtLocation parses through nested types and arrays to find all address locations. -func GetAddressAtLocation(args any, location string, debugID string) ([]solana.PublicKey, error) { - var addresses []solana.PublicKey +// GetValuesAtLocation parses through nested types and arrays to find all locations of values +func GetValuesAtLocation(args any, location string, debugID string) ([][]byte, error) { + var vals [][]byte path := strings.Split(location, ".") @@ -22,13 +22,15 @@ func GetAddressAtLocation(args any, location string, debugID string) ([]solana.P for _, value := range addressList { if byteArray, ok := value.([]byte); ok { - addresses = append(addresses, solana.PublicKeyFromBytes(byteArray)) + vals = append(vals, byteArray) + } else if address, ok := value.(solana.PublicKey); ok { + vals = append(vals, address.Bytes()) } else { - return nil, errorWithDebugID(fmt.Errorf("invalid address format at path: %s", location), debugID) + return nil, errorWithDebugID(fmt.Errorf("invalid value format at path: %s", location), debugID) } } - return addresses, nil + return vals, nil } func GetDebugIDAtLocation(args any, location string) (string, error) { @@ -83,6 +85,7 @@ func traversePath(data any, path []string) ([]any, error) { if val.Kind() == reflect.Ptr { val = val.Elem() } + fmt.Printf("Current path: %v, Current value type: %v\n", path, val.Kind()) switch val.Kind() { case reflect.Struct: @@ -105,6 +108,13 @@ func traversePath(data any, path []string) ([]any, error) { } return nil, errors.New("no matching field found in array") + case reflect.Map: + key := reflect.ValueOf(path[0]) + value := val.MapIndex(key) + if !value.IsValid() { + return nil, errors.New("key not found: " + path[0]) + } + return traversePath(value.Interface(), path[1:]) default: if len(path) == 1 && val.Kind() == reflect.Slice && val.Type().Elem().Kind() == reflect.Uint8 { return []any{val.Interface()}, nil diff --git a/pkg/solana/chainwriter/lookups.go b/pkg/solana/chainwriter/lookups.go index 72ee99e90..c16e442b2 100644 --- a/pkg/solana/chainwriter/lookups.go +++ b/pkg/solana/chainwriter/lookups.go @@ -79,15 +79,15 @@ func (ac AccountConstant) Resolve(_ context.Context, _ any, _ map[string]map[str } func (al AccountLookup) Resolve(_ context.Context, args any, _ map[string]map[string][]*solana.AccountMeta, debugID string) ([]*solana.AccountMeta, error) { - derivedAddresses, err := GetAddressAtLocation(args, al.Location, debugID) + derivedValues, err := GetValuesAtLocation(args, al.Location, debugID) if err != nil { return nil, errorWithDebugID(fmt.Errorf("error getting account from lookup: %w", err), debugID) } var metas []*solana.AccountMeta - for _, address := range derivedAddresses { + for _, address := range derivedValues { metas = append(metas, &solana.AccountMeta{ - PublicKey: address, + PublicKey: solana.PublicKeyFromBytes(address), IsSigner: al.IsSigner, IsWritable: al.IsWritable, }) @@ -146,16 +146,26 @@ func getSeedBytes(ctx context.Context, lookup PDALookups, args any, derivedTable // Process AddressSeeds first (e.g., public keys) for _, seed := range lookup.Seeds { - // Get the address(es) at the seed location - seedAddresses, err := GetAddresses(ctx, args, []Lookup{seed}, derivedTableMap, debugID) - if err != nil { - return nil, errorWithDebugID(fmt.Errorf("error getting address seed: %w", err), debugID) - } + if lookupSeed, ok := seed.(AccountLookup); ok { + // Get the values at the seed location + bytes, err := GetValuesAtLocation(args, lookupSeed.Location, debugID) + if err != nil { + return nil, errorWithDebugID(fmt.Errorf("error getting address seed: %w", err), debugID) + } + seedBytes = append(seedBytes, bytes...) + } else { + // Get the address(es) at the seed location + seedAddresses, err := GetAddresses(ctx, args, []Lookup{seed}, derivedTableMap, debugID) + if err != nil { + return nil, errorWithDebugID(fmt.Errorf("error getting address seed: %w", err), debugID) + } - // Add each address seed as bytes - for _, address := range seedAddresses { - seedBytes = append(seedBytes, address.PublicKey.Bytes()) + // Add each address seed as bytes + for _, address := range seedAddresses { + seedBytes = append(seedBytes, address.PublicKey.Bytes()) + } } + } return seedBytes, nil diff --git a/pkg/solana/chainwriter/lookups_test.go b/pkg/solana/chainwriter/lookups_test.go index 6658750ff..a196fa2d1 100644 --- a/pkg/solana/chainwriter/lookups_test.go +++ b/pkg/solana/chainwriter/lookups_test.go @@ -29,7 +29,6 @@ type InnerArgs struct { } func TestAccountContant(t *testing.T) { - t.Run("AccountConstant resolves valid address", func(t *testing.T) { expectedAddr := "4Nn9dsYBcSTzRbK9hg9kzCUdrCSkMZq1UR6Vw1Tkaf6M" expectedMeta := []*solana.AccountMeta{ @@ -131,22 +130,117 @@ func TestAccountLookups(t *testing.T) { } func TestPDALookups(t *testing.T) { - // TODO: May require deploying a program to test - // t.Run("PDALookup resolves valid address", func(t *testing.T) { - // expectedAddr := "4Nn9dsYBcSTzRbK9hg9kzCUdrCSkMZq1UR6Vw1Tkaf6M" - // expectedMeta := []*solana.AccountMeta{ - // { - // PublicKey: solana.MustPublicKeyFromBase58(expectedAddr), - // IsSigner: true, - // IsWritable: true, - // }, - // } - // lookupConfig := chainwriter.PDALookups{ - // Name: "TestAccount", - // PublicKey: - // } - - // }) + programID := solana.SystemProgramID + + t.Run("PDALookup resolves valid PDA with constant address seeds", func(t *testing.T) { + privKey, err := solana.NewRandomPrivateKey() + require.NoError(t, err) + seed := privKey.PublicKey() + + pda, _, err := solana.FindProgramAddress([][]byte{seed.Bytes()}, programID) + require.NoError(t, err) + + expectedMeta := []*solana.AccountMeta{ + { + PublicKey: pda, + IsSigner: false, + IsWritable: true, + }, + } + + pdaLookup := chainwriter.PDALookups{ + Name: "TestPDA", + PublicKey: chainwriter.AccountConstant{Name: "ProgramID", Address: programID.String()}, + Seeds: []chainwriter.Lookup{ + chainwriter.AccountConstant{Name: "seed", Address: seed.String()}, + }, + IsSigner: false, + IsWritable: true, + } + + ctx := context.Background() + result, err := pdaLookup.Resolve(ctx, nil, nil, "") + require.NoError(t, err) + require.Equal(t, expectedMeta, result) + }) + t.Run("PDALookup resolves valid PDA with non-address lookup seeds", func(t *testing.T) { + seed1 := []byte("test_seed") + seed2 := []byte("another_seed") + + pda, _, err := solana.FindProgramAddress([][]byte{seed1, seed2}, programID) + require.NoError(t, err) + + expectedMeta := []*solana.AccountMeta{ + { + PublicKey: pda, + IsSigner: false, + IsWritable: true, + }, + } + + pdaLookup := chainwriter.PDALookups{ + Name: "TestPDA", + PublicKey: chainwriter.AccountConstant{Name: "ProgramID", Address: programID.String()}, + Seeds: []chainwriter.Lookup{ + chainwriter.AccountLookup{Name: "seed1", Location: "test_seed"}, + chainwriter.AccountLookup{Name: "seed2", Location: "another_seed"}, + }, + IsSigner: false, + IsWritable: true, + } + + ctx := context.Background() + args := map[string]interface{}{ + "test_seed": seed1, + "another_seed": seed2, + } + + result, err := pdaLookup.Resolve(ctx, args, nil, "") + require.NoError(t, err) + require.Equal(t, expectedMeta, result) + }) + + t.Run("PDALookup resolves valid PDA with address lookup seeds", func(t *testing.T) { + privKey1, err := solana.NewRandomPrivateKey() + require.NoError(t, err) + seed1 := privKey1.PublicKey() + + privKey2, err := solana.NewRandomPrivateKey() + require.NoError(t, err) + seed2 := privKey2.PublicKey() + + pda, _, err := solana.FindProgramAddress([][]byte{seed1.Bytes(), seed2.Bytes()}, programID) + require.NoError(t, err) + + expectedMeta := []*solana.AccountMeta{ + { + PublicKey: pda, + IsSigner: false, + IsWritable: true, + }, + } + + pdaLookup := chainwriter.PDALookups{ + Name: "TestPDA", + PublicKey: chainwriter.AccountConstant{Name: "ProgramID", Address: programID.String()}, + Seeds: []chainwriter.Lookup{ + chainwriter.AccountLookup{Name: "seed1", Location: "test_seed"}, + chainwriter.AccountLookup{Name: "seed2", Location: "another_seed"}, + }, + IsSigner: false, + IsWritable: true, + } + + ctx := context.Background() + args := map[string]interface{}{ + "test_seed": seed1, + "another_seed": seed2, + } + + result, err := pdaLookup.Resolve(ctx, args, nil, "") + require.NoError(t, err) + require.Equal(t, expectedMeta, result) + }) } func TestLookupTables(t *testing.T) { From 183e7e28f2de49da6e56dc515eb8f70fc8c77f10 Mon Sep 17 00:00:00 2001 From: Silas Lenihan Date: Mon, 25 Nov 2024 11:51:15 -0500 Subject: [PATCH 20/22] Removed helpers_test --- pkg/solana/chainwriter/helpers_test.go | 109 ------------------------- 1 file changed, 109 deletions(-) delete mode 100644 pkg/solana/chainwriter/helpers_test.go diff --git a/pkg/solana/chainwriter/helpers_test.go b/pkg/solana/chainwriter/helpers_test.go deleted file mode 100644 index ba9df5c58..000000000 --- a/pkg/solana/chainwriter/helpers_test.go +++ /dev/null @@ -1,109 +0,0 @@ -package chainwriter_test - -// import ( -// "context" -// "testing" - -// "github.com/gagliardetto/solana-go" -// "github.com/smartcontractkit/chainlink-solana/pkg/solana/chainwriter" -// "github.com/test-go/testify/assert" -// "github.com/test-go/testify/require" -// ) - -// type TestStruct struct { -// Messages []Message -// } - -// type Message struct { -// TokenAmounts []TokenAmount -// } - -// type TokenAmount struct { -// SourceTokenAddress []byte -// DestTokenAddress []byte -// } - -// func TestHelpersTestGetAddresses(t *testing.T) { -// ctx := context.TODO() - -// chainWriterConfig := chainwriter.ChainWriterConfig{} -// service := chainwriter.NewChainWriterService(chainWriterConfig) - -// t.Run("success with AccountConstant", func(t *testing.T) { -// accounts := []chainwriter.Lookup{ -// chainwriter.AccountConstant{ -// Name: "test-account", -// Address: "4Nn9dsYBcSTzRbK9hg9kzCUdrCSkMZq1UR6Vw1Tkaf6M", -// IsSigner: true, -// IsWritable: false, -// }, -// } - -// // Call GetAddresses with the constant account -// addresses, err := service.GetAddresses(ctx, nil, accounts, nil, "test-debug-id") -// require.NoError(t, err) -// require.Len(t, addresses, 1) -// require.Equal(t, addresses[0].PublicKey.String(), "4Nn9dsYBcSTzRbK9hg9kzCUdrCSkMZq1UR6Vw1Tkaf6M") -// require.True(t, addresses[0].IsSigner) -// require.False(t, addresses[0].IsWritable) -// }) - -// t.Run("success with AccountLookup", func(t *testing.T) { -// accounts := []chainwriter.Lookup{ -// chainwriter.AccountLookup{ -// Name: "test-account", -// Location: "Messages.TokenAmounts.SourceTokenAddress", -// IsSigner: true, -// IsWritable: false, -// }, -// chainwriter.AccountLookup{ -// Name: "test-account", -// Location: "Messages.TokenAmounts.DestTokenAddress", -// IsSigner: true, -// IsWritable: false, -// }, -// } - -// // Create a test struct with the expected address -// addresses := make([][]byte, 8) -// for i := 0; i < 8; i++ { -// privKey, err := solana.NewRandomPrivateKey() -// require.NoError(t, err) -// addresses[i] = privKey.PublicKey().Bytes() -// } - -// exampleDecoded := TestStruct{ -// Messages: []Message{ -// { -// TokenAmounts: []TokenAmount{ -// {addresses[0], addresses[1]}, -// {addresses[2], addresses[3]}, -// }, -// }, -// { -// TokenAmounts: []TokenAmount{ -// {addresses[4], addresses[5]}, -// {addresses[6], addresses[7]}, -// }, -// }, -// }, -// } -// // Call GetAddresses with the lookup account -// derivedAddresses, err := service.GetAddresses(ctx, exampleDecoded, accounts, nil, "test-debug-id") - -// // Create a map of the expected addresses for fast lookup -// expectedAddresses := make(map[string]bool) -// for _, addr := range addresses { -// expectedAddresses[string(addr)] = true -// } - -// // Verify that each derived address matches an expected address -// for _, derivedAddr := range derivedAddresses { -// derivedBytes := derivedAddr.PublicKey.Bytes() -// assert.True(t, expectedAddresses[string(derivedBytes)], "Address not found in expected list") -// } - -// require.NoError(t, err) -// require.Len(t, derivedAddresses, 8) -// }) -// } From ed1270d80db39de1cc39ec5a28f0425255872d42 Mon Sep 17 00:00:00 2001 From: Silas Lenihan Date: Mon, 25 Nov 2024 12:06:25 -0500 Subject: [PATCH 21/22] refactored ccip example --- ..._writer_test.go => ccip_example_config.go} | 124 +++++++++--------- 1 file changed, 61 insertions(+), 63 deletions(-) rename pkg/solana/chainwriter/{chain_writer_test.go => ccip_example_config.go} (80%) diff --git a/pkg/solana/chainwriter/chain_writer_test.go b/pkg/solana/chainwriter/ccip_example_config.go similarity index 80% rename from pkg/solana/chainwriter/chain_writer_test.go rename to pkg/solana/chainwriter/ccip_example_config.go index 85b05b12b..bd5087af8 100644 --- a/pkg/solana/chainwriter/chain_writer_test.go +++ b/pkg/solana/chainwriter/ccip_example_config.go @@ -1,14 +1,12 @@ -package chainwriter_test +package chainwriter import ( "fmt" - "testing" commoncodec "github.com/smartcontractkit/chainlink-common/pkg/codec" - "github.com/smartcontractkit/chainlink-solana/pkg/solana/chainwriter" ) -func TestGetAddresses(t *testing.T) { +func TestConfig() { // Fake constant addresses for the purpose of this example. registryAddress := "4Nn9dsYBcSTzRbK9hg9kzCUdrCSkMZq1UR6Vw1Tkaf6A" routerProgramAddress := "4Nn9dsYBcSTzRbK9hg9kzCUdrCSkMZq1UR6Vw1Tkaf6B" @@ -23,7 +21,7 @@ func TestGetAddresses(t *testing.T) { executionReportSingleChainIDL := `{"name":"ExecutionReportSingleChain","type":{"kind":"struct","fields":[{"name":"source_chain_selector","type":"u64"},{"name":"message","type":{"defined":"Any2SolanaRampMessage"}},{"name":"root","type":{"array":["u8",32]}},{"name":"proofs","type":{"vec":{"array":["u8",32]}}}]}},{"name":"Any2SolanaRampMessage","type":{"kind":"struct","fields":[{"name":"header","type":{"defined":"RampMessageHeader"}},{"name":"sender","type":{"vec":"u8"}},{"name":"data","type":{"vec":"u8"}},{"name":"receiver","type":{"array":["u8",32]}},{"name":"extra_args","type":{"defined":"SolanaExtraArgs"}}]}},{"name":"RampMessageHeader","type":{"kind":"struct","fields":[{"name":"message_id","type":{"array":["u8",32]}},{"name":"source_chain_selector","type":"u64"},{"name":"dest_chain_selector","type":"u64"},{"name":"sequence_number","type":"u64"},{"name":"nonce","type":"u64"}]}},{"name":"SolanaExtraArgs","type":{"kind":"struct","fields":[{"name":"compute_units","type":"u32"},{"name":"allow_out_of_order_execution","type":"bool"}]}}` - executeConfig := chainwriter.MethodConfig{ + executeConfig := MethodConfig{ FromAddress: userAddress, InputModifications: commoncodec.ModifiersConfig{ // remove merkle root since it isn't a part of the on-chain type @@ -35,29 +33,29 @@ func TestGetAddresses(t *testing.T) { // LookupTables are on-chain stores of accounts. They can be used in two ways: // 1. As a way to store a list of accounts that are all associated together (i.e. Token State registry) // 2. To compress the transactions in a TX and reduce the size of the TX. (The traditional way) - LookupTables: chainwriter.LookupTables{ + LookupTables: LookupTables{ // DerivedLookupTables are useful in both the ways described above. // a. The user can configure any type of look up to get a list of lookupTables to read from. // b. The ChainWriter reads from this lookup table and store the internal addresses in memory // c. Later, in the []Accounts the user can specify which accounts to include in the TX with an AccountsFromLookupTable lookup. // d. Lastly, the lookup table is used to compress the size of the transaction. - DerivedLookupTables: []chainwriter.DerivedLookupTable{ + DerivedLookupTables: []DerivedLookupTable{ { Name: "RegistryTokenState", // In this case, the user configured the lookup table accounts to use a PDALookup, which // generates a list of one of more PDA accounts based on the input parameters. Specifically, // there will be multple PDA accounts if there are multiple addresses in the message, otherwise, // there will only be one PDA account to read from. The PDA account corresponds to the lookup table. - Accounts: chainwriter.PDALookups{ + Accounts: PDALookups{ Name: "RegistryTokenState", - PublicKey: chainwriter.AccountConstant{ + PublicKey: AccountConstant{ Address: registryAddress, IsSigner: false, IsWritable: false, }, // Seeds would be used if the user needed to look up addresses to use as seeds, which isn't the case here. - Seeds: []chainwriter.Lookup{ - chainwriter.AccountLookup{Location: "Message.TokenAmounts.DestTokenAddress"}, + Seeds: []Lookup{ + AccountLookup{Location: "Message.TokenAmounts.DestTokenAddress"}, }, IsSigner: false, IsWritable: false, @@ -74,7 +72,7 @@ func TestGetAddresses(t *testing.T) { }, // The Accounts field is where the user specifies which accounts to include in the transaction. Each Lookup // resolves to one or more on-chain addresses. - Accounts: []chainwriter.Lookup{ + Accounts: []Lookup{ // The accounts can be of any of the following types: // 1. Account constant // 2. Account Lookup - Based on data from input parameters @@ -86,51 +84,51 @@ func TestGetAddresses(t *testing.T) { // PDALookups can resolve to multiple addresses if: // A) The PublicKey lookup resolves to multiple addresses (i.e. multiple token addresses) // B) The Seeds or ValueSeeds resolve to multiple values - chainwriter.PDALookups{ + PDALookups{ Name: "PerChainConfig", // PublicKey is a constant account in this case, not a lookup. - PublicKey: chainwriter.AccountConstant{ + PublicKey: AccountConstant{ Address: registryAddress, IsSigner: false, IsWritable: false, }, // Similar to the RegistryTokenState above, the user is looking up PDA accounts based on the dest tokens. - Seeds: []chainwriter.Lookup{ - chainwriter.AccountLookup{Location: "Message.TokenAmounts.DestTokenAddress"}, - chainwriter.AccountLookup{Location: "Message.Header.DestChainSelector"}, + Seeds: []Lookup{ + AccountLookup{Location: "Message.TokenAmounts.DestTokenAddress"}, + AccountLookup{Location: "Message.Header.DestChainSelector"}, }, IsSigner: false, IsWritable: false, }, // Lookup Table content - Get the accounts from the derived lookup table above - chainwriter.AccountsFromLookupTable{ + AccountsFromLookupTable{ LookupTablesName: "RegistryTokenState", IncludeIndexes: []int{}, // If left empty, all addresses will be included. Otherwise, only the specified indexes will be included. }, // Account Lookup - Based on data from input parameters // In this case, the user wants to add the destination token addresses to the transaction. // Once again, this can be one or multiple addresses. - chainwriter.AccountLookup{ + AccountLookup{ Name: "TokenAccount", Location: "Message.TokenAmounts.DestTokenAddress", IsSigner: false, IsWritable: false, }, // PDA Account Lookup - Based on an account lookup and an address lookup - chainwriter.PDALookups{ + PDALookups{ // In this case, the token address is the public key, and the receiver is the seed. // Again, there could be multiple token addresses, in which case this would resolve to // multiple PDA accounts. Name: "ReceiverAssociatedTokenAccount", - PublicKey: chainwriter.AccountLookup{ + PublicKey: AccountLookup{ Name: "TokenAccount", Location: "Message.TokenAmounts.DestTokenAddress", IsSigner: false, IsWritable: false, }, // The seed is the receiver address. - Seeds: []chainwriter.Lookup{ - chainwriter.AccountLookup{ + Seeds: []Lookup{ + AccountLookup{ Name: "Receiver", Location: "Message.Receiver", IsSigner: false, @@ -139,69 +137,69 @@ func TestGetAddresses(t *testing.T) { }, }, // Account constant - chainwriter.AccountConstant{ + AccountConstant{ Name: "Registry", Address: registryAddress, IsSigner: false, IsWritable: false, }, // PDA Lookup for the RegistryTokenConfig. - chainwriter.PDALookups{ + PDALookups{ Name: "RegistryTokenConfig", // constant public key - PublicKey: chainwriter.AccountConstant{ + PublicKey: AccountConstant{ Address: registryAddress, IsSigner: false, IsWritable: false, }, // The seed, once again, is the destination token address. - Seeds: []chainwriter.Lookup{ - chainwriter.AccountLookup{Location: "Message.TokenAmounts.DestTokenAddress"}, + Seeds: []Lookup{ + AccountLookup{Location: "Message.TokenAmounts.DestTokenAddress"}, }, IsSigner: false, IsWritable: false, }, // Account constant - chainwriter.AccountConstant{ + AccountConstant{ Name: "RouterProgram", Address: routerProgramAddress, IsSigner: false, IsWritable: false, }, // Account constant - chainwriter.AccountConstant{ + AccountConstant{ Name: "RouterAccountConfig", Address: routerAccountConfigAddress, IsSigner: false, IsWritable: false, }, // PDA lookup to get the Router Chain Config - chainwriter.PDALookups{ + PDALookups{ Name: "RouterChainConfig", // The public key is a constant Router address. - PublicKey: chainwriter.AccountConstant{ + PublicKey: AccountConstant{ Address: routerProgramAddress, IsSigner: false, IsWritable: false, }, - Seeds: []chainwriter.Lookup{ - chainwriter.AccountLookup{Location: "Message.Header.DestChainSelector"}, - chainwriter.AccountLookup{Location: "Message.Header.SourceChainSelector"}, + Seeds: []Lookup{ + AccountLookup{Location: "Message.Header.DestChainSelector"}, + AccountLookup{Location: "Message.Header.SourceChainSelector"}, }, IsSigner: false, IsWritable: false, }, // PDA lookup to get the Router Report Accounts. - chainwriter.PDALookups{ + PDALookups{ Name: "RouterReportAccount", // The public key is a constant Router address. - PublicKey: chainwriter.AccountConstant{ + PublicKey: AccountConstant{ Address: routerProgramAddress, IsSigner: false, IsWritable: false, }, - Seeds: []chainwriter.Lookup{ - chainwriter.AccountLookup{ + Seeds: []Lookup{ + AccountLookup{ // The seed is the merkle root of the report, as passed into the input params. Location: "args.MerkleRoot", }, @@ -210,44 +208,44 @@ func TestGetAddresses(t *testing.T) { IsWritable: false, }, // PDA lookup to get UserNoncePerChain - chainwriter.PDALookups{ + PDALookups{ Name: "UserNoncePerChain", // The public key is a constant Router address. - PublicKey: chainwriter.AccountConstant{ + PublicKey: AccountConstant{ Address: routerProgramAddress, IsSigner: false, IsWritable: false, }, // In this case, the user configured multiple seeds. These will be used in conjunction // with the public key to generate one or multiple PDA accounts. - Seeds: []chainwriter.Lookup{ - chainwriter.AccountLookup{Location: "Message.Receiver"}, - chainwriter.AccountLookup{Location: "Message.Header.DestChainSelector"}, + Seeds: []Lookup{ + AccountLookup{Location: "Message.Receiver"}, + AccountLookup{Location: "Message.Header.DestChainSelector"}, }, }, // Account constant - chainwriter.AccountConstant{ + AccountConstant{ Name: "CPISigner", Address: cpiSignerAddress, IsSigner: true, IsWritable: false, }, // Account constant - chainwriter.AccountConstant{ + AccountConstant{ Name: "SystemProgram", Address: systemProgramAddress, IsSigner: true, IsWritable: false, }, // Account constant - chainwriter.AccountConstant{ + AccountConstant{ Name: "ComputeBudgetProgram", Address: computeBudgetProgramAddress, IsSigner: true, IsWritable: false, }, // Account constant - chainwriter.AccountConstant{ + AccountConstant{ Name: "SysvarProgram", Address: sysvarProgramAddress, IsSigner: true, @@ -259,42 +257,42 @@ func TestGetAddresses(t *testing.T) { DebugIDLocation: "Message.MessageID", } - commitConfig := chainwriter.MethodConfig{ + commitConfig := MethodConfig{ FromAddress: userAddress, InputModifications: nil, ChainSpecificName: "commit", - LookupTables: chainwriter.LookupTables{ + LookupTables: LookupTables{ StaticLookupTables: []string{ commonAddressesLookupTable, routerLookupTable, }, }, - Accounts: []chainwriter.Lookup{ + Accounts: []Lookup{ // Account constant - chainwriter.AccountConstant{ + AccountConstant{ Name: "RouterProgram", Address: routerProgramAddress, IsSigner: false, IsWritable: false, }, // Account constant - chainwriter.AccountConstant{ + AccountConstant{ Name: "RouterAccountConfig", Address: routerAccountConfigAddress, IsSigner: false, IsWritable: false, }, // PDA lookup to get the Router Report Accounts. - chainwriter.PDALookups{ + PDALookups{ Name: "RouterReportAccount", // The public key is a constant Router address. - PublicKey: chainwriter.AccountConstant{ + PublicKey: AccountConstant{ Address: routerProgramAddress, IsSigner: false, IsWritable: false, }, - Seeds: []chainwriter.Lookup{ - chainwriter.AccountLookup{ + Seeds: []Lookup{ + AccountLookup{ // The seed is the merkle root of the report, as passed into the input params. Location: "args.MerkleRoots", }, @@ -303,21 +301,21 @@ func TestGetAddresses(t *testing.T) { IsWritable: false, }, // Account constant - chainwriter.AccountConstant{ + AccountConstant{ Name: "SystemProgram", Address: systemProgramAddress, IsSigner: true, IsWritable: false, }, // Account constant - chainwriter.AccountConstant{ + AccountConstant{ Name: "ComputeBudgetProgram", Address: computeBudgetProgramAddress, IsSigner: true, IsWritable: false, }, // Account constant - chainwriter.AccountConstant{ + AccountConstant{ Name: "SysvarProgram", Address: sysvarProgramAddress, IsSigner: true, @@ -327,10 +325,10 @@ func TestGetAddresses(t *testing.T) { DebugIDLocation: "", } - chainWriterConfig := chainwriter.ChainWriterConfig{ - Programs: map[string]chainwriter.ProgramConfig{ + chainWriterConfig := ChainWriterConfig{ + Programs: map[string]ProgramConfig{ "ccip-router": { - Methods: map[string]chainwriter.MethodConfig{ + Methods: map[string]MethodConfig{ "execute": executeConfig, "commit": commitConfig, }, From 843eff76aa150adbd38d031a847417c41b4a394d Mon Sep 17 00:00:00 2001 From: Silas Lenihan Date: Tue, 26 Nov 2024 14:49:08 -0500 Subject: [PATCH 22/22] Completed chained lookup integration test --- contracts/Anchor.toml | 5 +- contracts/Cargo.lock | 7 + .../localnet/write_test-keypair.json | 1 + contracts/pnpm-lock.yaml | 60 +++-- contracts/programs/write_test/Cargo.toml | 19 ++ contracts/programs/write_test/Xargo.toml | 2 + contracts/programs/write_test/src/lib.rs | 52 ++++ pkg/solana/chainwriter/chain_writer.go | 13 +- pkg/solana/chainwriter/helpers.go | 6 +- pkg/solana/chainwriter/lookups.go | 151 ++++++++--- pkg/solana/chainwriter/lookups_test.go | 244 ++++++++++++++---- pkg/solana/client/test_helpers.go | 1 + pkg/solana/utils/utils.go | 42 ++- 13 files changed, 474 insertions(+), 129 deletions(-) create mode 100644 contracts/artifacts/localnet/write_test-keypair.json create mode 100644 contracts/programs/write_test/Cargo.toml create mode 100644 contracts/programs/write_test/Xargo.toml create mode 100644 contracts/programs/write_test/src/lib.rs diff --git a/contracts/Anchor.toml b/contracts/Anchor.toml index 78a2222ad..0f8fd5b99 100644 --- a/contracts/Anchor.toml +++ b/contracts/Anchor.toml @@ -27,6 +27,7 @@ test = "pnpm run test" [programs.localnet] access_controller = "9xi644bRR8birboDGdTiwBq3C7VEeR7VuamRYYXCubUW" -log-read-test = "J1zQwrBNBngz26jRPNWsUSZMHJwBwpkoDitXRV95LdK4" +log_read_test = "J1zQwrBNBngz26jRPNWsUSZMHJwBwpkoDitXRV95LdK4" ocr_2 = "cjg3oHmg9uuPsP8D6g29NWvhySJkdYdAo9D25PRbKXJ" # need to rename the idl to satisfy anchor.js... -store = "HEvSKofvBgfaexv23kMabbYqxasxU3mQ4ibBMEmJWHny" \ No newline at end of file +store = "HEvSKofvBgfaexv23kMabbYqxasxU3mQ4ibBMEmJWHny" +write_test = "39vbQVpEMtZtg3e6ZSE7nBSzmNZptmW45WnLkbqEe4TU" \ No newline at end of file diff --git a/contracts/Cargo.lock b/contracts/Cargo.lock index 953b2b81e..43a9fa6c6 100644 --- a/contracts/Cargo.lock +++ b/contracts/Cargo.lock @@ -2664,6 +2664,13 @@ dependencies = [ "memchr", ] +[[package]] +name = "write-test" +version = "0.1.0" +dependencies = [ + "anchor-lang", +] + [[package]] name = "zerocopy" version = "0.7.32" diff --git a/contracts/artifacts/localnet/write_test-keypair.json b/contracts/artifacts/localnet/write_test-keypair.json new file mode 100644 index 000000000..c4e6e125c --- /dev/null +++ b/contracts/artifacts/localnet/write_test-keypair.json @@ -0,0 +1 @@ +[26,39,164,161,246,97,149,0,58,187,146,162,53,35,107,2,117,242,83,171,48,7,63,240,69,221,239,45,97,55,112,106,192,228,214,205,123,71,58,23,62,229,166,213,149,122,96,145,35,150,16,156,247,199,242,108,173,80,62,231,39,196,27,192] \ No newline at end of file diff --git a/contracts/pnpm-lock.yaml b/contracts/pnpm-lock.yaml index 860108de1..b7cec1551 100644 --- a/contracts/pnpm-lock.yaml +++ b/contracts/pnpm-lock.yaml @@ -13,13 +13,13 @@ importers: version: link:../ts '@coral-xyz/anchor': specifier: ^0.29.0 - version: 0.29.0 + version: 0.29.0(bufferutil@4.0.8)(utf-8-validate@5.0.10) '@solana/spl-token': specifier: ^0.3.5 - version: 0.3.11(@solana/web3.js@1.92.3)(fastestsmallesttextencoderdecoder@1.0.22) + version: 0.3.11(@solana/web3.js@1.92.3(bufferutil@4.0.8)(utf-8-validate@5.0.10))(bufferutil@4.0.8)(fastestsmallesttextencoderdecoder@1.0.22)(utf-8-validate@5.0.10) '@solana/web3.js': specifier: ^1.50.1 <=1.92.3 - version: 1.92.3 + version: 1.92.3(bufferutil@4.0.8)(utf-8-validate@5.0.10) '@types/chai': specifier: ^4.2.22 version: 4.3.12 @@ -893,11 +893,11 @@ snapshots: dependencies: regenerator-runtime: 0.14.1 - '@coral-xyz/anchor@0.29.0': + '@coral-xyz/anchor@0.29.0(bufferutil@4.0.8)(utf-8-validate@5.0.10)': dependencies: - '@coral-xyz/borsh': 0.29.0(@solana/web3.js@1.95.3) + '@coral-xyz/borsh': 0.29.0(@solana/web3.js@1.95.3(bufferutil@4.0.8)(utf-8-validate@5.0.10)) '@noble/hashes': 1.5.0 - '@solana/web3.js': 1.95.3 + '@solana/web3.js': 1.95.3(bufferutil@4.0.8)(utf-8-validate@5.0.10) bn.js: 5.2.1 bs58: 4.0.1 buffer-layout: 1.2.2 @@ -914,9 +914,9 @@ snapshots: - encoding - utf-8-validate - '@coral-xyz/borsh@0.29.0(@solana/web3.js@1.95.3)': + '@coral-xyz/borsh@0.29.0(@solana/web3.js@1.95.3(bufferutil@4.0.8)(utf-8-validate@5.0.10))': dependencies: - '@solana/web3.js': 1.95.3 + '@solana/web3.js': 1.95.3(bufferutil@4.0.8)(utf-8-validate@5.0.10) bn.js: 5.2.1 buffer-layout: 1.2.2 @@ -926,10 +926,10 @@ snapshots: '@noble/hashes@1.5.0': {} - '@solana/buffer-layout-utils@0.2.0': + '@solana/buffer-layout-utils@0.2.0(bufferutil@4.0.8)(utf-8-validate@5.0.10)': dependencies: '@solana/buffer-layout': 4.0.1 - '@solana/web3.js': 1.95.3 + '@solana/web3.js': 1.95.3(bufferutil@4.0.8)(utf-8-validate@5.0.10) bigint-buffer: 1.1.5 bignumber.js: 9.1.2 transitivePeerDependencies: @@ -963,7 +963,7 @@ snapshots: '@solana/codecs-core': 2.0.0-experimental.8618508 '@solana/codecs-numbers': 2.0.0-experimental.8618508 - '@solana/spl-token-metadata@0.1.2(@solana/web3.js@1.92.3)(fastestsmallesttextencoderdecoder@1.0.22)': + '@solana/spl-token-metadata@0.1.2(@solana/web3.js@1.92.3(bufferutil@4.0.8)(utf-8-validate@5.0.10))(fastestsmallesttextencoderdecoder@1.0.22)': dependencies: '@solana/codecs-core': 2.0.0-experimental.8618508 '@solana/codecs-data-structures': 2.0.0-experimental.8618508 @@ -971,16 +971,16 @@ snapshots: '@solana/codecs-strings': 2.0.0-experimental.8618508(fastestsmallesttextencoderdecoder@1.0.22) '@solana/options': 2.0.0-experimental.8618508 '@solana/spl-type-length-value': 0.1.0 - '@solana/web3.js': 1.92.3 + '@solana/web3.js': 1.92.3(bufferutil@4.0.8)(utf-8-validate@5.0.10) transitivePeerDependencies: - fastestsmallesttextencoderdecoder - '@solana/spl-token@0.3.11(@solana/web3.js@1.92.3)(fastestsmallesttextencoderdecoder@1.0.22)': + '@solana/spl-token@0.3.11(@solana/web3.js@1.92.3(bufferutil@4.0.8)(utf-8-validate@5.0.10))(bufferutil@4.0.8)(fastestsmallesttextencoderdecoder@1.0.22)(utf-8-validate@5.0.10)': dependencies: '@solana/buffer-layout': 4.0.1 - '@solana/buffer-layout-utils': 0.2.0 - '@solana/spl-token-metadata': 0.1.2(@solana/web3.js@1.92.3)(fastestsmallesttextencoderdecoder@1.0.22) - '@solana/web3.js': 1.92.3 + '@solana/buffer-layout-utils': 0.2.0(bufferutil@4.0.8)(utf-8-validate@5.0.10) + '@solana/spl-token-metadata': 0.1.2(@solana/web3.js@1.92.3(bufferutil@4.0.8)(utf-8-validate@5.0.10))(fastestsmallesttextencoderdecoder@1.0.22) + '@solana/web3.js': 1.92.3(bufferutil@4.0.8)(utf-8-validate@5.0.10) buffer: 6.0.3 transitivePeerDependencies: - bufferutil @@ -992,7 +992,7 @@ snapshots: dependencies: buffer: 6.0.3 - '@solana/web3.js@1.92.3': + '@solana/web3.js@1.92.3(bufferutil@4.0.8)(utf-8-validate@5.0.10)': dependencies: '@babel/runtime': 7.25.6 '@noble/curves': 1.6.0 @@ -1005,7 +1005,7 @@ snapshots: bs58: 4.0.1 buffer: 6.0.3 fast-stable-stringify: 1.0.0 - jayson: 4.1.2 + jayson: 4.1.2(bufferutil@4.0.8)(utf-8-validate@5.0.10) node-fetch: 2.7.0 rpc-websockets: 8.0.1 superstruct: 1.0.4 @@ -1014,7 +1014,7 @@ snapshots: - encoding - utf-8-validate - '@solana/web3.js@1.95.3': + '@solana/web3.js@1.95.3(bufferutil@4.0.8)(utf-8-validate@5.0.10)': dependencies: '@babel/runtime': 7.25.6 '@noble/curves': 1.6.0 @@ -1027,7 +1027,7 @@ snapshots: bs58: 4.0.1 buffer: 6.0.3 fast-stable-stringify: 1.0.0 - jayson: 4.1.2 + jayson: 4.1.2(bufferutil@4.0.8)(utf-8-validate@5.0.10) node-fetch: 2.7.0 rpc-websockets: 9.0.2 superstruct: 2.0.2 @@ -1185,6 +1185,7 @@ snapshots: bufferutil@4.0.8: dependencies: node-gyp-build: 4.8.2 + optional: true camelcase@6.3.0: {} @@ -1268,6 +1269,7 @@ snapshots: debug@4.3.3(supports-color@8.1.1): dependencies: ms: 2.1.2 + optionalDependencies: supports-color: 8.1.1 decamelize@4.0.0: {} @@ -1433,11 +1435,11 @@ snapshots: isexe@2.0.0: {} - isomorphic-ws@4.0.1(ws@7.5.10): + isomorphic-ws@4.0.1(ws@7.5.10(bufferutil@4.0.8)(utf-8-validate@5.0.10)): dependencies: - ws: 7.5.10 + ws: 7.5.10(bufferutil@4.0.8)(utf-8-validate@5.0.10) - jayson@4.1.2: + jayson@4.1.2(bufferutil@4.0.8)(utf-8-validate@5.0.10): dependencies: '@types/connect': 3.4.38 '@types/node': 12.20.55 @@ -1447,10 +1449,10 @@ snapshots: delay: 5.0.0 es6-promisify: 5.0.0 eyes: 0.1.8 - isomorphic-ws: 4.0.1(ws@7.5.10) + isomorphic-ws: 4.0.1(ws@7.5.10(bufferutil@4.0.8)(utf-8-validate@5.0.10)) json-stringify-safe: 5.0.1 uuid: 8.3.2 - ws: 7.5.10 + ws: 7.5.10(bufferutil@4.0.8)(utf-8-validate@5.0.10) transitivePeerDependencies: - bufferutil - utf-8-validate @@ -1767,6 +1769,7 @@ snapshots: utf-8-validate@5.0.10: dependencies: node-gyp-build: 4.8.2 + optional: true util-deprecate@1.0.2: {} @@ -1793,10 +1796,13 @@ snapshots: wrappy@1.0.2: {} - ws@7.5.10: {} + ws@7.5.10(bufferutil@4.0.8)(utf-8-validate@5.0.10): + optionalDependencies: + bufferutil: 4.0.8 + utf-8-validate: 5.0.10 ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@5.0.10): - dependencies: + optionalDependencies: bufferutil: 4.0.8 utf-8-validate: 5.0.10 diff --git a/contracts/programs/write_test/Cargo.toml b/contracts/programs/write_test/Cargo.toml new file mode 100644 index 000000000..ee46888c6 --- /dev/null +++ b/contracts/programs/write_test/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "write-test" +version = "0.1.0" +description = "Created with Anchor" +edition = "2021" + +[lib] +crate-type = ["cdylib", "lib"] +name = "write_test" + +[features] +no-entrypoint = [] +no-idl = [] +no-log-ix-name = [] +cpi = ["no-entrypoint"] +default = [] + +[dependencies] +anchor-lang = "0.29.0" diff --git a/contracts/programs/write_test/Xargo.toml b/contracts/programs/write_test/Xargo.toml new file mode 100644 index 000000000..475fb71ed --- /dev/null +++ b/contracts/programs/write_test/Xargo.toml @@ -0,0 +1,2 @@ +[target.bpfel-unknown-unknown.dependencies.std] +features = [] diff --git a/contracts/programs/write_test/src/lib.rs b/contracts/programs/write_test/src/lib.rs new file mode 100644 index 000000000..4078bca4d --- /dev/null +++ b/contracts/programs/write_test/src/lib.rs @@ -0,0 +1,52 @@ +use anchor_lang::prelude::*; + +declare_id!("39vbQVpEMtZtg3e6ZSE7nBSzmNZptmW45WnLkbqEe4TU"); + +#[program] +pub mod write_test { + use super::*; + + pub fn initialize(ctx: Context, lookup_table: Pubkey) -> Result<()> { + let data = &mut ctx.accounts.data_account; + data.version = 1; + data.administrator = ctx.accounts.admin.key(); + data.pending_administrator = Pubkey::default(); + data.lookup_table = lookup_table; + + Ok(()) + } + +} + +#[derive(Accounts)] +pub struct Initialize<'info> { + /// PDA account, derived from seeds and created by the System Program in this instruction + #[account( + init, // Initialize the account + payer = admin, // Specify the payer + space = DataAccount::SIZE, // Specify the account size + seeds = [b"data"], // Define the PDA seeds + bump // Use the bump seed + )] + pub data_account: Account<'info, DataAccount>, + + /// Admin account that pays for PDA creation and signs the transaction + #[account(mut)] + pub admin: Signer<'info>, + + /// System Program is required for PDA creation + pub system_program: Program<'info, System>, +} + +#[account] +pub struct DataAccount { + pub version: u8, + pub administrator: Pubkey, + pub pending_administrator: Pubkey, + pub lookup_table: Pubkey, +} + +impl DataAccount { + /// The total size of the `DataAccount` struct, including the discriminator + pub const SIZE: usize = 8 + 1 + 32 * 3; // 8 bytes for discriminator + 1 byte for version + 32 bytes * 3 pubkeys +} diff --git a/pkg/solana/chainwriter/chain_writer.go b/pkg/solana/chainwriter/chain_writer.go index 7dea709aa..608f3c610 100644 --- a/pkg/solana/chainwriter/chain_writer.go +++ b/pkg/solana/chainwriter/chain_writer.go @@ -131,10 +131,10 @@ for Solana transactions. It handles constant addresses, dynamic lookups, program */ // GetAddresses resolves account addresses from various `Lookup` configurations to build the required `solana.AccountMeta` list // for Solana transactions. -func GetAddresses(ctx context.Context, args any, accounts []Lookup, derivedTableMap map[string]map[string][]*solana.AccountMeta, debugID string) ([]*solana.AccountMeta, error) { +func GetAddresses(ctx context.Context, args any, accounts []Lookup, derivedTableMap map[string]map[string][]*solana.AccountMeta, reader client.Reader) ([]*solana.AccountMeta, error) { var addresses []*solana.AccountMeta for _, accountConfig := range accounts { - meta, err := accountConfig.Resolve(ctx, args, derivedTableMap, debugID) + meta, err := accountConfig.Resolve(ctx, args, derivedTableMap, reader) if err != nil { return nil, err } @@ -147,7 +147,6 @@ func (s *SolanaChainWriterService) FilterLookupTableAddresses( accounts []*solana.AccountMeta, derivedTableMap map[string]map[string][]*solana.AccountMeta, staticTableMap map[solana.PublicKey]solana.PublicKeySlice, - debugID string, ) map[solana.PublicKey]solana.PublicKeySlice { filteredLookupTables := make(map[solana.PublicKey]solana.PublicKeySlice) @@ -162,7 +161,7 @@ func (s *SolanaChainWriterService) FilterLookupTableAddresses( for innerIdentifier, metas := range innerMap { tableKey, err := solana.PublicKeyFromBase58(innerIdentifier) if err != nil { - errorWithDebugID(fmt.Errorf("error parsing lookup table key: %w", err), debugID) + fmt.Errorf("error parsing lookup table key: %w", err) } // Collect public keys that are actually used @@ -212,19 +211,19 @@ func (s *SolanaChainWriterService) SubmitTransaction(ctx context.Context, contra } // Fetch derived and static table maps - derivedTableMap, staticTableMap, err := s.ResolveLookupTables(ctx, args, methodConfig.LookupTables, debugID) + derivedTableMap, staticTableMap, err := s.ResolveLookupTables(ctx, args, methodConfig.LookupTables) if err != nil { return errorWithDebugID(fmt.Errorf("error getting lookup tables: %w", err), debugID) } // Resolve account metas - accounts, err := GetAddresses(ctx, args, methodConfig.Accounts, derivedTableMap, debugID) + accounts, err := GetAddresses(ctx, args, methodConfig.Accounts, derivedTableMap, s.reader) if err != nil { return errorWithDebugID(fmt.Errorf("error resolving account addresses: %w", err), debugID) } // Filter the lookup table addresses based on which accounts are actually used - filteredLookupTableMap := s.FilterLookupTableAddresses(accounts, derivedTableMap, staticTableMap, debugID) + filteredLookupTableMap := s.FilterLookupTableAddresses(accounts, derivedTableMap, staticTableMap) // Fetch latest blockhash blockhash, err := s.reader.LatestBlockhash(ctx) diff --git a/pkg/solana/chainwriter/helpers.go b/pkg/solana/chainwriter/helpers.go index c2a143d98..4d5d00600 100644 --- a/pkg/solana/chainwriter/helpers.go +++ b/pkg/solana/chainwriter/helpers.go @@ -10,9 +10,8 @@ import ( ) // GetValuesAtLocation parses through nested types and arrays to find all locations of values -func GetValuesAtLocation(args any, location string, debugID string) ([][]byte, error) { +func GetValuesAtLocation(args any, location string) ([][]byte, error) { var vals [][]byte - path := strings.Split(location, ".") addressList, err := traversePath(args, path) @@ -26,7 +25,7 @@ func GetValuesAtLocation(args any, location string, debugID string) ([][]byte, e } else if address, ok := value.(solana.PublicKey); ok { vals = append(vals, address.Bytes()) } else { - return nil, errorWithDebugID(fmt.Errorf("invalid value format at path: %s", location), debugID) + return nil, fmt.Errorf("invalid value format at path: %s", location) } } @@ -85,7 +84,6 @@ func traversePath(data any, path []string) ([]any, error) { if val.Kind() == reflect.Ptr { val = val.Elem() } - fmt.Printf("Current path: %v, Current value type: %v\n", path, val.Kind()) switch val.Kind() { case reflect.Struct: diff --git a/pkg/solana/chainwriter/lookups.go b/pkg/solana/chainwriter/lookups.go index c16e442b2..1aa9ae92d 100644 --- a/pkg/solana/chainwriter/lookups.go +++ b/pkg/solana/chainwriter/lookups.go @@ -3,7 +3,9 @@ package chainwriter import ( "context" "fmt" + "reflect" + ag_binary "github.com/gagliardetto/binary" "github.com/gagliardetto/solana-go" addresslookuptable "github.com/gagliardetto/solana-go/programs/address-lookup-table" "github.com/gagliardetto/solana-go/rpc" @@ -11,7 +13,7 @@ import ( ) type Lookup interface { - Resolve(ctx context.Context, args any, derivedTableMap map[string]map[string][]*solana.AccountMeta, debugID string) ([]*solana.AccountMeta, error) + Resolve(ctx context.Context, args any, derivedTableMap map[string]map[string][]*solana.AccountMeta, reader client.Reader) ([]*solana.AccountMeta, error) } // AccountConstant represents a fixed address, provided in Base58 format, converted into a `solana.PublicKey`. @@ -40,6 +42,13 @@ type PDALookups struct { Seeds []Lookup IsSigner bool IsWritable bool + // OPTIONAL: On-chain location and type of desired data from PDA (e.g. a sub-account of the data account) + InternalField InternalField +} + +type InternalField struct { + Type reflect.Type + Location string } type ValueLookup struct { @@ -64,10 +73,10 @@ type AccountsFromLookupTable struct { IncludeIndexes []int } -func (ac AccountConstant) Resolve(_ context.Context, _ any, _ map[string]map[string][]*solana.AccountMeta, debugID string) ([]*solana.AccountMeta, error) { +func (ac AccountConstant) Resolve(_ context.Context, _ any, _ map[string]map[string][]*solana.AccountMeta, _ client.Reader) ([]*solana.AccountMeta, error) { address, err := solana.PublicKeyFromBase58(ac.Address) if err != nil { - return nil, errorWithDebugID(fmt.Errorf("error getting account from constant: %w", err), debugID) + return nil, fmt.Errorf("error getting account from constant: %w", err) } return []*solana.AccountMeta{ { @@ -78,10 +87,10 @@ func (ac AccountConstant) Resolve(_ context.Context, _ any, _ map[string]map[str }, nil } -func (al AccountLookup) Resolve(_ context.Context, args any, _ map[string]map[string][]*solana.AccountMeta, debugID string) ([]*solana.AccountMeta, error) { - derivedValues, err := GetValuesAtLocation(args, al.Location, debugID) +func (al AccountLookup) Resolve(_ context.Context, args any, _ map[string]map[string][]*solana.AccountMeta, _ client.Reader) ([]*solana.AccountMeta, error) { + derivedValues, err := GetValuesAtLocation(args, al.Location) if err != nil { - return nil, errorWithDebugID(fmt.Errorf("error getting account from lookup: %w", err), debugID) + return nil, fmt.Errorf("error getting account from lookup: %w", err) } var metas []*solana.AccountMeta @@ -95,11 +104,11 @@ func (al AccountLookup) Resolve(_ context.Context, args any, _ map[string]map[st return metas, nil } -func (alt AccountsFromLookupTable) Resolve(_ context.Context, _ any, derivedTableMap map[string]map[string][]*solana.AccountMeta, debugID string) ([]*solana.AccountMeta, error) { +func (alt AccountsFromLookupTable) Resolve(_ context.Context, _ any, derivedTableMap map[string]map[string][]*solana.AccountMeta, _ client.Reader) ([]*solana.AccountMeta, error) { // Fetch the inner map for the specified lookup table name innerMap, ok := derivedTableMap[alt.LookupTablesName] if !ok { - return nil, errorWithDebugID(fmt.Errorf("lookup table not found: %s", alt.LookupTablesName), debugID) + return nil, fmt.Errorf("lookup table not found: %s", alt.LookupTablesName) } var result []*solana.AccountMeta @@ -116,7 +125,7 @@ func (alt AccountsFromLookupTable) Resolve(_ context.Context, _ any, derivedTabl for publicKey, metas := range innerMap { for _, index := range alt.IncludeIndexes { if index < 0 || index >= len(metas) { - return nil, errorWithDebugID(fmt.Errorf("invalid index %d for account %s in lookup table %s", index, publicKey, alt.LookupTablesName), debugID) + return nil, fmt.Errorf("invalid index %d for account %s in lookup table %s", index, publicKey, alt.LookupTablesName) } result = append(result, metas[index]) } @@ -125,39 +134,97 @@ func (alt AccountsFromLookupTable) Resolve(_ context.Context, _ any, derivedTabl return result, nil } -func (pda PDALookups) Resolve(ctx context.Context, args any, derivedTableMap map[string]map[string][]*solana.AccountMeta, debugID string) ([]*solana.AccountMeta, error) { - publicKeys, err := GetAddresses(ctx, args, []Lookup{pda.PublicKey}, derivedTableMap, debugID) +func (pda PDALookups) Resolve(ctx context.Context, args any, derivedTableMap map[string]map[string][]*solana.AccountMeta, reader client.Reader) ([]*solana.AccountMeta, error) { + publicKeys, err := GetAddresses(ctx, args, []Lookup{pda.PublicKey}, derivedTableMap, reader) + if err != nil { + return nil, fmt.Errorf("error getting public key for PDALookups: %w", err) + } + + seeds, err := getSeedBytes(ctx, pda, args, derivedTableMap, reader) + if err != nil { + return nil, fmt.Errorf("error getting seeds for PDALookups: %w", err) + } + + pdas, err := generatePDAs(publicKeys, seeds, pda) if err != nil { - return nil, errorWithDebugID(fmt.Errorf("error getting public key for PDALookups: %w", err), debugID) + return nil, fmt.Errorf("error generating PDAs: %w", err) + } + + if pda.InternalField.Location == "" { + return pdas, nil + } + + // If a decoded location is specified, fetch the data at that location + var result []*solana.AccountMeta + for _, accountMeta := range pdas { + accountInfo, err := reader.GetAccountInfoWithOpts(ctx, accountMeta.PublicKey, &rpc.GetAccountInfoOpts{ + Encoding: "base64", + Commitment: rpc.CommitmentFinalized, + }) + + if err != nil || accountInfo == nil || accountInfo.Value == nil { + return nil, fmt.Errorf("error fetching account info for PDA account: %s, error: %w", accountMeta.PublicKey.String(), err) + } + + decoded, err := decodeBorshIntoType(accountInfo.GetBinary(), pda.InternalField.Type) + if err != nil { + return nil, fmt.Errorf("error decoding Borsh data dynamically: %w", err) + } + + value, err := GetValuesAtLocation(decoded, pda.InternalField.Location) + if err != nil { + return nil, fmt.Errorf("error getting value at location: %w", err) + } + if len(value) > 1 { + return nil, fmt.Errorf("multiple values found at location: %s", pda.InternalField.Location) + } + + result = append(result, &solana.AccountMeta{ + PublicKey: solana.PublicKeyFromBytes(value[0]), + IsSigner: accountMeta.IsSigner, + IsWritable: accountMeta.IsWritable, + }) + } + return result, nil +} + +func decodeBorshIntoType(data []byte, typ reflect.Type) (interface{}, error) { + // Ensure the type is a struct + if typ.Kind() != reflect.Struct { + return nil, fmt.Errorf("provided type is not a struct: %s", typ.Kind()) } - seeds, err := getSeedBytes(ctx, pda, args, derivedTableMap, debugID) + // Create a new instance of the type + instance := reflect.New(typ).Interface() + + // Decode using Borsh + err := ag_binary.NewBorshDecoder(data).Decode(instance) if err != nil { - return nil, errorWithDebugID(fmt.Errorf("error getting seeds for PDALookups: %w", err), debugID) + return nil, fmt.Errorf("error decoding Borsh data: %w", err) } - return generatePDAs(publicKeys, seeds, pda, debugID) + // Return the underlying value (not a pointer) + return reflect.ValueOf(instance).Elem().Interface(), nil } // getSeedBytes extracts the seeds for the PDALookups. // It handles both AddressSeeds (which are public keys) and ValueSeeds (which are byte arrays from input args). -func getSeedBytes(ctx context.Context, lookup PDALookups, args any, derivedTableMap map[string]map[string][]*solana.AccountMeta, debugID string) ([][]byte, error) { +func getSeedBytes(ctx context.Context, lookup PDALookups, args any, derivedTableMap map[string]map[string][]*solana.AccountMeta, reader client.Reader) ([][]byte, error) { var seedBytes [][]byte - // Process AddressSeeds first (e.g., public keys) for _, seed := range lookup.Seeds { if lookupSeed, ok := seed.(AccountLookup); ok { - // Get the values at the seed location - bytes, err := GetValuesAtLocation(args, lookupSeed.Location, debugID) + // Get value from a location (This doens't have to be an address, it can be any value) + bytes, err := GetValuesAtLocation(args, lookupSeed.Location) if err != nil { - return nil, errorWithDebugID(fmt.Errorf("error getting address seed: %w", err), debugID) + return nil, fmt.Errorf("error getting address seed: %w", err) } seedBytes = append(seedBytes, bytes...) } else { - // Get the address(es) at the seed location - seedAddresses, err := GetAddresses(ctx, args, []Lookup{seed}, derivedTableMap, debugID) + // Get address seeds from the lookup + seedAddresses, err := GetAddresses(ctx, args, []Lookup{seed}, derivedTableMap, reader) if err != nil { - return nil, errorWithDebugID(fmt.Errorf("error getting address seed: %w", err), debugID) + return nil, fmt.Errorf("error getting address seed: %w", err) } // Add each address seed as bytes @@ -165,23 +232,22 @@ func getSeedBytes(ctx context.Context, lookup PDALookups, args any, derivedTable seedBytes = append(seedBytes, address.PublicKey.Bytes()) } } - } return seedBytes, nil } // generatePDAs generates program-derived addresses (PDAs) from public keys and seeds. -func generatePDAs(publicKeys []*solana.AccountMeta, seeds [][]byte, lookup PDALookups, debugID string) ([]*solana.AccountMeta, error) { +func generatePDAs(publicKeys []*solana.AccountMeta, seeds [][]byte, lookup PDALookups) ([]*solana.AccountMeta, error) { if len(seeds) > 1 && len(publicKeys) > 1 { - return nil, errorWithDebugID(fmt.Errorf("multiple public keys and multiple seeds are not allowed"), debugID) + return nil, fmt.Errorf("multiple public keys and multiple seeds are not allowed") } var addresses []*solana.AccountMeta for _, publicKeyMeta := range publicKeys { address, _, err := solana.FindProgramAddress(seeds, publicKeyMeta.PublicKey) if err != nil { - return nil, errorWithDebugID(fmt.Errorf("error finding program address: %w", err), debugID) + return nil, fmt.Errorf("error finding program address: %w", err) } addresses = append(addresses, &solana.AccountMeta{ PublicKey: address, @@ -192,15 +258,15 @@ func generatePDAs(publicKeys []*solana.AccountMeta, seeds [][]byte, lookup PDALo return addresses, nil } -func (s *SolanaChainWriterService) ResolveLookupTables(ctx context.Context, args any, lookupTables LookupTables, debugID string) (map[string]map[string][]*solana.AccountMeta, map[solana.PublicKey]solana.PublicKeySlice, error) { +func (s *SolanaChainWriterService) ResolveLookupTables(ctx context.Context, args any, lookupTables LookupTables) (map[string]map[string][]*solana.AccountMeta, map[solana.PublicKey]solana.PublicKeySlice, error) { derivedTableMap := make(map[string]map[string][]*solana.AccountMeta) staticTableMap := make(map[solana.PublicKey]solana.PublicKeySlice) // Read derived lookup tables for _, derivedLookup := range lookupTables.DerivedLookupTables { - lookupTableMap, _, err := s.LoadTable(ctx, args, derivedLookup, s.reader, derivedTableMap, debugID) + lookupTableMap, _, err := s.LoadTable(ctx, args, derivedLookup, s.reader, derivedTableMap) if err != nil { - return nil, nil, errorWithDebugID(fmt.Errorf("error loading derived lookup table: %w", err), debugID) + return nil, nil, fmt.Errorf("error loading derived lookup table: %w", err) } // Merge the loaded table map into the result @@ -219,21 +285,24 @@ func (s *SolanaChainWriterService) ResolveLookupTables(ctx context.Context, args // Parse the static table address tableAddress, err := solana.PublicKeyFromBase58(staticTable) if err != nil { - return nil, nil, errorWithDebugID(fmt.Errorf("invalid static lookup table address: %s, error: %w", staticTable, err), debugID) + return nil, nil, fmt.Errorf("invalid static lookup table address: %s, error: %w", staticTable, err) } - addressses, err := getLookupTableAddress(ctx, s.reader, tableAddress, debugID) + addressses, err := getLookupTableAddress(ctx, s.reader, tableAddress) + if err != nil { + return nil, nil, fmt.Errorf("error fetching static lookup table address: %w", err) + } staticTableMap[tableAddress] = addressses } return derivedTableMap, staticTableMap, nil } -func (s *SolanaChainWriterService) LoadTable(ctx context.Context, args any, rlt DerivedLookupTable, reader client.Reader, derivedTableMap map[string]map[string][]*solana.AccountMeta, debugID string) (map[string]map[string][]*solana.AccountMeta, []*solana.AccountMeta, error) { +func (s *SolanaChainWriterService) LoadTable(ctx context.Context, args any, rlt DerivedLookupTable, reader client.Reader, derivedTableMap map[string]map[string][]*solana.AccountMeta) (map[string]map[string][]*solana.AccountMeta, []*solana.AccountMeta, error) { // Resolve all addresses specified by the identifier - lookupTableAddresses, err := GetAddresses(ctx, args, []Lookup{rlt.Accounts}, nil, debugID) + lookupTableAddresses, err := GetAddresses(ctx, args, []Lookup{rlt.Accounts}, nil, reader) if err != nil { - return nil, nil, errorWithDebugID(fmt.Errorf("error resolving addresses for lookup table: %w", err), debugID) + return nil, nil, fmt.Errorf("error resolving addresses for lookup table: %w", err) } resultMap := make(map[string]map[string][]*solana.AccountMeta) @@ -242,9 +311,9 @@ func (s *SolanaChainWriterService) LoadTable(ctx context.Context, args any, rlt // Iterate over each address of the lookup table for _, addressMeta := range lookupTableAddresses { // Fetch account info - addresses, err := getLookupTableAddress(ctx, reader, addressMeta.PublicKey, debugID) + addresses, err := getLookupTableAddress(ctx, reader, addressMeta.PublicKey) if err != nil { - return nil, nil, errorWithDebugID(fmt.Errorf("error fetching lookup table address: %w", err), debugID) + return nil, nil, fmt.Errorf("error fetching lookup table address: %w", err) } // Create the inner map for this lookup table @@ -268,19 +337,19 @@ func (s *SolanaChainWriterService) LoadTable(ctx context.Context, args any, rlt return resultMap, lookupTableMetas, nil } -func getLookupTableAddress(ctx context.Context, reader client.Reader, tableAddress solana.PublicKey, debugID string) (solana.PublicKeySlice, error) { +func getLookupTableAddress(ctx context.Context, reader client.Reader, tableAddress solana.PublicKey) (solana.PublicKeySlice, error) { // Fetch the account info for the static table accountInfo, err := reader.GetAccountInfoWithOpts(ctx, tableAddress, &rpc.GetAccountInfoOpts{ Encoding: "base64", - Commitment: rpc.CommitmentConfirmed, + Commitment: rpc.CommitmentFinalized, }) if err != nil || accountInfo == nil || accountInfo.Value == nil { - return nil, errorWithDebugID(fmt.Errorf("error fetching account info for table: %s, error: %w", tableAddress.String(), err), debugID) + return nil, fmt.Errorf("error fetching account info for table: %s, error: %w", tableAddress.String(), err) } alt, err := addresslookuptable.DecodeAddressLookupTableState(accountInfo.GetBinary()) if err != nil { - return nil, errorWithDebugID(fmt.Errorf("error decoding address lookup table state: %w", err), debugID) + return nil, fmt.Errorf("error decoding address lookup table state: %w", err) } return alt.Addresses, nil } diff --git a/pkg/solana/chainwriter/lookups_test.go b/pkg/solana/chainwriter/lookups_test.go index a196fa2d1..2a75814bf 100644 --- a/pkg/solana/chainwriter/lookups_test.go +++ b/pkg/solana/chainwriter/lookups_test.go @@ -2,6 +2,8 @@ package chainwriter_test import ( "context" + "crypto/sha256" + "reflect" "testing" "time" @@ -28,38 +30,46 @@ type InnerArgs struct { Address []byte } +type DataAccount struct { + Discriminator [8]byte + Version uint8 + Administrator solana.PublicKey + PendingAdministrator solana.PublicKey + LookupTable solana.PublicKey +} + func TestAccountContant(t *testing.T) { t.Run("AccountConstant resolves valid address", func(t *testing.T) { - expectedAddr := "4Nn9dsYBcSTzRbK9hg9kzCUdrCSkMZq1UR6Vw1Tkaf6M" + expectedAddr := getRandomPubKey(t) expectedMeta := []*solana.AccountMeta{ { - PublicKey: solana.MustPublicKeyFromBase58(expectedAddr), + PublicKey: expectedAddr, IsSigner: true, IsWritable: true, }, } constantConfig := chainwriter.AccountConstant{ Name: "TestAccount", - Address: expectedAddr, + Address: expectedAddr.String(), IsSigner: true, IsWritable: true, } - result, err := constantConfig.Resolve(nil, nil, nil, "") + result, err := constantConfig.Resolve(nil, nil, nil, nil) require.NoError(t, err) require.Equal(t, expectedMeta, result) }) } func TestAccountLookups(t *testing.T) { t.Run("AccountLookup resolves valid address with just one address", func(t *testing.T) { - expectedAddr := "4Nn9dsYBcSTzRbK9hg9kzCUdrCSkMZq1UR6Vw1Tkaf6M" + expectedAddr := getRandomPubKey(t) testArgs := TestArgs{ Inner: []InnerArgs{ - {Address: solana.MustPublicKeyFromBase58(expectedAddr).Bytes()}, + {Address: expectedAddr.Bytes()}, }, } expectedMeta := []*solana.AccountMeta{ { - PublicKey: solana.MustPublicKeyFromBase58(expectedAddr), + PublicKey: expectedAddr, IsSigner: true, IsWritable: true, }, @@ -71,28 +81,29 @@ func TestAccountLookups(t *testing.T) { IsSigner: true, IsWritable: true, } - result, err := lookupConfig.Resolve(nil, testArgs, nil, "") + result, err := lookupConfig.Resolve(nil, testArgs, nil, nil) require.NoError(t, err) require.Equal(t, expectedMeta, result) }) t.Run("AccountLookup resolves valid address with just multiple addresses", func(t *testing.T) { - expectedAddr1 := "4Nn9dsYBcSTzRbK9hg9kzCUdrCSkMZq1UR6Vw1Tkaf6M" - expectedAddr2 := "4Nn9dsYBcSTzRbK9hg9kzCUdrCSkMZq1UR6Vw1Tkaf6N" + expectedAddr1 := getRandomPubKey(t) + expectedAddr2 := getRandomPubKey(t) + testArgs := TestArgs{ Inner: []InnerArgs{ - {Address: solana.MustPublicKeyFromBase58(expectedAddr1).Bytes()}, - {Address: solana.MustPublicKeyFromBase58(expectedAddr2).Bytes()}, + {Address: expectedAddr1.Bytes()}, + {Address: expectedAddr2.Bytes()}, }, } expectedMeta := []*solana.AccountMeta{ { - PublicKey: solana.MustPublicKeyFromBase58(expectedAddr1), + PublicKey: expectedAddr1, IsSigner: true, IsWritable: true, }, { - PublicKey: solana.MustPublicKeyFromBase58(expectedAddr2), + PublicKey: expectedAddr2, IsSigner: true, IsWritable: true, }, @@ -104,7 +115,7 @@ func TestAccountLookups(t *testing.T) { IsSigner: true, IsWritable: true, } - result, err := lookupConfig.Resolve(nil, testArgs, nil, "") + result, err := lookupConfig.Resolve(nil, testArgs, nil, nil) require.NoError(t, err) for i, meta := range result { require.Equal(t, expectedMeta[i], meta) @@ -112,10 +123,11 @@ func TestAccountLookups(t *testing.T) { }) t.Run("AccountLookup fails when address isn't in args", func(t *testing.T) { - expectedAddr := "4Nn9dsYBcSTzRbK9hg9kzCUdrCSkMZq1UR6Vw1Tkaf6M" + expectedAddr := getRandomPubKey(t) + testArgs := TestArgs{ Inner: []InnerArgs{ - {Address: solana.MustPublicKeyFromBase58(expectedAddr).Bytes()}, + {Address: expectedAddr.Bytes()}, }, } lookupConfig := chainwriter.AccountLookup{ @@ -124,7 +136,7 @@ func TestAccountLookups(t *testing.T) { IsSigner: true, IsWritable: true, } - _, err := lookupConfig.Resolve(nil, testArgs, nil, "") + _, err := lookupConfig.Resolve(nil, testArgs, nil, nil) require.Error(t, err) }) } @@ -133,9 +145,7 @@ func TestPDALookups(t *testing.T) { programID := solana.SystemProgramID t.Run("PDALookup resolves valid PDA with constant address seeds", func(t *testing.T) { - privKey, err := solana.NewRandomPrivateKey() - require.NoError(t, err) - seed := privKey.PublicKey() + seed := getRandomPubKey(t) pda, _, err := solana.FindProgramAddress([][]byte{seed.Bytes()}, programID) require.NoError(t, err) @@ -159,7 +169,7 @@ func TestPDALookups(t *testing.T) { } ctx := context.Background() - result, err := pdaLookup.Resolve(ctx, nil, nil, "") + result, err := pdaLookup.Resolve(ctx, nil, nil, nil) require.NoError(t, err) require.Equal(t, expectedMeta, result) }) @@ -195,19 +205,37 @@ func TestPDALookups(t *testing.T) { "another_seed": seed2, } - result, err := pdaLookup.Resolve(ctx, args, nil, "") + result, err := pdaLookup.Resolve(ctx, args, nil, nil) require.NoError(t, err) require.Equal(t, expectedMeta, result) }) - t.Run("PDALookup resolves valid PDA with address lookup seeds", func(t *testing.T) { - privKey1, err := solana.NewRandomPrivateKey() - require.NoError(t, err) - seed1 := privKey1.PublicKey() + t.Run("PDALookup fails with missing seeds", func(t *testing.T) { + programID := solana.SystemProgramID - privKey2, err := solana.NewRandomPrivateKey() - require.NoError(t, err) - seed2 := privKey2.PublicKey() + pdaLookup := chainwriter.PDALookups{ + Name: "TestPDA", + PublicKey: chainwriter.AccountConstant{Name: "ProgramID", Address: programID.String()}, + Seeds: []chainwriter.Lookup{ + chainwriter.AccountLookup{Name: "seed1", Location: "MissingSeed"}, + }, + IsSigner: false, + IsWritable: true, + } + + ctx := context.Background() + args := map[string]interface{}{ + "test_seed": []byte("data"), + } + + _, err := pdaLookup.Resolve(ctx, args, nil, nil) + require.Error(t, err) + require.Contains(t, err.Error(), "key not found") + }) + + t.Run("PDALookup resolves valid PDA with address lookup seeds", func(t *testing.T) { + seed1 := getRandomPubKey(t) + seed2 := getRandomPubKey(t) pda, _, err := solana.FindProgramAddress([][]byte{seed1.Bytes(), seed2.Bytes()}, programID) require.NoError(t, err) @@ -237,7 +265,7 @@ func TestPDALookups(t *testing.T) { "another_seed": seed2, } - result, err := pdaLookup.Resolve(ctx, args, nil, "") + result, err := pdaLookup.Resolve(ctx, args, nil, nil) require.NoError(t, err) require.Equal(t, expectedMeta, result) }) @@ -245,15 +273,18 @@ func TestPDALookups(t *testing.T) { func TestLookupTables(t *testing.T) { ctx := tests.Context(t) - url := client.SetupLocalSolNode(t) - c := rpc.New(url) sender, err := solana.NewRandomPrivateKey() require.NoError(t, err) - utils.FundAccounts(ctx, []solana.PrivateKey{sender}, c, t) + + url := utils.SetupTestValidatorWithAnchorPrograms(t, utils.PathToAnchorConfig, sender.PublicKey().String()) + rpcClient := rpc.New(url) + + utils.FundAccounts(ctx, []solana.PrivateKey{sender}, rpcClient, t) cfg := config.NewDefault() solanaClient, err := client.NewClient(url, cfg, 5*time.Second, nil) + require.NoError(t, err) loader := commonutils.NewLazyLoad(func() (client.ReaderWriter, error) { return solanaClient, nil }) mkey := keyMocks.NewSimpleKeystore(t) @@ -261,22 +292,22 @@ func TestLookupTables(t *testing.T) { txm := txm.NewTxm("localnet", loader, nil, cfg, mkey, lggr) - chainWriter, err := chainwriter.NewSolanaChainWriterService(solanaClient, *txm, nil, chainwriter.ChainWriterConfig{}) + cw, err := chainwriter.NewSolanaChainWriterService(solanaClient, *txm, nil, chainwriter.ChainWriterConfig{}) t.Run("StaticLookup table resolves properly", func(t *testing.T) { pubKeys := createTestPubKeys(t, 8) - table := CreateTestLookupTable(t, ctx, c, sender, pubKeys) + table := CreateTestLookupTable(ctx, t, rpcClient, sender, pubKeys) lookupConfig := chainwriter.LookupTables{ DerivedLookupTables: nil, StaticLookupTables: []string{table.String()}, } - _, staticTableMap, err := chainWriter.ResolveLookupTables(ctx, nil, lookupConfig, "test-debug-id") + _, staticTableMap, err := cw.ResolveLookupTables(ctx, nil, lookupConfig) require.NoError(t, err) require.Equal(t, pubKeys, staticTableMap[table]) }) - t.Run("Derived lookup table resovles properly with constant address", func(t *testing.T) { + t.Run("Derived lookup table resolves properly with constant address", func(t *testing.T) { pubKeys := createTestPubKeys(t, 8) - table := CreateTestLookupTable(t, ctx, c, sender, pubKeys) + table := CreateTestLookupTable(ctx, t, rpcClient, sender, pubKeys) lookupConfig := chainwriter.LookupTables{ DerivedLookupTables: []chainwriter.DerivedLookupTable{ { @@ -291,7 +322,7 @@ func TestLookupTables(t *testing.T) { }, StaticLookupTables: nil, } - derivedTableMap, _, err := chainWriter.ResolveLookupTables(ctx, nil, lookupConfig, "test-debug-id") + derivedTableMap, _, err := cw.ResolveLookupTables(ctx, nil, lookupConfig) require.NoError(t, err) addresses, ok := derivedTableMap["DerivedTable"][table.String()] @@ -301,9 +332,45 @@ func TestLookupTables(t *testing.T) { } }) + t.Run("Derived lookup table fails with invalid address", func(t *testing.T) { + invalidTable := getRandomPubKey(t) + + lookupConfig := chainwriter.LookupTables{ + DerivedLookupTables: []chainwriter.DerivedLookupTable{ + { + Name: "DerivedTable", + Accounts: chainwriter.AccountConstant{ + Name: "InvalidTable", + Address: invalidTable.String(), + IsSigner: true, + IsWritable: true, + }, + }, + }, + StaticLookupTables: nil, + } + + _, _, err = cw.ResolveLookupTables(ctx, nil, lookupConfig) + require.Error(t, err) + require.Contains(t, err.Error(), "error fetching account info for table") // Example error message + }) + + t.Run("Static lookup table fails with invalid address", func(t *testing.T) { + invalidTable := getRandomPubKey(t) + + lookupConfig := chainwriter.LookupTables{ + DerivedLookupTables: nil, + StaticLookupTables: []string{invalidTable.String()}, + } + + _, _, err = cw.ResolveLookupTables(ctx, nil, lookupConfig) + require.Error(t, err) + require.Contains(t, err.Error(), "error fetching account info for table") // Example error message + }) + t.Run("Derived lookup table resolves properly with account lookup address", func(t *testing.T) { pubKeys := createTestPubKeys(t, 8) - table := CreateTestLookupTable(t, ctx, c, sender, pubKeys) + table := CreateTestLookupTable(ctx, t, rpcClient, sender, pubKeys) lookupConfig := chainwriter.LookupTables{ DerivedLookupTables: []chainwriter.DerivedLookupTable{ { @@ -324,7 +391,7 @@ func TestLookupTables(t *testing.T) { }, } - derivedTableMap, _, err := chainWriter.ResolveLookupTables(ctx, testArgs, lookupConfig, "test-debug-id") + derivedTableMap, _, err := cw.ResolveLookupTables(ctx, testArgs, lookupConfig) require.NoError(t, err) addresses, ok := derivedTableMap["DerivedTable"][table.String()] @@ -333,19 +400,104 @@ func TestLookupTables(t *testing.T) { require.Equal(t, pubKeys[i], address.PublicKey) } }) + + t.Run("Derived lookup table resolves properly with PDALookup address", func(t *testing.T) { + // Deployed write_test contract + programID := solana.MustPublicKeyFromBase58("39vbQVpEMtZtg3e6ZSE7nBSzmNZptmW45WnLkbqEe4TU") + + lookupKeys := createTestPubKeys(t, 5) + lookupTable := CreateTestLookupTable(ctx, t, rpcClient, sender, lookupKeys) + + InitializeDataAccount(ctx, t, rpcClient, programID, sender, lookupTable) + + args := map[string]interface{}{ + "seed1": []byte("data"), + } + + lookupConfig := chainwriter.LookupTables{ + DerivedLookupTables: []chainwriter.DerivedLookupTable{ + { + Name: "DerivedTable", + Accounts: chainwriter.PDALookups{ + Name: "DataAccountPDA", + PublicKey: chainwriter.AccountConstant{Name: "WriteTest", Address: programID.String()}, + Seeds: []chainwriter.Lookup{ + chainwriter.AccountLookup{Name: "seed1", Location: "seed1"}, + }, + IsSigner: false, + IsWritable: false, + InternalField: chainwriter.InternalField{ + Type: reflect.TypeOf(DataAccount{}), + Location: "LookupTable", + }, + }, + }, + }, + StaticLookupTables: nil, + } + + derivedTableMap, _, err := cw.ResolveLookupTables(ctx, args, lookupConfig) + require.NoError(t, err) + + addresses, ok := derivedTableMap["DerivedTable"][lookupTable.String()] + require.True(t, ok) + for i, address := range addresses { + require.Equal(t, lookupKeys[i], address.PublicKey) + } + }) +} + +func InitializeDataAccount( + ctx context.Context, + t *testing.T, + client *rpc.Client, + programID solana.PublicKey, + admin solana.PrivateKey, + lookupTable solana.PublicKey, +) { + pda, _, err := solana.FindProgramAddress([][]byte{[]byte("data")}, programID) + require.NoError(t, err) + + discriminator := getDiscriminator("initialize") + + instructionData := append(discriminator[:], lookupTable.Bytes()...) + + instruction := solana.NewInstruction( + programID, + solana.AccountMetaSlice{ + solana.Meta(pda).WRITE(), + solana.Meta(admin.PublicKey()).SIGNER().WRITE(), + solana.Meta(solana.SystemProgramID), + }, + instructionData, + ) + + // Send and confirm the transaction + utils.SendAndConfirm(ctx, t, client, []solana.Instruction{instruction}, admin, rpc.CommitmentFinalized) +} + +func getDiscriminator(instruction string) [8]byte { + fullHash := sha256.Sum256([]byte("global:" + instruction)) + var discriminator [8]byte + copy(discriminator[:], fullHash[:8]) + return discriminator +} + +func getRandomPubKey(t *testing.T) solana.PublicKey { + privKey, err := solana.NewRandomPrivateKey() + require.NoError(t, err) + return privKey.PublicKey() } func createTestPubKeys(t *testing.T, num int) solana.PublicKeySlice { addresses := make([]solana.PublicKey, num) for i := 0; i < num; i++ { - privKey, err := solana.NewRandomPrivateKey() - require.NoError(t, err) - addresses[i] = privKey.PublicKey() + addresses[i] = getRandomPubKey(t) } return addresses } -func CreateTestLookupTable(t *testing.T, ctx context.Context, c *rpc.Client, sender solana.PrivateKey, addresses []solana.PublicKey) solana.PublicKey { +func CreateTestLookupTable(ctx context.Context, t *testing.T, c *rpc.Client, sender solana.PrivateKey, addresses []solana.PublicKey) solana.PublicKey { // Create lookup tables slot, serr := c.GetSlot(ctx, rpc.CommitmentFinalized) require.NoError(t, serr) diff --git a/pkg/solana/client/test_helpers.go b/pkg/solana/client/test_helpers.go index 5bb8b1cde..8d5ab4f88 100644 --- a/pkg/solana/client/test_helpers.go +++ b/pkg/solana/client/test_helpers.go @@ -66,6 +66,7 @@ func SetupLocalSolNodeWithFlags(t *testing.T, flags ...string) (string, string) out, err := client.GetHealth(tests.Context(t)) if err != nil || out != rpc.HealthOk { t.Logf("API server not ready yet (attempt %d)\n", i+1) + t.Logf("Error from API server: %v\n", err) continue } ready = true diff --git a/pkg/solana/utils/utils.go b/pkg/solana/utils/utils.go index 3ce1f788e..0c772065b 100644 --- a/pkg/solana/utils/utils.go +++ b/pkg/solana/utils/utils.go @@ -4,15 +4,29 @@ import ( "context" "encoding/binary" "fmt" + "os" + "path/filepath" + "runtime" "testing" "time" "github.com/gagliardetto/solana-go" "github.com/gagliardetto/solana-go/rpc" + "github.com/pelletier/go-toml/v2" + "github.com/smartcontractkit/chainlink-solana/pkg/solana/client" "github.com/smartcontractkit/chainlink-solana/pkg/solana/internal" "github.com/test-go/testify/require" ) +var ( + _, b, _, _ = runtime.Caller(0) + // ProjectRoot Root folder of this project + ProjectRoot = filepath.Join(filepath.Dir(b), "/../../..") + // ContractsDir path to our contracts + ContractsDir = filepath.Join(ProjectRoot, "contracts", "target", "deploy") + PathToAnchorConfig = filepath.Join(ProjectRoot, "contracts", "Anchor.toml") +) + func LamportsToSol(lamports uint64) float64 { return internal.LamportsToSol(lamports) } // TxModifier is a dynamic function used to flexibly add components to a transaction such as additional signers, and compute budget parameters @@ -60,7 +74,7 @@ func sendTransaction(ctx context.Context, rpcClient *rpc.Client, t *testing.T, i var txStatus rpc.ConfirmationStatusType count := 0 - for txStatus != rpc.ConfirmationStatusConfirmed && txStatus != rpc.ConfirmationStatusFinalized { + for txStatus != rpc.ConfirmationStatusType(commitment) && txStatus != rpc.ConfirmationStatusFinalized { count++ statusRes, sigErr := rpcClient.GetSignatureStatuses(ctx, true, txsig) require.NoError(t, sigErr) @@ -68,7 +82,7 @@ func sendTransaction(ctx context.Context, rpcClient *rpc.Client, t *testing.T, i txStatus = statusRes.Value[0].ConfirmationStatus } time.Sleep(100 * time.Millisecond) - if count > 50 { + if count > 500 { require.NoError(t, fmt.Errorf("unable to find transaction within timeout")) } } @@ -176,3 +190,27 @@ func FundAccounts(ctx context.Context, accounts []solana.PrivateKey, solanaGoCli } } } + +func DeployAllPrograms(t *testing.T, pathToAnchorConfig string, admin solana.PrivateKey) *rpc.Client { + return rpc.New(SetupTestValidatorWithAnchorPrograms(t, pathToAnchorConfig, admin.PublicKey().String())) +} + +func SetupTestValidatorWithAnchorPrograms(t *testing.T, pathToAnchorConfig string, upgradeAuthority string) string { + anchorData := struct { + Programs struct { + Localnet map[string]string + } + }{} + + // upload programs to validator + anchorBytes, err := os.ReadFile(pathToAnchorConfig) + require.NoError(t, err) + require.NoError(t, toml.Unmarshal(anchorBytes, &anchorData)) + + flags := []string{} + for k, v := range anchorData.Programs.Localnet { + flags = append(flags, "--upgradeable-program", v, filepath.Join(ContractsDir, k+".so"), upgradeAuthority) + } + url, _ := client.SetupLocalSolNodeWithFlags(t, flags...) + return url +}