From 4213d7c3b4aad3f4a15faafd0e3a1e140d473400 Mon Sep 17 00:00:00 2001 From: Silas Lenihan Date: Fri, 11 Oct 2024 10:18:21 -0500 Subject: [PATCH 01/29] 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 a75c03898182f9d1ee3ec1f4c6763153481fced9 Mon Sep 17 00:00:00 2001 From: Silas Lenihan Date: Fri, 25 Oct 2024 15:09:13 -0400 Subject: [PATCH 02/29] 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 257a465afc2bbd029f800c88317ee48189159619 Mon Sep 17 00:00:00 2001 From: Silas Lenihan Date: Mon, 28 Oct 2024 14:11:07 -0400 Subject: [PATCH 03/29] 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 a251834ef41e2fb81690e906ed3b8d00611083a0 Mon Sep 17 00:00:00 2001 From: Silas Lenihan Date: Tue, 29 Oct 2024 15:09:05 -0400 Subject: [PATCH 04/29] 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 40cbaedd2f3b38dd3234ac0c01815c02264d1139 Mon Sep 17 00:00:00 2001 From: Silas Lenihan Date: Wed, 30 Oct 2024 11:11:33 -0400 Subject: [PATCH 05/29] 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 2cde8f8bd0663029ef3a1e14c63f259452f65481 Mon Sep 17 00:00:00 2001 From: Silas Lenihan Date: Wed, 30 Oct 2024 11:12:57 -0400 Subject: [PATCH 06/29] 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 ae873ddddc997b8fb3c6227637b1ccd92ca2989a Mon Sep 17 00:00:00 2001 From: pablolagreca Date: Wed, 30 Oct 2024 14:22:17 -0300 Subject: [PATCH 07/29] 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 28875370304696b168d4341c027effe85efc28fe Mon Sep 17 00:00:00 2001 From: Silas Lenihan Date: Wed, 30 Oct 2024 14:31:44 -0400 Subject: [PATCH 08/29] 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 c3b094fe9127ca5632da0d51cc44a53d45872e57 Mon Sep 17 00:00:00 2001 From: Silas Lenihan Date: Mon, 4 Nov 2024 11:24:36 -0500 Subject: [PATCH 09/29] 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 d6eb8015d719b01f7531fe2f8f856d1066deab80 Mon Sep 17 00:00:00 2001 From: Silas Lenihan Date: Mon, 4 Nov 2024 11:48:07 -0500 Subject: [PATCH 10/29] 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 9e56357f8d280efa9274ba7945d05447e3177274 Mon Sep 17 00:00:00 2001 From: Silas Lenihan Date: Mon, 4 Nov 2024 13:03:37 -0500 Subject: [PATCH 11/29] 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 c1c3c284fe9ad61fcebe4d1583d5e27e25aeb643 Mon Sep 17 00:00:00 2001 From: Silas Lenihan Date: Wed, 6 Nov 2024 15:16:59 -0500 Subject: [PATCH 12/29] 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 ca98a6838cff09a74065706ef8b9d08ed0ddf55f Mon Sep 17 00:00:00 2001 From: Silas Lenihan Date: Fri, 15 Nov 2024 14:08:56 -0500 Subject: [PATCH 13/29] 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 018242f6a5a90ed259f31f5447405a05513282e8 Mon Sep 17 00:00:00 2001 From: Silas Lenihan Date: Mon, 18 Nov 2024 11:16:04 -0500 Subject: [PATCH 14/29] 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 908621501609653e08d49b01f21667a7d3fda37e Mon Sep 17 00:00:00 2001 From: Silas Lenihan Date: Tue, 19 Nov 2024 14:57:20 -0500 Subject: [PATCH 15/29] 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 872645214a8e920deeefcf08c6e99378b59deb0c Mon Sep 17 00:00:00 2001 From: Silas Lenihan Date: Wed, 20 Nov 2024 10:35:39 -0500 Subject: [PATCH 16/29] 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 208487185f7b6ac74022ec369e48cb6fa0c99582 Mon Sep 17 00:00:00 2001 From: Silas Lenihan Date: Thu, 21 Nov 2024 09:48:39 -0500 Subject: [PATCH 17/29] 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 6b58adcfe..70a31342a 100644 --- a/go.mod +++ b/go.mod @@ -20,6 +20,7 @@ require ( github.com/smartcontractkit/chainlink-common v0.4.1-0.20241223143929-db7919d60550 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.10.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 eb78592340966f7f3cc0aec46d68741dec87c105 Mon Sep 17 00:00:00 2001 From: Silas Lenihan Date: Thu, 21 Nov 2024 16:45:54 -0500 Subject: [PATCH 18/29] 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 8a44f4f2efd419c3afc83f6c15167dc14ab2e514 Mon Sep 17 00:00:00 2001 From: Silas Lenihan Date: Mon, 25 Nov 2024 11:49:10 -0500 Subject: [PATCH 19/29] 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 593c9f7c1ba762c3849ab00dfa1bef7c02e51c20 Mon Sep 17 00:00:00 2001 From: Silas Lenihan Date: Mon, 25 Nov 2024 11:51:15 -0500 Subject: [PATCH 20/29] 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 c82c48e3a2ce62d9156ffed732ec1c0982495bd5 Mon Sep 17 00:00:00 2001 From: Silas Lenihan Date: Mon, 25 Nov 2024 12:06:25 -0500 Subject: [PATCH 21/29] 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 7914967a5f9ad27a0d30b2c0ba697ca821c22b2d Mon Sep 17 00:00:00 2001 From: Silas Lenihan Date: Tue, 26 Nov 2024 14:49:08 -0500 Subject: [PATCH 22/29] Completed chained lookup integration test --- contracts/Anchor.toml | 3 +- 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, 473 insertions(+), 128 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 30b788caa..7d18084b0 100644 --- a/contracts/Anchor.toml +++ b/contracts/Anchor.toml @@ -30,4 +30,5 @@ access_controller = "9xi644bRR8birboDGdTiwBq3C7VEeR7VuamRYYXCubUW" contract-reader-interface = "6AfuXF6HapDUhQfE4nQG9C1SGtA1YjP3icaJyRfU4RyE" 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 0209f1dbb..ab6454e91 100644 --- a/contracts/Cargo.lock +++ b/contracts/Cargo.lock @@ -2680,6 +2680,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 +} From ff65ec2356f597cc4443dadb172f6def803e74c0 Mon Sep 17 00:00:00 2001 From: amit-momin <108959691+amit-momin@users.noreply.github.com> Date: Mon, 9 Dec 2024 14:00:59 -0600 Subject: [PATCH 23/29] ChainWriter unit tests (#948) * Added ChainWriter unit tests for GetFeeComponents and GetTransactionStatus * Created SubmitTransaction tests * Created SubmitTransaction tests * Moved txm utils into own package and generated txm mock * Updated chain writer tests to use txm mock * Added GetAddresses unit test and fixed SubmitTransaction unit test * Fixed linting and removed file read for IDL * Fixed filter lookup table error case and fixed linting * Added filter lookup table addresses unit tests * Added new test case and fixed formatting issues * Addressed golang lint suggestions * Cleaned out unused dependency and fixed remaining golang lint errors * Added derived lookup table indeces unit tests --------- Co-authored-by: Silas Lenihan --- .mockery.yaml | 1 + .../localnet/write_test-keypair.json | 7 +- contracts/programs/write_test/src/lib.rs | 3 +- go.mod | 1 - pkg/solana/chain.go | 9 +- pkg/solana/chainwriter/ccip_example_config.go | 6 +- pkg/solana/chainwriter/chain_writer.go | 56 +- pkg/solana/chainwriter/chain_writer_test.go | 690 ++++++++++++++++++ pkg/solana/chainwriter/helpers.go | 86 ++- pkg/solana/chainwriter/lookups.go | 16 +- pkg/solana/chainwriter/lookups_test.go | 140 +--- pkg/solana/relay.go | 1 + pkg/solana/transmitter_test.go | 2 +- pkg/solana/txm/mocks/tx_manager.go | 390 ++++++++++ pkg/solana/txm/pendingtx.go | 36 +- pkg/solana/txm/pendingtx_test.go | 66 +- pkg/solana/txm/txm.go | 40 +- pkg/solana/txm/txm_internal_test.go | 1 + pkg/solana/txm/{ => utils}/utils.go | 31 +- pkg/solana/txm/{ => utils}/utils_test.go | 4 +- pkg/solana/utils/utils.go | 3 +- pkg/solana/utils/utils_test.go | 3 +- 22 files changed, 1350 insertions(+), 242 deletions(-) create mode 100644 pkg/solana/chainwriter/chain_writer_test.go create mode 100644 pkg/solana/txm/mocks/tx_manager.go rename pkg/solana/txm/{ => utils}/utils.go (82%) rename pkg/solana/txm/{ => utils}/utils_test.go (98%) diff --git a/.mockery.yaml b/.mockery.yaml index 1df96bfec..347d69c58 100644 --- a/.mockery.yaml +++ b/.mockery.yaml @@ -37,6 +37,7 @@ packages: config: filename: simple_keystore.go case: underscore + TxManager: github.com/smartcontractkit/chainlink-solana/pkg/solana/logpoller: interfaces: RPCClient: diff --git a/contracts/artifacts/localnet/write_test-keypair.json b/contracts/artifacts/localnet/write_test-keypair.json index c4e6e125c..dfb18e9c4 100644 --- a/contracts/artifacts/localnet/write_test-keypair.json +++ b/contracts/artifacts/localnet/write_test-keypair.json @@ -1 +1,6 @@ -[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 +[ + 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 +] diff --git a/contracts/programs/write_test/src/lib.rs b/contracts/programs/write_test/src/lib.rs index 4078bca4d..8d8fa3cac 100644 --- a/contracts/programs/write_test/src/lib.rs +++ b/contracts/programs/write_test/src/lib.rs @@ -12,10 +12,9 @@ pub mod write_test { data.administrator = ctx.accounts.admin.key(); data.pending_administrator = Pubkey::default(); data.lookup_table = lookup_table; - + Ok(()) } - } #[derive(Accounts)] diff --git a/go.mod b/go.mod index 70a31342a..6b58adcfe 100644 --- a/go.mod +++ b/go.mod @@ -20,7 +20,6 @@ require ( github.com/smartcontractkit/chainlink-common v0.4.1-0.20241223143929-db7919d60550 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.10.0 diff --git a/pkg/solana/chain.go b/pkg/solana/chain.go index 5ed5eb8cb..722f99bf5 100644 --- a/pkg/solana/chain.go +++ b/pkg/solana/chain.go @@ -30,6 +30,7 @@ import ( "github.com/smartcontractkit/chainlink-solana/pkg/solana/internal" "github.com/smartcontractkit/chainlink-solana/pkg/solana/monitor" "github.com/smartcontractkit/chainlink-solana/pkg/solana/txm" + txmutils "github.com/smartcontractkit/chainlink-solana/pkg/solana/txm/utils" ) type Chain interface { @@ -578,10 +579,10 @@ func (c *chain) sendTx(ctx context.Context, from, to string, amount *big.Int, ba err = chainTxm.Enqueue(ctx, "", tx, nil, blockhash.Value.LastValidBlockHeight, txm.SetComputeUnitLimit(500), // reduce from default 200K limit - should only take 450 compute units // no fee bumping and no additional fee - makes validating balance accurate - txm.SetComputeUnitPriceMax(0), - txm.SetComputeUnitPriceMin(0), - txm.SetBaseComputeUnitPrice(0), - txm.SetFeeBumpPeriod(0), + txmutils.SetComputeUnitPriceMax(0), + txmutils.SetComputeUnitPriceMin(0), + txmutils.SetBaseComputeUnitPrice(0), + txmutils.SetFeeBumpPeriod(0), ) if err != nil { return fmt.Errorf("transaction failed: %w", err) diff --git a/pkg/solana/chainwriter/ccip_example_config.go b/pkg/solana/chainwriter/ccip_example_config.go index bd5087af8..89038fd6a 100644 --- a/pkg/solana/chainwriter/ccip_example_config.go +++ b/pkg/solana/chainwriter/ccip_example_config.go @@ -79,7 +79,7 @@ func TestConfig() { // 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 + // -> input parameters // -> constant // PDALookups can resolve to multiple addresses if: // A) The PublicKey lookup resolves to multiple addresses (i.e. multiple token addresses) @@ -102,8 +102,8 @@ func TestConfig() { }, // Lookup Table content - Get the accounts from the derived lookup table above AccountsFromLookupTable{ - LookupTablesName: "RegistryTokenState", - IncludeIndexes: []int{}, // If left empty, all addresses will be included. Otherwise, only the specified indexes will be included. + LookupTableName: "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. diff --git a/pkg/solana/chainwriter/chain_writer.go b/pkg/solana/chainwriter/chain_writer.go index 608f3c610..4fcc5caa0 100644 --- a/pkg/solana/chainwriter/chain_writer.go +++ b/pkg/solana/chainwriter/chain_writer.go @@ -21,12 +21,13 @@ import ( type SolanaChainWriterService struct { reader client.Reader - txm txm.Txm + txm txm.TxManager ge fees.Estimator config ChainWriterConfig codecs map[string]types.Codec } +// nolint // ignoring naming suggestion type ChainWriterConfig struct { Programs map[string]ProgramConfig } @@ -46,7 +47,7 @@ type MethodConfig struct { DebugIDLocation string } -func NewSolanaChainWriterService(reader client.Reader, txm txm.Txm, ge fees.Estimator, config ChainWriterConfig) (*SolanaChainWriterService, error) { +func NewSolanaChainWriterService(reader client.Reader, txm txm.TxManager, 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) @@ -68,7 +69,7 @@ func parseIDLCodecs(config ChainWriterConfig) (map[string]types.Codec, error) { 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()) + idlCodec, err := codec.NewIDLInstructionsCodec(idl, binary.LittleEndian()) if err != nil { return nil, fmt.Errorf("failed to create codec from IDL: %w", err) } @@ -79,7 +80,7 @@ func parseIDLCodecs(config ChainWriterConfig) (map[string]types.Codec, error) { 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) + idlCodec, err = codec.NewNamedModifierCodec(idlCodec, method, modConfig) if err != nil { return nil, fmt.Errorf("failed to create named codec: %w", err) } @@ -90,14 +91,6 @@ func parseIDLCodecs(config ChainWriterConfig) (map[string]types.Codec, error) { 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) -} - /* 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. @@ -161,7 +154,7 @@ func (s *SolanaChainWriterService) FilterLookupTableAddresses( for innerIdentifier, metas := range innerMap { tableKey, err := solana.PublicKeyFromBase58(innerIdentifier) if err != nil { - fmt.Errorf("error parsing lookup table key: %w", err) + continue } // Collect public keys that are actually used @@ -198,18 +191,31 @@ func (s *SolanaChainWriterService) FilterLookupTableAddresses( } 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] + programConfig, exists := s.config.Programs[contractName] + if !exists { + return fmt.Errorf("failed to find program config for contract name: %s", contractName) + } + methodConfig, exists := programConfig.Methods[method] + if !exists { + return fmt.Errorf("failed to find method config for method: %s", method) + } // Configure debug ID debugID := "" if methodConfig.DebugIDLocation != "" { - debugID, err := GetDebugIDAtLocation(args, methodConfig.DebugIDLocation) + var err error + debugID, err = GetDebugIDAtLocation(args, methodConfig.DebugIDLocation) if err != nil { return errorWithDebugID(fmt.Errorf("error getting debug ID from input args: %w", err), debugID) } } + codec := s.codecs[contractName] + encodedPayload, err := codec.Encode(ctx, args, method) + if err != nil { + return errorWithDebugID(fmt.Errorf("error encoding transaction payload: %w", err), debugID) + } + // Fetch derived and static table maps derivedTableMap, staticTableMap, err := s.ResolveLookupTables(ctx, args, methodConfig.LookupTables) if err != nil { @@ -232,7 +238,7 @@ func (s *SolanaChainWriterService) SubmitTransaction(ctx context.Context, contra } // Prepare transaction - programId, err := solana.PublicKeyFromBase58(contractName) + programID, err := solana.PublicKeyFromBase58(contractName) if err != nil { return errorWithDebugID(fmt.Errorf("error parsing program ID: %w", err), debugID) } @@ -242,15 +248,9 @@ func (s *SolanaChainWriterService) SubmitTransaction(ctx context.Context, contra return errorWithDebugID(fmt.Errorf("error parsing fee payer address: %w", err), debugID) } - 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) - } - tx, err := solana.NewTransaction( []solana.Instruction{ - solana.NewInstruction(programId, accounts, encodedPayload), + solana.NewInstruction(programID, accounts, encodedPayload), }, blockhash.Value.Blockhash, solana.TransactionPayer(feePayer), @@ -269,13 +269,13 @@ func (s *SolanaChainWriterService) SubmitTransaction(ctx context.Context, contra } var ( - _ services.Service = &SolanaChainWriterService{} - _ types.ChainWriter = &SolanaChainWriterService{} + _ services.Service = &SolanaChainWriterService{} + _ types.ContractWriter = &SolanaChainWriterService{} ) // 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 + return s.txm.GetTransactionStatus(ctx, transactionID) } // GetFeeComponents retrieves the associated gas costs for executing a transaction. @@ -286,7 +286,7 @@ func (s *SolanaChainWriterService) GetFeeComponents(ctx context.Context) (*types fee := s.ge.BaseComputeUnitPrice() return &types.ChainFeeComponents{ - ExecutionFee: big.NewInt(int64(fee)), + ExecutionFee: new(big.Int).SetUint64(fee), DataAvailabilityFee: nil, }, 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..d931fb6d8 --- /dev/null +++ b/pkg/solana/chainwriter/chain_writer_test.go @@ -0,0 +1,690 @@ +package chainwriter_test + +import ( + "bytes" + "errors" + "math/big" + "reflect" + "testing" + + 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" + "github.com/google/uuid" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + + "github.com/smartcontractkit/chainlink-common/pkg/types" + "github.com/smartcontractkit/chainlink-common/pkg/utils/tests" + + "github.com/smartcontractkit/chainlink-solana/pkg/solana/chainwriter" + clientmocks "github.com/smartcontractkit/chainlink-solana/pkg/solana/client/mocks" + feemocks "github.com/smartcontractkit/chainlink-solana/pkg/solana/fees/mocks" + txmMocks "github.com/smartcontractkit/chainlink-solana/pkg/solana/txm/mocks" +) + +var writeTestIdlJSON = `{"version": "0.1.0","name": "write_test","instructions": [{"name": "initialize","accounts": [{"name": "dataAccount","isMut": true,"isSigner": false,"docs": ["PDA account, derived from seeds and created by the System Program in this instruction"]},{"name": "admin","isMut": true,"isSigner": true,"docs": ["Admin account that pays for PDA creation and signs the transaction"]},{"name": "systemProgram","isMut": false,"isSigner": false,"docs": ["System Program is required for PDA creation"]}],"args": [{"name": "lookupTable","type": "publicKey"}]}],"accounts": [{"name": "DataAccount","type": {"kind": "struct","fields": [{"name": "version","type": "u8"},{"name": "administrator","type": "publicKey"},{"name": "pendingAdministrator","type": "publicKey"},{"name": "lookupTable","type": "publicKey"}]}}]}` + +func TestChainWriter_GetAddresses(t *testing.T) { + ctx := tests.Context(t) + + // mock client + rw := clientmocks.NewReaderWriter(t) + // mock estimator + ge := feemocks.NewEstimator(t) + // mock txm + txm := txmMocks.NewTxManager(t) + + // initialize chain writer + cw, err := chainwriter.NewSolanaChainWriterService(rw, txm, ge, chainwriter.ChainWriterConfig{}) + require.NoError(t, err) + + // expected account meta for constant account + constantAccountMeta := &solana.AccountMeta{ + IsSigner: true, + IsWritable: true, + } + + // expected account meta for account lookup + accountLookupMeta := &solana.AccountMeta{ + IsSigner: true, + IsWritable: false, + } + + // setup pda account address + seed1 := []byte("seed1") + pda1 := mustFindPdaProgramAddress(t, [][]byte{seed1}, solana.SystemProgramID) + // expected account meta for pda lookup + pdaLookupMeta := &solana.AccountMeta{ + PublicKey: pda1, + IsSigner: false, + IsWritable: false, + } + + // setup pda account with inner field lookup + programID := chainwriter.GetRandomPubKey(t) + seed2 := []byte("seed2") + pda2 := mustFindPdaProgramAddress(t, [][]byte{seed2}, programID) + // mock data account response from program + lookupTablePubkey := mockDataAccountLookupTable(t, rw, pda2) + // mock fetch lookup table addresses call + storedPubKeys := chainwriter.CreateTestPubKeys(t, 3) + mockFetchLookupTableAddresses(t, rw, lookupTablePubkey, storedPubKeys) + // expected account meta for derived table lookup + derivedTablePdaLookupMeta := &solana.AccountMeta{ + IsSigner: false, + IsWritable: true, + } + + lookupTableConfig := chainwriter.LookupTables{ + DerivedLookupTables: []chainwriter.DerivedLookupTable{ + { + Name: "DerivedTable", + Accounts: chainwriter.PDALookups{ + Name: "DataAccountPDA", + PublicKey: chainwriter.AccountConstant{Name: "WriteTest", Address: programID.String()}, + Seeds: []chainwriter.Lookup{ + // extract seed2 for PDA lookup + chainwriter.AccountLookup{Name: "seed2", Location: "seed2"}, + }, + IsSigner: derivedTablePdaLookupMeta.IsSigner, + IsWritable: derivedTablePdaLookupMeta.IsWritable, + InternalField: chainwriter.InternalField{ + Type: reflect.TypeOf(DataAccount{}), + Location: "LookupTable", + }, + }, + }, + }, + StaticLookupTables: nil, + } + + t.Run("resolve addresses from different types of lookups", func(t *testing.T) { + constantAccountMeta.PublicKey = chainwriter.GetRandomPubKey(t) + accountLookupMeta.PublicKey = chainwriter.GetRandomPubKey(t) + // correlates to DerivedTable index in account lookup config + derivedTablePdaLookupMeta.PublicKey = storedPubKeys[0] + + args := map[string]interface{}{ + "lookup_table": accountLookupMeta.PublicKey.Bytes(), + "seed1": seed1, + "seed2": seed2, + } + + accountLookupConfig := []chainwriter.Lookup{ + chainwriter.AccountConstant{ + Name: "Constant", + Address: constantAccountMeta.PublicKey.String(), + IsSigner: constantAccountMeta.IsSigner, + IsWritable: constantAccountMeta.IsWritable, + }, + chainwriter.AccountLookup{ + Name: "LookupTable", + Location: "lookup_table", + IsSigner: accountLookupMeta.IsSigner, + IsWritable: accountLookupMeta.IsWritable, + }, + chainwriter.PDALookups{ + Name: "DataAccountPDA", + PublicKey: chainwriter.AccountConstant{Name: "WriteTest", Address: solana.SystemProgramID.String()}, + Seeds: []chainwriter.Lookup{ + // extract seed1 for PDA lookup + chainwriter.AccountLookup{Name: "seed1", Location: "seed1"}, + }, + IsSigner: pdaLookupMeta.IsSigner, + IsWritable: pdaLookupMeta.IsWritable, + // Just get the address of the account, nothing internal. + InternalField: chainwriter.InternalField{}, + }, + chainwriter.AccountsFromLookupTable{ + LookupTableName: "DerivedTable", + IncludeIndexes: []int{0}, + }, + } + + // Fetch derived table map + derivedTableMap, _, err := cw.ResolveLookupTables(ctx, args, lookupTableConfig) + require.NoError(t, err) + + // Resolve account metas + accounts, err := chainwriter.GetAddresses(ctx, args, accountLookupConfig, derivedTableMap, rw) + require.NoError(t, err) + + // account metas should be returned in the same order as the provided account lookup configs + require.Len(t, accounts, 4) + + // Validate account constant + require.Equal(t, constantAccountMeta.PublicKey, accounts[0].PublicKey) + require.Equal(t, constantAccountMeta.IsSigner, accounts[0].IsSigner) + require.Equal(t, constantAccountMeta.IsWritable, accounts[0].IsWritable) + + // Validate account lookup + require.Equal(t, accountLookupMeta.PublicKey, accounts[1].PublicKey) + require.Equal(t, accountLookupMeta.IsSigner, accounts[1].IsSigner) + require.Equal(t, accountLookupMeta.IsWritable, accounts[1].IsWritable) + + // Validate pda lookup + require.Equal(t, pdaLookupMeta.PublicKey, accounts[2].PublicKey) + require.Equal(t, pdaLookupMeta.IsSigner, accounts[2].IsSigner) + require.Equal(t, pdaLookupMeta.IsWritable, accounts[2].IsWritable) + + // Validate pda lookup with inner field from derived table + require.Equal(t, derivedTablePdaLookupMeta.PublicKey, accounts[3].PublicKey) + require.Equal(t, derivedTablePdaLookupMeta.IsSigner, accounts[3].IsSigner) + require.Equal(t, derivedTablePdaLookupMeta.IsWritable, accounts[3].IsWritable) + }) + + t.Run("resolve addresses for multiple indices from derived lookup table", func(t *testing.T) { + args := map[string]interface{}{ + "seed2": seed2, + } + + accountLookupConfig := []chainwriter.Lookup{ + chainwriter.AccountsFromLookupTable{ + LookupTableName: "DerivedTable", + IncludeIndexes: []int{0, 2}, + }, + } + + // Fetch derived table map + derivedTableMap, _, err := cw.ResolveLookupTables(ctx, args, lookupTableConfig) + require.NoError(t, err) + + // Resolve account metas + accounts, err := chainwriter.GetAddresses(ctx, args, accountLookupConfig, derivedTableMap, rw) + require.NoError(t, err) + + require.Len(t, accounts, 2) + require.Equal(t, storedPubKeys[0], accounts[0].PublicKey) + require.Equal(t, storedPubKeys[2], accounts[1].PublicKey) + }) + + t.Run("resolve all addresses from derived lookup table if indices not specified", func(t *testing.T) { + args := map[string]interface{}{ + "seed2": seed2, + } + + accountLookupConfig := []chainwriter.Lookup{ + chainwriter.AccountsFromLookupTable{ + LookupTableName: "DerivedTable", + }, + } + + // Fetch derived table map + derivedTableMap, _, err := cw.ResolveLookupTables(ctx, args, lookupTableConfig) + require.NoError(t, err) + + // Resolve account metas + accounts, err := chainwriter.GetAddresses(ctx, args, accountLookupConfig, derivedTableMap, rw) + require.NoError(t, err) + + require.Len(t, accounts, 3) + for i, storedPubkey := range storedPubKeys { + require.Equal(t, storedPubkey, accounts[i].PublicKey) + } + }) +} + +func TestChainWriter_FilterLookupTableAddresses(t *testing.T) { + ctx := tests.Context(t) + + // mock client + rw := clientmocks.NewReaderWriter(t) + // mock estimator + ge := feemocks.NewEstimator(t) + // mock txm + txm := txmMocks.NewTxManager(t) + + // initialize chain writer + cw, err := chainwriter.NewSolanaChainWriterService(rw, txm, ge, chainwriter.ChainWriterConfig{}) + require.NoError(t, err) + + programID := chainwriter.GetRandomPubKey(t) + seed1 := []byte("seed1") + pda1 := mustFindPdaProgramAddress(t, [][]byte{seed1}, programID) + // mock data account response from program + lookupTablePubkey := mockDataAccountLookupTable(t, rw, pda1) + // mock fetch lookup table addresses call + storedPubKey := chainwriter.GetRandomPubKey(t) + mockFetchLookupTableAddresses(t, rw, lookupTablePubkey, []solana.PublicKey{storedPubKey}) + + unusedProgramID := chainwriter.GetRandomPubKey(t) + seed2 := []byte("seed2") + unusedPda := mustFindPdaProgramAddress(t, [][]byte{seed2}, unusedProgramID) + // mock data account response from program + unusedLookupTable := mockDataAccountLookupTable(t, rw, unusedPda) + // mock fetch lookup table addresses call + unusedKeys := chainwriter.GetRandomPubKey(t) + mockFetchLookupTableAddresses(t, rw, unusedLookupTable, []solana.PublicKey{unusedKeys}) + + // mock static lookup table calls + staticLookupTablePubkey1 := chainwriter.GetRandomPubKey(t) + mockFetchLookupTableAddresses(t, rw, staticLookupTablePubkey1, chainwriter.CreateTestPubKeys(t, 2)) + staticLookupTablePubkey2 := chainwriter.GetRandomPubKey(t) + mockFetchLookupTableAddresses(t, rw, staticLookupTablePubkey2, chainwriter.CreateTestPubKeys(t, 2)) + + lookupTableConfig := chainwriter.LookupTables{ + DerivedLookupTables: []chainwriter.DerivedLookupTable{ + { + Name: "DerivedTable", + Accounts: chainwriter.PDALookups{ + Name: "DataAccountPDA", + PublicKey: chainwriter.AccountConstant{Name: "WriteTest", Address: programID.String()}, + Seeds: []chainwriter.Lookup{ + // extract seed2 for PDA lookup + chainwriter.AccountLookup{Name: "seed1", Location: "seed1"}, + }, + IsSigner: true, + IsWritable: true, + InternalField: chainwriter.InternalField{ + Type: reflect.TypeOf(DataAccount{}), + Location: "LookupTable", + }, + }, + }, + { + Name: "MiscDerivedTable", + Accounts: chainwriter.PDALookups{ + Name: "MiscPDA", + PublicKey: chainwriter.AccountConstant{Name: "UnusedAccount", Address: unusedProgramID.String()}, + Seeds: []chainwriter.Lookup{ + // extract seed2 for PDA lookup + chainwriter.AccountLookup{Name: "seed2", Location: "seed2"}, + }, + IsSigner: true, + IsWritable: true, + InternalField: chainwriter.InternalField{ + Type: reflect.TypeOf(DataAccount{}), + Location: "LookupTable", + }, + }, + }, + }, + StaticLookupTables: []string{staticLookupTablePubkey1.String(), staticLookupTablePubkey2.String()}, + } + + args := map[string]interface{}{ + "seed1": seed1, + "seed2": seed2, + } + + t.Run("returns filtered map with only relevant addresses required by account lookup config", func(t *testing.T) { + accountLookupConfig := []chainwriter.Lookup{ + chainwriter.AccountsFromLookupTable{ + LookupTableName: "DerivedTable", + IncludeIndexes: []int{0}, + }, + } + + // Fetch derived table map + derivedTableMap, staticTableMap, err := cw.ResolveLookupTables(ctx, args, lookupTableConfig) + require.NoError(t, err) + + // Resolve account metas + accounts, err := chainwriter.GetAddresses(ctx, args, accountLookupConfig, derivedTableMap, rw) + require.NoError(t, err) + + // Filter the lookup table addresses based on which accounts are actually used + filteredLookupTableMap := cw.FilterLookupTableAddresses(accounts, derivedTableMap, staticTableMap) + + // Filter map should only contain the address for the DerivedTable lookup defined in the account lookup config + require.Len(t, filteredLookupTableMap, len(accounts)) + entry, exists := filteredLookupTableMap[lookupTablePubkey] + require.True(t, exists) + require.Len(t, entry, 1) + require.Equal(t, storedPubKey, entry[0]) + }) + + t.Run("returns empty map if empty account lookup config provided", func(t *testing.T) { + accountLookupConfig := []chainwriter.Lookup{} + + // Fetch derived table map + derivedTableMap, staticTableMap, err := cw.ResolveLookupTables(ctx, args, lookupTableConfig) + require.NoError(t, err) + + // Resolve account metas + accounts, err := chainwriter.GetAddresses(ctx, args, accountLookupConfig, derivedTableMap, rw) + require.NoError(t, err) + + // Filter the lookup table addresses based on which accounts are actually used + filteredLookupTableMap := cw.FilterLookupTableAddresses(accounts, derivedTableMap, staticTableMap) + require.Empty(t, filteredLookupTableMap) + }) + + t.Run("returns empty map if only constant account lookup required", func(t *testing.T) { + accountLookupConfig := []chainwriter.Lookup{ + chainwriter.AccountConstant{ + Name: "Constant", + Address: chainwriter.GetRandomPubKey(t).String(), + IsSigner: false, + IsWritable: false, + }, + } + + // Fetch derived table map + derivedTableMap, staticTableMap, err := cw.ResolveLookupTables(ctx, args, lookupTableConfig) + require.NoError(t, err) + + // Resolve account metas + accounts, err := chainwriter.GetAddresses(ctx, args, accountLookupConfig, derivedTableMap, rw) + require.NoError(t, err) + + // Filter the lookup table addresses based on which accounts are actually used + filteredLookupTableMap := cw.FilterLookupTableAddresses(accounts, derivedTableMap, staticTableMap) + require.Empty(t, filteredLookupTableMap) + }) +} + +func TestChainWriter_SubmitTransaction(t *testing.T) { + t.Parallel() + + ctx := tests.Context(t) + // mock client + rw := clientmocks.NewReaderWriter(t) + // mock estimator + ge := feemocks.NewEstimator(t) + // mock txm + txm := txmMocks.NewTxManager(t) + + // setup admin key + adminPk, err := solana.NewRandomPrivateKey() + require.NoError(t, err) + admin := adminPk.PublicKey() + + account1 := chainwriter.GetRandomPubKey(t) + account2 := chainwriter.GetRandomPubKey(t) + + seed1 := []byte("seed1") + account3 := mustFindPdaProgramAddress(t, [][]byte{seed1}, solana.SystemProgramID) + + // create lookup table addresses + seed2 := []byte("seed2") + programID := chainwriter.GetRandomPubKey(t) + derivedTablePda := mustFindPdaProgramAddress(t, [][]byte{seed2}, programID) + // mock data account response from program + derivedLookupTablePubkey := mockDataAccountLookupTable(t, rw, derivedTablePda) + // mock fetch lookup table addresses call + derivedLookupKeys := chainwriter.CreateTestPubKeys(t, 1) + mockFetchLookupTableAddresses(t, rw, derivedLookupTablePubkey, derivedLookupKeys) + + // mock static lookup table call + staticLookupTablePubkey := chainwriter.GetRandomPubKey(t) + staticLookupKeys := chainwriter.CreateTestPubKeys(t, 2) + mockFetchLookupTableAddresses(t, rw, staticLookupTablePubkey, staticLookupKeys) + + cwConfig := chainwriter.ChainWriterConfig{ + Programs: map[string]chainwriter.ProgramConfig{ + "39vbQVpEMtZtg3e6ZSE7nBSzmNZptmW45WnLkbqEe4TU": { + Methods: map[string]chainwriter.MethodConfig{ + "initialize": { + FromAddress: admin.String(), + ChainSpecificName: "initialize", + LookupTables: chainwriter.LookupTables{ + DerivedLookupTables: []chainwriter.DerivedLookupTable{ + { + Name: "DerivedTable", + Accounts: chainwriter.PDALookups{ + Name: "DataAccountPDA", + PublicKey: chainwriter.AccountConstant{Name: "WriteTest", Address: programID.String()}, + Seeds: []chainwriter.Lookup{ + // extract seed2 for PDA lookup + chainwriter.AccountLookup{Name: "seed2", Location: "seed2"}, + }, + IsSigner: false, + IsWritable: false, + InternalField: chainwriter.InternalField{ + Type: reflect.TypeOf(DataAccount{}), + Location: "LookupTable", + }, + }, + }, + }, + StaticLookupTables: []string{staticLookupTablePubkey.String()}, + }, + Accounts: []chainwriter.Lookup{ + chainwriter.AccountConstant{ + Name: "Constant", + Address: account1.String(), + IsSigner: false, + IsWritable: false, + }, + chainwriter.AccountLookup{ + Name: "LookupTable", + Location: "lookup_table", + IsSigner: false, + IsWritable: false, + }, + chainwriter.PDALookups{ + Name: "DataAccountPDA", + PublicKey: chainwriter.AccountConstant{Name: "WriteTest", Address: solana.SystemProgramID.String()}, + Seeds: []chainwriter.Lookup{ + // extract seed1 for PDA lookup + chainwriter.AccountLookup{Name: "seed1", Location: "seed1"}, + }, + IsSigner: false, + IsWritable: false, + // Just get the address of the account, nothing internal. + InternalField: chainwriter.InternalField{}, + }, + chainwriter.AccountsFromLookupTable{ + LookupTableName: "DerivedTable", + IncludeIndexes: []int{0}, + }, + }, + }, + }, + IDL: writeTestIdlJSON, + }, + }, + } + + // initialize chain writer + cw, err := chainwriter.NewSolanaChainWriterService(rw, txm, ge, cwConfig) + require.NoError(t, err) + + t.Run("fails with invalid ABI", func(t *testing.T) { + invalidCWConfig := chainwriter.ChainWriterConfig{ + Programs: map[string]chainwriter.ProgramConfig{ + "write_test": { + Methods: map[string]chainwriter.MethodConfig{ + "invalid": { + ChainSpecificName: "invalid", + }, + }, + IDL: "", + }, + }, + } + + _, err := chainwriter.NewSolanaChainWriterService(rw, txm, ge, invalidCWConfig) + require.Error(t, err) + }) + + t.Run("fails to encode payload if args with missing values provided", func(t *testing.T) { + txID := uuid.NewString() + args := map[string]interface{}{} + submitErr := cw.SubmitTransaction(ctx, "39vbQVpEMtZtg3e6ZSE7nBSzmNZptmW45WnLkbqEe4TU", "initialize", args, txID, programID.String(), nil, nil) + require.Error(t, submitErr) + }) + + t.Run("fails if invalid contract name provided", func(t *testing.T) { + txID := uuid.NewString() + args := map[string]interface{}{} + submitErr := cw.SubmitTransaction(ctx, "write_test", "initialize", args, txID, programID.String(), nil, nil) + require.Error(t, submitErr) + }) + + t.Run("fails if invalid method provided", func(t *testing.T) { + txID := uuid.NewString() + args := map[string]interface{}{} + submitErr := cw.SubmitTransaction(ctx, "39vbQVpEMtZtg3e6ZSE7nBSzmNZptmW45WnLkbqEe4TU", "badMethod", args, txID, programID.String(), nil, nil) + require.Error(t, submitErr) + }) + + t.Run("submits transaction successfully", func(t *testing.T) { + recentBlockHash := solana.Hash{} + rw.On("LatestBlockhash", mock.Anything).Return(&rpc.GetLatestBlockhashResult{Value: &rpc.LatestBlockhashResult{Blockhash: recentBlockHash, LastValidBlockHeight: uint64(100)}}, nil).Once() + txID := uuid.NewString() + configProgramID := solana.MustPublicKeyFromBase58("39vbQVpEMtZtg3e6ZSE7nBSzmNZptmW45WnLkbqEe4TU") + + txm.On("Enqueue", mock.Anything, account1.String(), mock.MatchedBy(func(tx *solana.Transaction) bool { + // match transaction fields to ensure it was built as expected + require.Equal(t, recentBlockHash, tx.Message.RecentBlockhash) + require.Len(t, tx.Message.Instructions, 1) + require.Len(t, tx.Message.AccountKeys, 5) // fee payer + derived accounts + require.Equal(t, admin, tx.Message.AccountKeys[0]) // fee payer + require.Equal(t, account1, tx.Message.AccountKeys[1]) // account constant + require.Equal(t, account2, tx.Message.AccountKeys[2]) // account lookup + require.Equal(t, account3, tx.Message.AccountKeys[3]) // pda lookup + require.Equal(t, configProgramID, tx.Message.AccountKeys[4]) // instruction program ID + require.Len(t, tx.Message.AddressTableLookups, 1) // address table look contains entry + require.Equal(t, derivedLookupTablePubkey, tx.Message.AddressTableLookups[0].AccountKey) // address table + return true + }), &txID).Return(nil).Once() + + args := map[string]interface{}{ + "lookupTable": chainwriter.GetRandomPubKey(t).Bytes(), + "lookup_table": account2.Bytes(), + "seed1": seed1, + "seed2": seed2, + } + submitErr := cw.SubmitTransaction(ctx, "39vbQVpEMtZtg3e6ZSE7nBSzmNZptmW45WnLkbqEe4TU", "initialize", args, txID, programID.String(), nil, nil) + require.NoError(t, submitErr) + }) +} + +func TestChainWriter_GetTransactionStatus(t *testing.T) { + t.Parallel() + + ctx := tests.Context(t) + rw := clientmocks.NewReaderWriter(t) + ge := feemocks.NewEstimator(t) + + // mock txm + txm := txmMocks.NewTxManager(t) + + // initialize chain writer + cw, err := chainwriter.NewSolanaChainWriterService(rw, txm, ge, chainwriter.ChainWriterConfig{}) + require.NoError(t, err) + + t.Run("returns unknown with error if ID not found", func(t *testing.T) { + txID := uuid.NewString() + txm.On("GetTransactionStatus", mock.Anything, txID).Return(types.Unknown, errors.New("tx not found")).Once() + status, err := cw.GetTransactionStatus(ctx, txID) + require.Error(t, err) + require.Equal(t, types.Unknown, status) + }) + + t.Run("returns pending when transaction is pending", func(t *testing.T) { + txID := uuid.NewString() + txm.On("GetTransactionStatus", mock.Anything, txID).Return(types.Pending, nil).Once() + status, err := cw.GetTransactionStatus(ctx, txID) + require.NoError(t, err) + require.Equal(t, types.Pending, status) + }) + + t.Run("returns unconfirmed when transaction is unconfirmed", func(t *testing.T) { + txID := uuid.NewString() + txm.On("GetTransactionStatus", mock.Anything, txID).Return(types.Unconfirmed, nil).Once() + status, err := cw.GetTransactionStatus(ctx, txID) + require.NoError(t, err) + require.Equal(t, types.Unconfirmed, status) + }) + + t.Run("returns finalized when transaction is finalized", func(t *testing.T) { + txID := uuid.NewString() + txm.On("GetTransactionStatus", mock.Anything, txID).Return(types.Finalized, nil).Once() + status, err := cw.GetTransactionStatus(ctx, txID) + require.NoError(t, err) + require.Equal(t, types.Finalized, status) + }) + + t.Run("returns failed when transaction error classfied as failed", func(t *testing.T) { + txID := uuid.NewString() + txm.On("GetTransactionStatus", mock.Anything, txID).Return(types.Failed, nil).Once() + status, err := cw.GetTransactionStatus(ctx, txID) + require.NoError(t, err) + require.Equal(t, types.Failed, status) + }) + + t.Run("returns fatal when transaction error classfied as fatal", func(t *testing.T) { + txID := uuid.NewString() + txm.On("GetTransactionStatus", mock.Anything, txID).Return(types.Fatal, nil).Once() + status, err := cw.GetTransactionStatus(ctx, txID) + require.NoError(t, err) + require.Equal(t, types.Fatal, status) + }) +} + +func TestChainWriter_GetFeeComponents(t *testing.T) { + t.Parallel() + + ctx := tests.Context(t) + rw := clientmocks.NewReaderWriter(t) + ge := feemocks.NewEstimator(t) + ge.On("BaseComputeUnitPrice").Return(uint64(100)) + + // mock txm + txm := txmMocks.NewTxManager(t) + + cw, err := chainwriter.NewSolanaChainWriterService(rw, txm, ge, chainwriter.ChainWriterConfig{}) + require.NoError(t, err) + + t.Run("returns valid compute unit price", func(t *testing.T) { + feeComponents, err := cw.GetFeeComponents(ctx) + require.NoError(t, err) + require.Equal(t, big.NewInt(100), feeComponents.ExecutionFee) + require.Nil(t, feeComponents.DataAvailabilityFee) // always nil for Solana + }) + + t.Run("fails if gas estimator not set", func(t *testing.T) { + cwNoEstimator, err := chainwriter.NewSolanaChainWriterService(rw, txm, nil, chainwriter.ChainWriterConfig{}) + require.NoError(t, err) + _, err = cwNoEstimator.GetFeeComponents(ctx) + require.Error(t, err) + }) +} + +func mustBorshEncodeStruct(t *testing.T, data interface{}) []byte { + buf := new(bytes.Buffer) + err := ag_binary.NewBorshEncoder(buf).Encode(data) + require.NoError(t, err) + return buf.Bytes() +} + +func mustFindPdaProgramAddress(t *testing.T, seeds [][]byte, programID solana.PublicKey) solana.PublicKey { + pda, _, err := solana.FindProgramAddress(seeds, programID) + require.NoError(t, err) + return pda +} + +func mockDataAccountLookupTable(t *testing.T, rw *clientmocks.ReaderWriter, pda solana.PublicKey) solana.PublicKey { + lookupTablePubkey := chainwriter.GetRandomPubKey(t) + dataAccount := DataAccount{ + Discriminator: [8]byte{}, + Version: 1, + Administrator: chainwriter.GetRandomPubKey(t), + PendingAdministrator: chainwriter.GetRandomPubKey(t), + LookupTable: lookupTablePubkey, + } + dataAccountBytes := mustBorshEncodeStruct(t, dataAccount) + rw.On("GetAccountInfoWithOpts", mock.Anything, pda, mock.Anything).Return(&rpc.GetAccountInfoResult{ + RPCContext: rpc.RPCContext{}, + Value: &rpc.Account{Data: rpc.DataBytesOrJSONFromBytes(dataAccountBytes)}, + }, nil) + return lookupTablePubkey +} + +func mockFetchLookupTableAddresses(t *testing.T, rw *clientmocks.ReaderWriter, lookupTablePubkey solana.PublicKey, storedPubkeys []solana.PublicKey) { + var lookupTablePubkeySlice solana.PublicKeySlice + lookupTablePubkeySlice.Append(storedPubkeys...) + lookupTableState := addresslookuptable.AddressLookupTableState{ + Addresses: lookupTablePubkeySlice, + } + lookupTableStateBytes := mustBorshEncodeStruct(t, lookupTableState) + rw.On("GetAccountInfoWithOpts", mock.Anything, lookupTablePubkey, mock.Anything).Return(&rpc.GetAccountInfoResult{ + RPCContext: rpc.RPCContext{}, + Value: &rpc.Account{Data: rpc.DataBytesOrJSONFromBytes(lookupTableStateBytes)}, + }, nil) +} diff --git a/pkg/solana/chainwriter/helpers.go b/pkg/solana/chainwriter/helpers.go index 4d5d00600..bc256c60a 100644 --- a/pkg/solana/chainwriter/helpers.go +++ b/pkg/solana/chainwriter/helpers.go @@ -1,12 +1,19 @@ package chainwriter import ( + "context" + "crypto/sha256" "errors" "fmt" "reflect" "strings" + "testing" "github.com/gagliardetto/solana-go" + "github.com/gagliardetto/solana-go/rpc" + "github.com/stretchr/testify/require" + + "github.com/smartcontractkit/chainlink-solana/pkg/solana/utils" ) // GetValuesAtLocation parses through nested types and arrays to find all locations of values @@ -54,11 +61,11 @@ func GetValueAtLocation(args any, location string) ([][]byte, error) { var values [][]byte for _, value := range valueList { - if byteArray, ok := value.([]byte); ok { - values = append(values, byteArray) - } else { + byteArray, ok := value.([]byte) + if !ok { return nil, fmt.Errorf("invalid value format at path: %s", location) } + values = append(values, byteArray) } return values, nil @@ -120,3 +127,76 @@ func traversePath(data any, path []string) ([]any, error) { return nil, errors.New("unexpected type encountered at path: " + path[0]) } } + +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++ { + addresses[i] = GetRandomPubKey(t) + } + return addresses +} + +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) + table, instruction, ierr := utils.NewCreateLookupTableInstruction( + sender.PublicKey(), + sender.PublicKey(), + slot, + ) + require.NoError(t, ierr) + utils.SendAndConfirm(ctx, t, c, []solana.Instruction{instruction}, sender, rpc.CommitmentConfirmed) + + // add entries to lookup table + utils.SendAndConfirm(ctx, t, c, []solana.Instruction{ + utils.NewExtendLookupTableInstruction( + table, sender.PublicKey(), sender.PublicKey(), + addresses, + ), + }, sender, rpc.CommitmentConfirmed) + + return table +} diff --git a/pkg/solana/chainwriter/lookups.go b/pkg/solana/chainwriter/lookups.go index 1aa9ae92d..1947b060d 100644 --- a/pkg/solana/chainwriter/lookups.go +++ b/pkg/solana/chainwriter/lookups.go @@ -9,6 +9,7 @@ import ( "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" ) @@ -69,8 +70,8 @@ type DerivedLookupTable struct { // AccountsFromLookupTable extracts accounts from a lookup table that was previously read and stored in memory. type AccountsFromLookupTable struct { - LookupTablesName string - IncludeIndexes []int + LookupTableName string + IncludeIndexes []int } func (ac AccountConstant) Resolve(_ context.Context, _ any, _ map[string]map[string][]*solana.AccountMeta, _ client.Reader) ([]*solana.AccountMeta, error) { @@ -106,9 +107,9 @@ func (al AccountLookup) Resolve(_ context.Context, args any, _ map[string]map[st 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] + innerMap, ok := derivedTableMap[alt.LookupTableName] if !ok { - return nil, fmt.Errorf("lookup table not found: %s", alt.LookupTablesName) + return nil, fmt.Errorf("lookup table not found: %s", alt.LookupTableName) } var result []*solana.AccountMeta @@ -125,7 +126,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, fmt.Errorf("invalid index %d for account %s in lookup table %s", index, publicKey, alt.LookupTablesName) + return nil, fmt.Errorf("invalid index %d for account %s in lookup table %s", index, publicKey, alt.LookupTableName) } result = append(result, metas[index]) } @@ -161,6 +162,7 @@ func (pda PDALookups) Resolve(ctx context.Context, args any, derivedTableMap map Encoding: "base64", Commitment: rpc.CommitmentFinalized, }) + fmt.Printf("Accounts Info: %+v", accountInfo) 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) @@ -325,8 +327,8 @@ func (s *SolanaChainWriterService) LoadTable(ctx context.Context, args any, rlt for _, addr := range addresses { resultMap[rlt.Name][addressMeta.PublicKey.String()] = append(resultMap[rlt.Name][addressMeta.PublicKey.String()], &solana.AccountMeta{ PublicKey: addr, - IsSigner: false, - IsWritable: false, + IsSigner: addressMeta.IsSigner, + IsWritable: addressMeta.IsWritable, }) } diff --git a/pkg/solana/chainwriter/lookups_test.go b/pkg/solana/chainwriter/lookups_test.go index 2a75814bf..53972feac 100644 --- a/pkg/solana/chainwriter/lookups_test.go +++ b/pkg/solana/chainwriter/lookups_test.go @@ -2,24 +2,24 @@ package chainwriter_test import ( "context" - "crypto/sha256" "reflect" "testing" "time" "github.com/gagliardetto/solana-go" "github.com/gagliardetto/solana-go/rpc" + "github.com/stretchr/testify/require" + "github.com/smartcontractkit/chainlink-common/pkg/logger" + commonutils "github.com/smartcontractkit/chainlink-common/pkg/utils" "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-solana/pkg/solana/utils" - - commonutils "github.com/smartcontractkit/chainlink-common/pkg/utils" - "github.com/test-go/testify/require" ) type TestArgs struct { @@ -40,7 +40,7 @@ type DataAccount struct { func TestAccountContant(t *testing.T) { t.Run("AccountConstant resolves valid address", func(t *testing.T) { - expectedAddr := getRandomPubKey(t) + expectedAddr := chainwriter.GetRandomPubKey(t) expectedMeta := []*solana.AccountMeta{ { PublicKey: expectedAddr, @@ -54,14 +54,15 @@ func TestAccountContant(t *testing.T) { IsSigner: true, IsWritable: true, } - result, err := constantConfig.Resolve(nil, nil, nil, nil) + result, err := constantConfig.Resolve(tests.Context(t), nil, nil, nil) require.NoError(t, err) require.Equal(t, expectedMeta, result) }) } func TestAccountLookups(t *testing.T) { + ctx := tests.Context(t) t.Run("AccountLookup resolves valid address with just one address", func(t *testing.T) { - expectedAddr := getRandomPubKey(t) + expectedAddr := chainwriter.GetRandomPubKey(t) testArgs := TestArgs{ Inner: []InnerArgs{ {Address: expectedAddr.Bytes()}, @@ -81,14 +82,14 @@ func TestAccountLookups(t *testing.T) { IsSigner: true, IsWritable: true, } - result, err := lookupConfig.Resolve(nil, testArgs, nil, nil) + result, err := lookupConfig.Resolve(ctx, 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 := getRandomPubKey(t) - expectedAddr2 := getRandomPubKey(t) + expectedAddr1 := chainwriter.GetRandomPubKey(t) + expectedAddr2 := chainwriter.GetRandomPubKey(t) testArgs := TestArgs{ Inner: []InnerArgs{ @@ -115,7 +116,7 @@ func TestAccountLookups(t *testing.T) { IsSigner: true, IsWritable: true, } - result, err := lookupConfig.Resolve(nil, testArgs, nil, nil) + result, err := lookupConfig.Resolve(ctx, testArgs, nil, nil) require.NoError(t, err) for i, meta := range result { require.Equal(t, expectedMeta[i], meta) @@ -123,7 +124,7 @@ func TestAccountLookups(t *testing.T) { }) t.Run("AccountLookup fails when address isn't in args", func(t *testing.T) { - expectedAddr := getRandomPubKey(t) + expectedAddr := chainwriter.GetRandomPubKey(t) testArgs := TestArgs{ Inner: []InnerArgs{ @@ -136,7 +137,7 @@ func TestAccountLookups(t *testing.T) { IsSigner: true, IsWritable: true, } - _, err := lookupConfig.Resolve(nil, testArgs, nil, nil) + _, err := lookupConfig.Resolve(ctx, testArgs, nil, nil) require.Error(t, err) }) } @@ -145,7 +146,7 @@ func TestPDALookups(t *testing.T) { programID := solana.SystemProgramID t.Run("PDALookup resolves valid PDA with constant address seeds", func(t *testing.T) { - seed := getRandomPubKey(t) + seed := chainwriter.GetRandomPubKey(t) pda, _, err := solana.FindProgramAddress([][]byte{seed.Bytes()}, programID) require.NoError(t, err) @@ -211,8 +212,6 @@ func TestPDALookups(t *testing.T) { }) t.Run("PDALookup fails with missing seeds", func(t *testing.T) { - programID := solana.SystemProgramID - pdaLookup := chainwriter.PDALookups{ Name: "TestPDA", PublicKey: chainwriter.AccountConstant{Name: "ProgramID", Address: programID.String()}, @@ -234,8 +233,8 @@ func TestPDALookups(t *testing.T) { }) t.Run("PDALookup resolves valid PDA with address lookup seeds", func(t *testing.T) { - seed1 := getRandomPubKey(t) - seed2 := getRandomPubKey(t) + seed1 := chainwriter.GetRandomPubKey(t) + seed2 := chainwriter.GetRandomPubKey(t) pda, _, err := solana.FindProgramAddress([][]byte{seed1.Bytes(), seed2.Bytes()}, programID) require.NoError(t, err) @@ -292,22 +291,22 @@ func TestLookupTables(t *testing.T) { txm := txm.NewTxm("localnet", loader, nil, cfg, mkey, lggr) - cw, 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(ctx, t, rpcClient, sender, pubKeys) + pubKeys := chainwriter.CreateTestPubKeys(t, 8) + table := chainwriter.CreateTestLookupTable(ctx, t, rpcClient, sender, pubKeys) lookupConfig := chainwriter.LookupTables{ DerivedLookupTables: nil, StaticLookupTables: []string{table.String()}, } - _, staticTableMap, err := cw.ResolveLookupTables(ctx, nil, lookupConfig) - require.NoError(t, err) + _, staticTableMap, resolveErr := cw.ResolveLookupTables(ctx, nil, lookupConfig) + require.NoError(t, resolveErr) require.Equal(t, pubKeys, staticTableMap[table]) }) t.Run("Derived lookup table resolves properly with constant address", func(t *testing.T) { - pubKeys := createTestPubKeys(t, 8) - table := CreateTestLookupTable(ctx, t, rpcClient, sender, pubKeys) + pubKeys := chainwriter.CreateTestPubKeys(t, 8) + table := chainwriter.CreateTestLookupTable(ctx, t, rpcClient, sender, pubKeys) lookupConfig := chainwriter.LookupTables{ DerivedLookupTables: []chainwriter.DerivedLookupTable{ { @@ -322,8 +321,8 @@ func TestLookupTables(t *testing.T) { }, StaticLookupTables: nil, } - derivedTableMap, _, err := cw.ResolveLookupTables(ctx, nil, lookupConfig) - require.NoError(t, err) + derivedTableMap, _, resolveErr := cw.ResolveLookupTables(ctx, nil, lookupConfig) + require.NoError(t, resolveErr) addresses, ok := derivedTableMap["DerivedTable"][table.String()] require.True(t, ok) @@ -333,7 +332,7 @@ func TestLookupTables(t *testing.T) { }) t.Run("Derived lookup table fails with invalid address", func(t *testing.T) { - invalidTable := getRandomPubKey(t) + invalidTable := chainwriter.GetRandomPubKey(t) lookupConfig := chainwriter.LookupTables{ DerivedLookupTables: []chainwriter.DerivedLookupTable{ @@ -356,7 +355,7 @@ func TestLookupTables(t *testing.T) { }) t.Run("Static lookup table fails with invalid address", func(t *testing.T) { - invalidTable := getRandomPubKey(t) + invalidTable := chainwriter.GetRandomPubKey(t) lookupConfig := chainwriter.LookupTables{ DerivedLookupTables: nil, @@ -369,8 +368,8 @@ func TestLookupTables(t *testing.T) { }) t.Run("Derived lookup table resolves properly with account lookup address", func(t *testing.T) { - pubKeys := createTestPubKeys(t, 8) - table := CreateTestLookupTable(ctx, t, rpcClient, sender, pubKeys) + pubKeys := chainwriter.CreateTestPubKeys(t, 8) + table := chainwriter.CreateTestLookupTable(ctx, t, rpcClient, sender, pubKeys) lookupConfig := chainwriter.LookupTables{ DerivedLookupTables: []chainwriter.DerivedLookupTable{ { @@ -405,10 +404,10 @@ func TestLookupTables(t *testing.T) { // Deployed write_test contract programID := solana.MustPublicKeyFromBase58("39vbQVpEMtZtg3e6ZSE7nBSzmNZptmW45WnLkbqEe4TU") - lookupKeys := createTestPubKeys(t, 5) - lookupTable := CreateTestLookupTable(ctx, t, rpcClient, sender, lookupKeys) + lookupKeys := chainwriter.CreateTestPubKeys(t, 5) + lookupTable := chainwriter.CreateTestLookupTable(ctx, t, rpcClient, sender, lookupKeys) - InitializeDataAccount(ctx, t, rpcClient, programID, sender, lookupTable) + chainwriter.InitializeDataAccount(ctx, t, rpcClient, programID, sender, lookupTable) args := map[string]interface{}{ "seed1": []byte("data"), @@ -446,76 +445,3 @@ func TestLookupTables(t *testing.T) { } }) } - -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++ { - addresses[i] = getRandomPubKey(t) - } - return addresses -} - -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) - table, instruction, ierr := utils.NewCreateLookupTableInstruction( - sender.PublicKey(), - sender.PublicKey(), - slot, - ) - require.NoError(t, ierr) - utils.SendAndConfirm(ctx, t, c, []solana.Instruction{instruction}, sender, rpc.CommitmentConfirmed) - - // add entries to lookup table - utils.SendAndConfirm(ctx, t, c, []solana.Instruction{ - utils.NewExtendLookupTableInstruction( - table, sender.PublicKey(), sender.PublicKey(), - addresses, - ), - }, sender, rpc.CommitmentConfirmed) - - return table -} diff --git a/pkg/solana/relay.go b/pkg/solana/relay.go index fca61ba9f..7b5c3b39f 100644 --- a/pkg/solana/relay.go +++ b/pkg/solana/relay.go @@ -19,6 +19,7 @@ import ( "github.com/smartcontractkit/chainlink-solana/pkg/solana/client" "github.com/smartcontractkit/chainlink-solana/pkg/solana/txm" + txmutils "github.com/smartcontractkit/chainlink-solana/pkg/solana/txm/utils" ) var _ TxManager = (*txm.Txm)(nil) diff --git a/pkg/solana/transmitter_test.go b/pkg/solana/transmitter_test.go index f6db01d6c..71d9d7bc3 100644 --- a/pkg/solana/transmitter_test.go +++ b/pkg/solana/transmitter_test.go @@ -17,7 +17,7 @@ import ( "github.com/smartcontractkit/chainlink-solana/pkg/solana/client" clientmocks "github.com/smartcontractkit/chainlink-solana/pkg/solana/client/mocks" "github.com/smartcontractkit/chainlink-solana/pkg/solana/fees" - "github.com/smartcontractkit/chainlink-solana/pkg/solana/txm" + txmutils "github.com/smartcontractkit/chainlink-solana/pkg/solana/txm/utils" ) // custom mock txm instead of mockery generated because SetTxConfig causes circular imports diff --git a/pkg/solana/txm/mocks/tx_manager.go b/pkg/solana/txm/mocks/tx_manager.go new file mode 100644 index 000000000..50806a4da --- /dev/null +++ b/pkg/solana/txm/mocks/tx_manager.go @@ -0,0 +1,390 @@ +// Code generated by mockery v2.43.2. DO NOT EDIT. + +package mocks + +import ( + context "context" + + solana "github.com/gagliardetto/solana-go" + mock "github.com/stretchr/testify/mock" + + types "github.com/smartcontractkit/chainlink-common/pkg/types" + + utils "github.com/smartcontractkit/chainlink-solana/pkg/solana/txm/utils" +) + +// TxManager is an autogenerated mock type for the TxManager type +type TxManager struct { + mock.Mock +} + +type TxManager_Expecter struct { + mock *mock.Mock +} + +func (_m *TxManager) EXPECT() *TxManager_Expecter { + return &TxManager_Expecter{mock: &_m.Mock} +} + +// Close provides a mock function with given fields: +func (_m *TxManager) Close() error { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for Close") + } + + var r0 error + if rf, ok := ret.Get(0).(func() error); ok { + r0 = rf() + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// TxManager_Close_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Close' +type TxManager_Close_Call struct { + *mock.Call +} + +// Close is a helper method to define mock.On call +func (_e *TxManager_Expecter) Close() *TxManager_Close_Call { + return &TxManager_Close_Call{Call: _e.mock.On("Close")} +} + +func (_c *TxManager_Close_Call) Run(run func()) *TxManager_Close_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *TxManager_Close_Call) Return(_a0 error) *TxManager_Close_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *TxManager_Close_Call) RunAndReturn(run func() error) *TxManager_Close_Call { + _c.Call.Return(run) + return _c +} + +// Enqueue provides a mock function with given fields: ctx, accountID, tx, txID, txCfgs +func (_m *TxManager) Enqueue(ctx context.Context, accountID string, tx *solana.Transaction, txID *string, txCfgs ...utils.SetTxConfig) error { + _va := make([]interface{}, len(txCfgs)) + for _i := range txCfgs { + _va[_i] = txCfgs[_i] + } + var _ca []interface{} + _ca = append(_ca, ctx, accountID, tx, txID) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + if len(ret) == 0 { + panic("no return value specified for Enqueue") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string, *solana.Transaction, *string, ...utils.SetTxConfig) error); ok { + r0 = rf(ctx, accountID, tx, txID, txCfgs...) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// TxManager_Enqueue_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Enqueue' +type TxManager_Enqueue_Call struct { + *mock.Call +} + +// Enqueue is a helper method to define mock.On call +// - ctx context.Context +// - accountID string +// - tx *solana.Transaction +// - txID *string +// - txCfgs ...utils.SetTxConfig +func (_e *TxManager_Expecter) Enqueue(ctx interface{}, accountID interface{}, tx interface{}, txID interface{}, txCfgs ...interface{}) *TxManager_Enqueue_Call { + return &TxManager_Enqueue_Call{Call: _e.mock.On("Enqueue", + append([]interface{}{ctx, accountID, tx, txID}, txCfgs...)...)} +} + +func (_c *TxManager_Enqueue_Call) Run(run func(ctx context.Context, accountID string, tx *solana.Transaction, txID *string, txCfgs ...utils.SetTxConfig)) *TxManager_Enqueue_Call { + _c.Call.Run(func(args mock.Arguments) { + variadicArgs := make([]utils.SetTxConfig, len(args)-4) + for i, a := range args[4:] { + if a != nil { + variadicArgs[i] = a.(utils.SetTxConfig) + } + } + run(args[0].(context.Context), args[1].(string), args[2].(*solana.Transaction), args[3].(*string), variadicArgs...) + }) + return _c +} + +func (_c *TxManager_Enqueue_Call) Return(_a0 error) *TxManager_Enqueue_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *TxManager_Enqueue_Call) RunAndReturn(run func(context.Context, string, *solana.Transaction, *string, ...utils.SetTxConfig) error) *TxManager_Enqueue_Call { + _c.Call.Return(run) + return _c +} + +// GetTransactionStatus provides a mock function with given fields: ctx, transactionID +func (_m *TxManager) GetTransactionStatus(ctx context.Context, transactionID string) (types.TransactionStatus, error) { + ret := _m.Called(ctx, transactionID) + + if len(ret) == 0 { + panic("no return value specified for GetTransactionStatus") + } + + var r0 types.TransactionStatus + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string) (types.TransactionStatus, error)); ok { + return rf(ctx, transactionID) + } + if rf, ok := ret.Get(0).(func(context.Context, string) types.TransactionStatus); ok { + r0 = rf(ctx, transactionID) + } else { + r0 = ret.Get(0).(types.TransactionStatus) + } + + if rf, ok := ret.Get(1).(func(context.Context, string) error); ok { + r1 = rf(ctx, transactionID) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// TxManager_GetTransactionStatus_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetTransactionStatus' +type TxManager_GetTransactionStatus_Call struct { + *mock.Call +} + +// GetTransactionStatus is a helper method to define mock.On call +// - ctx context.Context +// - transactionID string +func (_e *TxManager_Expecter) GetTransactionStatus(ctx interface{}, transactionID interface{}) *TxManager_GetTransactionStatus_Call { + return &TxManager_GetTransactionStatus_Call{Call: _e.mock.On("GetTransactionStatus", ctx, transactionID)} +} + +func (_c *TxManager_GetTransactionStatus_Call) Run(run func(ctx context.Context, transactionID string)) *TxManager_GetTransactionStatus_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(string)) + }) + return _c +} + +func (_c *TxManager_GetTransactionStatus_Call) Return(_a0 types.TransactionStatus, _a1 error) *TxManager_GetTransactionStatus_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *TxManager_GetTransactionStatus_Call) RunAndReturn(run func(context.Context, string) (types.TransactionStatus, error)) *TxManager_GetTransactionStatus_Call { + _c.Call.Return(run) + return _c +} + +// HealthReport provides a mock function with given fields: +func (_m *TxManager) HealthReport() map[string]error { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for HealthReport") + } + + var r0 map[string]error + if rf, ok := ret.Get(0).(func() map[string]error); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(map[string]error) + } + } + + return r0 +} + +// TxManager_HealthReport_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'HealthReport' +type TxManager_HealthReport_Call struct { + *mock.Call +} + +// HealthReport is a helper method to define mock.On call +func (_e *TxManager_Expecter) HealthReport() *TxManager_HealthReport_Call { + return &TxManager_HealthReport_Call{Call: _e.mock.On("HealthReport")} +} + +func (_c *TxManager_HealthReport_Call) Run(run func()) *TxManager_HealthReport_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *TxManager_HealthReport_Call) Return(_a0 map[string]error) *TxManager_HealthReport_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *TxManager_HealthReport_Call) RunAndReturn(run func() map[string]error) *TxManager_HealthReport_Call { + _c.Call.Return(run) + return _c +} + +// Name provides a mock function with given fields: +func (_m *TxManager) Name() string { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for Name") + } + + var r0 string + if rf, ok := ret.Get(0).(func() string); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(string) + } + + return r0 +} + +// TxManager_Name_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Name' +type TxManager_Name_Call struct { + *mock.Call +} + +// Name is a helper method to define mock.On call +func (_e *TxManager_Expecter) Name() *TxManager_Name_Call { + return &TxManager_Name_Call{Call: _e.mock.On("Name")} +} + +func (_c *TxManager_Name_Call) Run(run func()) *TxManager_Name_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *TxManager_Name_Call) Return(_a0 string) *TxManager_Name_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *TxManager_Name_Call) RunAndReturn(run func() string) *TxManager_Name_Call { + _c.Call.Return(run) + return _c +} + +// Ready provides a mock function with given fields: +func (_m *TxManager) Ready() error { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for Ready") + } + + var r0 error + if rf, ok := ret.Get(0).(func() error); ok { + r0 = rf() + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// TxManager_Ready_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Ready' +type TxManager_Ready_Call struct { + *mock.Call +} + +// Ready is a helper method to define mock.On call +func (_e *TxManager_Expecter) Ready() *TxManager_Ready_Call { + return &TxManager_Ready_Call{Call: _e.mock.On("Ready")} +} + +func (_c *TxManager_Ready_Call) Run(run func()) *TxManager_Ready_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *TxManager_Ready_Call) Return(_a0 error) *TxManager_Ready_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *TxManager_Ready_Call) RunAndReturn(run func() error) *TxManager_Ready_Call { + _c.Call.Return(run) + return _c +} + +// Start provides a mock function with given fields: _a0 +func (_m *TxManager) Start(_a0 context.Context) error { + ret := _m.Called(_a0) + + if len(ret) == 0 { + panic("no return value specified for Start") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context) error); ok { + r0 = rf(_a0) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// TxManager_Start_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Start' +type TxManager_Start_Call struct { + *mock.Call +} + +// Start is a helper method to define mock.On call +// - _a0 context.Context +func (_e *TxManager_Expecter) Start(_a0 interface{}) *TxManager_Start_Call { + return &TxManager_Start_Call{Call: _e.mock.On("Start", _a0)} +} + +func (_c *TxManager_Start_Call) Run(run func(_a0 context.Context)) *TxManager_Start_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context)) + }) + return _c +} + +func (_c *TxManager_Start_Call) Return(_a0 error) *TxManager_Start_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *TxManager_Start_Call) RunAndReturn(run func(context.Context) error) *TxManager_Start_Call { + _c.Call.Return(run) + return _c +} + +// NewTxManager creates a new instance of TxManager. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewTxManager(t interface { + mock.TestingT + Cleanup(func()) +}) *TxManager { + mock := &TxManager{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/pkg/solana/txm/pendingtx.go b/pkg/solana/txm/pendingtx.go index b08039ab7..3b60f0248 100644 --- a/pkg/solana/txm/pendingtx.go +++ b/pkg/solana/txm/pendingtx.go @@ -9,6 +9,8 @@ import ( "github.com/gagliardetto/solana-go" "golang.org/x/exp/maps" + + "github.com/smartcontractkit/chainlink-solana/pkg/solana/txm/utils" ) var ( @@ -40,11 +42,11 @@ type PendingTxContext interface { // OnFinalized marks transaction as Finalized, moves it from the broadcasted or confirmed map to finalized map, removes signatures from signature map to stop confirmation checks OnFinalized(sig solana.Signature, retentionTimeout time.Duration) (string, error) // OnPrebroadcastError adds transaction that has not yet been broadcasted to the finalized/errored map as errored, matches err type using enum - OnPrebroadcastError(id string, retentionTimeout time.Duration, txState TxState, errType TxErrType) error + OnPrebroadcastError(id string, retentionTimeout time.Duration, txState utils.TxState, errType TxErrType) error // OnError marks transaction as errored, matches err type using enum, moves it from the broadcasted or confirmed map to finalized/errored map, removes signatures from signature map to stop confirmation checks - OnError(sig solana.Signature, retentionTimeout time.Duration, txState TxState, errType TxErrType) (string, error) + OnError(sig solana.Signature, retentionTimeout time.Duration, txState utils.TxState, errType TxErrType) (string, error) // GetTxState returns the transaction state for the provided ID if it exists - GetTxState(id string) (TxState, error) + GetTxState(id string) (utils.TxState, error) // TrimFinalizedErroredTxs removes transactions that have reached their retention time TrimFinalizedErroredTxs() int } @@ -63,7 +65,7 @@ type pendingTx struct { // finishedTx is used to store minimal info specifically for finalized or errored transactions for external status checks type finishedTx struct { retentionTs time.Time - state TxState + state utils.TxState } var _ PendingTxContext = &pendingTxContext{} @@ -133,7 +135,7 @@ func (c *pendingTxContext) New(tx pendingTx, sig solana.Signature, cancel contex // add signature to tx tx.signatures = append(tx.signatures, sig) tx.createTs = time.Now() - tx.state = Broadcasted + tx.state = utils.Broadcasted // save to the broadcasted map since transaction was just broadcasted c.broadcastedProcessedTxs[tx.id] = tx return "", nil @@ -273,7 +275,7 @@ func (c *pendingTxContext) OnProcessed(sig solana.Signature) (string, error) { return ErrTransactionNotFound } // Check if tranasction already in processed state - if tx.state == Processed { + if tx.state == utils.Processed { return ErrAlreadyInExpectedState } return nil @@ -293,7 +295,7 @@ func (c *pendingTxContext) OnProcessed(sig solana.Signature) (string, error) { return id, ErrTransactionNotFound } // update tx state to Processed - tx.state = Processed + tx.state = utils.Processed // save updated tx back to the broadcasted map c.broadcastedProcessedTxs[id] = tx return id, nil @@ -308,7 +310,7 @@ func (c *pendingTxContext) OnConfirmed(sig solana.Signature) (string, error) { return ErrSigDoesNotExist } // Check if transaction already in confirmed state - if tx, exists := c.confirmedTxs[id]; exists && tx.state == Confirmed { + if tx, exists := c.confirmedTxs[id]; exists && tx.state == utils.Confirmed { return ErrAlreadyInExpectedState } // Transactions should only move to confirmed from broadcasted/processed @@ -337,7 +339,7 @@ func (c *pendingTxContext) OnConfirmed(sig solana.Signature) (string, error) { delete(c.cancelBy, id) } // update tx state to Confirmed - tx.state = Confirmed + tx.state = utils.Confirmed // move tx to confirmed map c.confirmedTxs[id] = tx // remove tx from broadcasted map @@ -401,7 +403,7 @@ func (c *pendingTxContext) OnFinalized(sig solana.Signature, retentionTimeout ti return id, nil } finalizedTx := finishedTx{ - state: Finalized, + state: utils.Finalized, retentionTs: time.Now().Add(retentionTimeout), } // move transaction from confirmed to finalized map @@ -410,7 +412,7 @@ func (c *pendingTxContext) OnFinalized(sig solana.Signature, retentionTimeout ti }) } -func (c *pendingTxContext) OnPrebroadcastError(id string, retentionTimeout time.Duration, txState TxState, _ TxErrType) error { +func (c *pendingTxContext) OnPrebroadcastError(id string, retentionTimeout time.Duration, txState utils.TxState, _ TxErrType) error { // nothing to do if retention timeout is 0 since transaction is not stored yet. if retentionTimeout == 0 { return nil @@ -452,7 +454,7 @@ func (c *pendingTxContext) OnPrebroadcastError(id string, retentionTimeout time. return err } -func (c *pendingTxContext) OnError(sig solana.Signature, retentionTimeout time.Duration, txState TxState, _ TxErrType) (string, error) { +func (c *pendingTxContext) OnError(sig solana.Signature, retentionTimeout time.Duration, txState utils.TxState, _ TxErrType) (string, error) { err := c.withReadLock(func() error { id, sigExists := c.sigToID[sig] if !sigExists { @@ -517,7 +519,7 @@ func (c *pendingTxContext) OnError(sig solana.Signature, retentionTimeout time.D }) } -func (c *pendingTxContext) GetTxState(id string) (TxState, error) { +func (c *pendingTxContext) GetTxState(id string) (utils.TxState, error) { c.lock.RLock() defer c.lock.RUnlock() if tx, exists := c.broadcastedProcessedTxs[id]; exists { @@ -529,7 +531,7 @@ func (c *pendingTxContext) GetTxState(id string) (TxState, error) { if tx, exists := c.finalizedErroredTxs[id]; exists { return tx.state, nil } - return NotFound, fmt.Errorf("failed to find transaction for id: %s", id) + return utils.NotFound, fmt.Errorf("failed to find transaction for id: %s", id) } // TrimFinalizedErroredTxs deletes transactions from the finalized/errored map and the allTxs map after the retention period has passed @@ -644,7 +646,7 @@ func (c *pendingTxContextWithProm) OnFinalized(sig solana.Signature, retentionTi return id, err } -func (c *pendingTxContextWithProm) OnError(sig solana.Signature, retentionTimeout time.Duration, txState TxState, errType TxErrType) (string, error) { +func (c *pendingTxContextWithProm) OnError(sig solana.Signature, retentionTimeout time.Duration, txState utils.TxState, errType TxErrType) (string, error) { id, err := c.pendingTx.OnError(sig, retentionTimeout, txState, errType) // err indicates transaction not found so may already be removed if err == nil { incrementErrorMetrics(errType, c.chainID) @@ -652,7 +654,7 @@ func (c *pendingTxContextWithProm) OnError(sig solana.Signature, retentionTimeou return id, err } -func (c *pendingTxContextWithProm) OnPrebroadcastError(id string, retentionTimeout time.Duration, txState TxState, errType TxErrType) error { +func (c *pendingTxContextWithProm) OnPrebroadcastError(id string, retentionTimeout time.Duration, txState utils.TxState, errType TxErrType) error { err := c.pendingTx.OnPrebroadcastError(id, retentionTimeout, txState, errType) // err indicates transaction not found so may already be removed if err == nil { incrementErrorMetrics(errType, c.chainID) @@ -679,7 +681,7 @@ func incrementErrorMetrics(errType TxErrType, chainID string) { promSolTxmErrorTxs.WithLabelValues(chainID).Inc() } -func (c *pendingTxContextWithProm) GetTxState(id string) (TxState, error) { +func (c *pendingTxContextWithProm) GetTxState(id string) (utils.TxState, error) { return c.pendingTx.GetTxState(id) } diff --git a/pkg/solana/txm/pendingtx_test.go b/pkg/solana/txm/pendingtx_test.go index a79f9f7aa..ea8e65162 100644 --- a/pkg/solana/txm/pendingtx_test.go +++ b/pkg/solana/txm/pendingtx_test.go @@ -13,6 +13,8 @@ import ( "github.com/stretchr/testify/require" "github.com/smartcontractkit/chainlink-common/pkg/utils/tests" + + "github.com/smartcontractkit/chainlink-solana/pkg/solana/txm/utils" ) func TestPendingTxContext_add_remove_multiple(t *testing.T) { @@ -250,7 +252,7 @@ func TestPendingTxContext_on_broadcasted_processed(t *testing.T) { require.Equal(t, sig, tx.signatures[0]) // Check status is Processed - require.Equal(t, Processed, tx.state) + require.Equal(t, utils.Processed, tx.state) // Check it does not exist in confirmed map _, exists = txs.confirmedTxs[msg.id] @@ -321,7 +323,7 @@ func TestPendingTxContext_on_broadcasted_processed(t *testing.T) { require.NoError(t, err) // Transition to errored state - id, err := txs.OnError(sig, retentionTimeout, Errored, 0) + id, err := txs.OnError(sig, retentionTimeout, utils.Errored, 0) require.NoError(t, err) require.Equal(t, msg.id, id) @@ -389,7 +391,7 @@ func TestPendingTxContext_on_confirmed(t *testing.T) { require.Equal(t, sig, tx.signatures[0]) // Check status is Confirmed - require.Equal(t, Confirmed, tx.state) + require.Equal(t, utils.Confirmed, tx.state) // Check it does not exist in finalized map _, exists = txs.finalizedErroredTxs[msg.id] @@ -433,7 +435,7 @@ func TestPendingTxContext_on_confirmed(t *testing.T) { require.NoError(t, err) // Transition to errored state - id, err := txs.OnError(sig, retentionTimeout, Errored, 0) + id, err := txs.OnError(sig, retentionTimeout, utils.Errored, 0) require.NoError(t, err) require.Equal(t, msg.id, id) @@ -503,7 +505,7 @@ func TestPendingTxContext_on_finalized(t *testing.T) { require.True(t, exists) // Check status is Finalized - require.Equal(t, Finalized, tx.state) + require.Equal(t, utils.Finalized, tx.state) // Check sigs do no exist in signature map _, exists = txs.sigToID[sig1] @@ -553,7 +555,7 @@ func TestPendingTxContext_on_finalized(t *testing.T) { require.True(t, exists) // Check status is Finalized - require.Equal(t, Finalized, tx.state) + require.Equal(t, utils.Finalized, tx.state) // Check sigs do no exist in signature map _, exists = txs.sigToID[sig1] @@ -611,7 +613,7 @@ func TestPendingTxContext_on_finalized(t *testing.T) { require.NoError(t, err) // Transition to errored state - id, err := txs.OnError(sig, retentionTimeout, Errored, 0) + id, err := txs.OnError(sig, retentionTimeout, utils.Errored, 0) require.NoError(t, err) require.Equal(t, msg.id, id) @@ -636,7 +638,7 @@ func TestPendingTxContext_on_error(t *testing.T) { require.NoError(t, err) // Transition to errored state - id, err := txs.OnError(sig, retentionTimeout, Errored, 0) + id, err := txs.OnError(sig, retentionTimeout, utils.Errored, 0) require.NoError(t, err) require.Equal(t, msg.id, id) @@ -653,7 +655,7 @@ func TestPendingTxContext_on_error(t *testing.T) { require.True(t, exists) // Check status is Finalized - require.Equal(t, Errored, tx.state) + require.Equal(t, utils.Errored, tx.state) // Check sigs do no exist in signature map _, exists = txs.sigToID[sig] @@ -674,7 +676,7 @@ func TestPendingTxContext_on_error(t *testing.T) { require.Equal(t, msg.id, id) // Transition to errored state - id, err = txs.OnError(sig, retentionTimeout, Errored, 0) + id, err = txs.OnError(sig, retentionTimeout, utils.Errored, 0) require.NoError(t, err) require.Equal(t, msg.id, id) @@ -691,7 +693,7 @@ func TestPendingTxContext_on_error(t *testing.T) { require.True(t, exists) // Check status is Finalized - require.Equal(t, Errored, tx.state) + require.Equal(t, utils.Errored, tx.state) // Check sigs do no exist in signature map _, exists = txs.sigToID[sig] @@ -707,7 +709,7 @@ func TestPendingTxContext_on_error(t *testing.T) { require.NoError(t, err) // Transition to fatally errored state - id, err := txs.OnError(sig, retentionTimeout, FatallyErrored, 0) + id, err := txs.OnError(sig, retentionTimeout, utils.FatallyErrored, 0) require.NoError(t, err) require.Equal(t, msg.id, id) @@ -720,7 +722,7 @@ func TestPendingTxContext_on_error(t *testing.T) { require.True(t, exists) // Check status is Errored - require.Equal(t, FatallyErrored, tx.state) + require.Equal(t, utils.FatallyErrored, tx.state) // Check sigs do no exist in signature map _, exists = txs.sigToID[sig] @@ -741,7 +743,7 @@ func TestPendingTxContext_on_error(t *testing.T) { require.Equal(t, msg.id, id) // Transition to errored state - id, err = txs.OnError(sig, 0*time.Second, Errored, 0) + id, err = txs.OnError(sig, 0*time.Second, utils.Errored, 0) require.NoError(t, err) require.Equal(t, msg.id, id) @@ -776,7 +778,7 @@ func TestPendingTxContext_on_error(t *testing.T) { require.Equal(t, msg.id, id) // Transition back to confirmed state - id, err = txs.OnError(sig, retentionTimeout, Errored, 0) + id, err = txs.OnError(sig, retentionTimeout, utils.Errored, 0) require.Error(t, err) require.Equal(t, "", id) }) @@ -792,7 +794,7 @@ func TestPendingTxContext_on_prebroadcast_error(t *testing.T) { // Create new transaction msg := pendingTx{id: uuid.NewString()} // Transition to errored state - err := txs.OnPrebroadcastError(msg.id, retentionTimeout, Errored, 0) + err := txs.OnPrebroadcastError(msg.id, retentionTimeout, utils.Errored, 0) require.NoError(t, err) // Check it exists in errored map @@ -800,7 +802,7 @@ func TestPendingTxContext_on_prebroadcast_error(t *testing.T) { require.True(t, exists) // Check status is Errored - require.Equal(t, Errored, tx.state) + require.Equal(t, utils.Errored, tx.state) }) t.Run("successfully adds transaction with fatally errored state", func(t *testing.T) { @@ -808,7 +810,7 @@ func TestPendingTxContext_on_prebroadcast_error(t *testing.T) { msg := pendingTx{id: uuid.NewString()} // Transition to fatally errored state - err := txs.OnPrebroadcastError(msg.id, retentionTimeout, FatallyErrored, 0) + err := txs.OnPrebroadcastError(msg.id, retentionTimeout, utils.FatallyErrored, 0) require.NoError(t, err) // Check it exists in errored map @@ -816,7 +818,7 @@ func TestPendingTxContext_on_prebroadcast_error(t *testing.T) { require.True(t, exists) // Check status is Errored - require.Equal(t, FatallyErrored, tx.state) + require.Equal(t, utils.FatallyErrored, tx.state) }) t.Run("fails to add transaction to errored map if id exists in another map already", func(t *testing.T) { @@ -829,7 +831,7 @@ func TestPendingTxContext_on_prebroadcast_error(t *testing.T) { require.NoError(t, err) // Transition to errored state - err = txs.OnPrebroadcastError(msg.id, retentionTimeout, FatallyErrored, 0) + err = txs.OnPrebroadcastError(msg.id, retentionTimeout, utils.FatallyErrored, 0) require.ErrorIs(t, err, ErrIDAlreadyExists) }) @@ -837,11 +839,11 @@ func TestPendingTxContext_on_prebroadcast_error(t *testing.T) { txID := uuid.NewString() // Transition to errored state - err := txs.OnPrebroadcastError(txID, retentionTimeout, Errored, 0) + err := txs.OnPrebroadcastError(txID, retentionTimeout, utils.Errored, 0) require.NoError(t, err) // Transition back to errored state - err = txs.OnPrebroadcastError(txID, retentionTimeout, Errored, 0) + err = txs.OnPrebroadcastError(txID, retentionTimeout, utils.Errored, 0) require.ErrorIs(t, err, ErrAlreadyInExpectedState) }) } @@ -900,7 +902,7 @@ func TestPendingTxContext_remove(t *testing.T) { erroredMsg := pendingTx{id: erroredID} err = txs.New(erroredMsg, erroredSig, cancel) require.NoError(t, err) - id, err = txs.OnError(erroredSig, retentionTimeout, Errored, 0) + id, err = txs.OnError(erroredSig, retentionTimeout, utils.Errored, 0) require.NoError(t, err) require.Equal(t, erroredMsg.id, id) @@ -1097,7 +1099,7 @@ func TestGetTxState(t *testing.T) { err := txs.New(broadcastedMsg, broadcastedSig, cancel) require.NoError(t, err) - var state TxState + var state utils.TxState // Create new processed transaction processedMsg := pendingTx{id: uuid.NewString()} err = txs.New(processedMsg, processedSig, cancel) @@ -1108,7 +1110,7 @@ func TestGetTxState(t *testing.T) { // Check Processed state is returned state, err = txs.GetTxState(processedMsg.id) require.NoError(t, err) - require.Equal(t, Processed, state) + require.Equal(t, utils.Processed, state) // Create new confirmed transaction confirmedMsg := pendingTx{id: uuid.NewString()} @@ -1120,7 +1122,7 @@ func TestGetTxState(t *testing.T) { // Check Confirmed state is returned state, err = txs.GetTxState(confirmedMsg.id) require.NoError(t, err) - require.Equal(t, Confirmed, state) + require.Equal(t, utils.Confirmed, state) // Create new finalized transaction finalizedMsg := pendingTx{id: uuid.NewString()} @@ -1132,36 +1134,36 @@ func TestGetTxState(t *testing.T) { // Check Finalized state is returned state, err = txs.GetTxState(finalizedMsg.id) require.NoError(t, err) - require.Equal(t, Finalized, state) + require.Equal(t, utils.Finalized, state) // Create new errored transaction erroredMsg := pendingTx{id: uuid.NewString()} err = txs.New(erroredMsg, erroredSig, cancel) require.NoError(t, err) - id, err = txs.OnError(erroredSig, retentionTimeout, Errored, 0) + id, err = txs.OnError(erroredSig, retentionTimeout, utils.Errored, 0) require.NoError(t, err) require.Equal(t, erroredMsg.id, id) // Check Errored state is returned state, err = txs.GetTxState(erroredMsg.id) require.NoError(t, err) - require.Equal(t, Errored, state) + require.Equal(t, utils.Errored, state) // Create new fatally errored transaction fatallyErroredMsg := pendingTx{id: uuid.NewString()} err = txs.New(fatallyErroredMsg, fatallyErroredSig, cancel) require.NoError(t, err) - id, err = txs.OnError(fatallyErroredSig, retentionTimeout, FatallyErrored, 0) + id, err = txs.OnError(fatallyErroredSig, retentionTimeout, utils.FatallyErrored, 0) require.NoError(t, err) require.Equal(t, fatallyErroredMsg.id, id) // Check Errored state is returned state, err = txs.GetTxState(fatallyErroredMsg.id) require.NoError(t, err) - require.Equal(t, FatallyErrored, state) + require.Equal(t, utils.FatallyErrored, state) // Check NotFound state is returned if unknown id provided state, err = txs.GetTxState("unknown id") require.Error(t, err) - require.Equal(t, NotFound, state) + require.Equal(t, utils.NotFound, state) } func randomSignature(t *testing.T) solana.Signature { diff --git a/pkg/solana/txm/txm.go b/pkg/solana/txm/txm.go index 3e169d88a..e9a099f9c 100644 --- a/pkg/solana/txm/txm.go +++ b/pkg/solana/txm/txm.go @@ -25,6 +25,7 @@ import ( "github.com/smartcontractkit/chainlink-solana/pkg/solana/config" "github.com/smartcontractkit/chainlink-solana/pkg/solana/fees" "github.com/smartcontractkit/chainlink-solana/pkg/solana/internal" + txmutils "github.com/smartcontractkit/chainlink-solana/pkg/solana/txm/utils" ) const ( @@ -36,8 +37,6 @@ const ( MaxComputeUnitLimit = 1_400_000 // max compute unit limit a transaction can have ) -var _ services.Service = (*Txm)(nil) - type SimpleKeystore interface { Sign(ctx context.Context, account string, data []byte) (signature []byte, err error) Accounts(ctx context.Context) (accounts []string, err error) @@ -45,6 +44,14 @@ type SimpleKeystore interface { var _ loop.Keystore = (SimpleKeystore)(nil) +type TxManager interface { + services.Service + Enqueue(ctx context.Context, accountID string, tx *solanaGo.Transaction, txID *string, txCfgs ...txmutils.SetTxConfig) error + GetTransactionStatus(ctx context.Context, transactionID string) (commontypes.TransactionStatus, error) +} + +var _ TxManager = (*Txm)(nil) + // Txm manages transactions for the solana blockchain. // simple implementation with no persistently stored txs type Txm struct { @@ -64,19 +71,6 @@ type Txm struct { sendTx func(ctx context.Context, tx *solanaGo.Transaction) (solanaGo.Signature, error) } -type TxConfig struct { - Timeout time.Duration // transaction broadcast timeout - - // compute unit price config - FeeBumpPeriod time.Duration // how often to bump fee - BaseComputeUnitPrice uint64 // starting price - ComputeUnitPriceMin uint64 // min price - ComputeUnitPriceMax uint64 // max price - - EstimateComputeUnitLimit bool // enable compute limit estimations using simulation - ComputeUnitLimit uint32 // compute unit limit -} - // NewTxm creates a txm. Uses simulation so should only be used to send txes to trusted contracts i.e. OCR. func NewTxm(chainID string, client internal.Loader[client.ReaderWriter], sendTx func(ctx context.Context, tx *solanaGo.Transaction) (solanaGo.Signature, error), @@ -744,15 +738,15 @@ func (txm *Txm) GetTransactionStatus(ctx context.Context, transactionID string) } switch state { - case Broadcasted: + case txmutils.Broadcasted: return commontypes.Pending, nil - case Processed, Confirmed: + case txmutils.Processed, txmutils.Confirmed: return commontypes.Unconfirmed, nil - case Finalized: + case txmutils.Finalized: return commontypes.Finalized, nil - case Errored: + case txmutils.Errored: return commontypes.Failed, nil - case FatallyErrored: + case txmutils.FatallyErrored: return commontypes.Fatal, nil default: return commontypes.Unknown, fmt.Errorf("found unknown transaction state: %s", state.String()) @@ -915,7 +909,7 @@ func (txm *Txm) ProcessError(sig solanaGo.Signature, resErr interface{}, simulat errType = TxFailSimOther } txm.lggr.Errorw("unrecognized error", logValues...) - return Errored, errType + return txmutils.Errored, errType } } return @@ -938,8 +932,8 @@ func (txm *Txm) Name() string { return txm.lggr.Name() } func (txm *Txm) HealthReport() map[string]error { return map[string]error{txm.Name(): txm.Healthy()} } -func (txm *Txm) defaultTxConfig() TxConfig { - return TxConfig{ +func (txm *Txm) defaultTxConfig() txmutils.TxConfig { + return txmutils.TxConfig{ Timeout: txm.cfg.TxRetryTimeout(), FeeBumpPeriod: txm.cfg.FeeBumpPeriod(), BaseComputeUnitPrice: txm.fee.BaseComputeUnitPrice(), diff --git a/pkg/solana/txm/txm_internal_test.go b/pkg/solana/txm/txm_internal_test.go index 13c861362..740c2b500 100644 --- a/pkg/solana/txm/txm_internal_test.go +++ b/pkg/solana/txm/txm_internal_test.go @@ -24,6 +24,7 @@ import ( "github.com/smartcontractkit/chainlink-solana/pkg/solana/config" "github.com/smartcontractkit/chainlink-solana/pkg/solana/fees" keyMocks "github.com/smartcontractkit/chainlink-solana/pkg/solana/txm/mocks" + txmutils "github.com/smartcontractkit/chainlink-solana/pkg/solana/txm/utils" relayconfig "github.com/smartcontractkit/chainlink-common/pkg/config" "github.com/smartcontractkit/chainlink-common/pkg/logger" diff --git a/pkg/solana/txm/utils.go b/pkg/solana/txm/utils/utils.go similarity index 82% rename from pkg/solana/txm/utils.go rename to pkg/solana/txm/utils/utils.go index fef260e3d..7f3ffb9e2 100644 --- a/pkg/solana/txm/utils.go +++ b/pkg/solana/txm/utils/utils.go @@ -1,4 +1,4 @@ -package txm +package utils import ( "errors" @@ -111,39 +111,39 @@ func convertStatus(res *rpc.SignatureStatusesResult) TxState { return NotFound } -type signatureList struct { +type SignatureList struct { sigs []solana.Signature lock sync.RWMutex wg []*sync.WaitGroup } // internal function that should be called using the proper lock -func (s *signatureList) get(index int) (sig solana.Signature, err error) { +func (s *SignatureList) get(index int) (sig solana.Signature, err error) { if index >= len(s.sigs) { return sig, errors.New("invalid index") } return s.sigs[index], nil } -func (s *signatureList) Get(index int) (sig solana.Signature, err error) { +func (s *SignatureList) Get(index int) (sig solana.Signature, err error) { s.lock.RLock() defer s.lock.RUnlock() return s.get(index) } -func (s *signatureList) List() []solana.Signature { +func (s *SignatureList) List() []solana.Signature { s.lock.RLock() defer s.lock.RUnlock() return s.sigs } -func (s *signatureList) Length() int { +func (s *SignatureList) Length() int { s.lock.RLock() defer s.lock.RUnlock() return len(s.sigs) } -func (s *signatureList) Allocate() (index int) { +func (s *SignatureList) Allocate() (index int) { s.lock.Lock() defer s.lock.Unlock() @@ -156,7 +156,7 @@ func (s *signatureList) Allocate() (index int) { return len(s.sigs) - 1 } -func (s *signatureList) Set(index int, sig solana.Signature) error { +func (s *SignatureList) Set(index int, sig solana.Signature) error { s.lock.Lock() defer s.lock.Unlock() @@ -174,7 +174,7 @@ func (s *signatureList) Set(index int, sig solana.Signature) error { return nil } -func (s *signatureList) Wait(index int) { +func (s *SignatureList) Wait(index int) { wg := &sync.WaitGroup{} s.lock.RLock() if index < len(s.wg) { @@ -185,6 +185,19 @@ func (s *signatureList) Wait(index int) { wg.Wait() } +type TxConfig struct { + Timeout time.Duration // transaction broadcast timeout + + // compute unit price config + FeeBumpPeriod time.Duration // how often to bump fee + BaseComputeUnitPrice uint64 // starting price + ComputeUnitPriceMin uint64 // min price + ComputeUnitPriceMax uint64 // max price + + EstimateComputeUnitLimit bool // enable compute limit estimations using simulation + ComputeUnitLimit uint32 // compute unit limit +} + type SetTxConfig func(*TxConfig) func SetTimeout(t time.Duration) SetTxConfig { diff --git a/pkg/solana/txm/utils_test.go b/pkg/solana/txm/utils/utils_test.go similarity index 98% rename from pkg/solana/txm/utils_test.go rename to pkg/solana/txm/utils/utils_test.go index f4ac868ff..676f04202 100644 --- a/pkg/solana/txm/utils_test.go +++ b/pkg/solana/txm/utils/utils_test.go @@ -1,4 +1,4 @@ -package txm +package utils import ( "sync" @@ -42,7 +42,7 @@ func TestSortSignaturesAndResults(t *testing.T) { } func TestSignatureList_AllocateWaitSet(t *testing.T) { - sigs := signatureList{} + sigs := SignatureList{} assert.Equal(t, 0, sigs.Length()) // can't set without pre-allocating diff --git a/pkg/solana/utils/utils.go b/pkg/solana/utils/utils.go index 0c772065b..3353d40b3 100644 --- a/pkg/solana/utils/utils.go +++ b/pkg/solana/utils/utils.go @@ -13,9 +13,10 @@ import ( "github.com/gagliardetto/solana-go" "github.com/gagliardetto/solana-go/rpc" "github.com/pelletier/go-toml/v2" + "github.com/stretchr/testify/require" + "github.com/smartcontractkit/chainlink-solana/pkg/solana/client" "github.com/smartcontractkit/chainlink-solana/pkg/solana/internal" - "github.com/test-go/testify/require" ) var ( diff --git a/pkg/solana/utils/utils_test.go b/pkg/solana/utils/utils_test.go index 15a3e47d8..0f41f80c9 100644 --- a/pkg/solana/utils/utils_test.go +++ b/pkg/solana/utils/utils_test.go @@ -3,8 +3,9 @@ package utils_test import ( "testing" - "github.com/smartcontractkit/chainlink-solana/pkg/solana/utils" "github.com/stretchr/testify/assert" + + "github.com/smartcontractkit/chainlink-solana/pkg/solana/utils" ) func TestLamportsToSol(t *testing.T) { From 59392e8bf342b1a68b6ca6c123302b0737a0b4ac Mon Sep 17 00:00:00 2001 From: Silas Lenihan Date: Wed, 11 Dec 2024 16:58:25 -0500 Subject: [PATCH 24/29] merged with develop --- gotest.log | 92 +++++++++++++++++++++++++++++++++ pkg/solana/txm/txm.go | 20 +++---- pkg/solana/txm/txm_unit_test.go | 17 +++--- pkg/solana/utils/utils.go | 2 + 4 files changed, 113 insertions(+), 18 deletions(-) create mode 100644 gotest.log diff --git a/gotest.log b/gotest.log new file mode 100644 index 000000000..41b099eb4 --- /dev/null +++ b/gotest.log @@ -0,0 +1,92 @@ +📦 github.com/smartcontractkit/chainlink-solana/pkg/solana/chainwriter +exit status 1 + ❌ TestLookupTables (30.04s) + ports.go:37: found open port: 41544 + ports.go:37: found open port: 39418 + utils.go:215: API server not ready yet (attempt 1) + utils.go:215: Error from API server: rpc call getHealth() on http://127.0.0.1:41544: Post "http://127.0.0.1:41544": dial tcp 127.0.0.1:41544: connect: connection refused + utils.go:215: API server not ready yet (attempt 2) + utils.go:215: Error from API server: rpc call getHealth() on http://127.0.0.1:41544: Post "http://127.0.0.1:41544": dial tcp 127.0.0.1:41544: connect: connection refused + utils.go:215: API server not ready yet (attempt 3) + utils.go:215: Error from API server: rpc call getHealth() on http://127.0.0.1:41544: Post "http://127.0.0.1:41544": dial tcp 127.0.0.1:41544: connect: connection refused + utils.go:215: API server not ready yet (attempt 4) + utils.go:215: Error from API server: rpc call getHealth() on http://127.0.0.1:41544: Post "http://127.0.0.1:41544": dial tcp 127.0.0.1:41544: connect: connection refused + utils.go:215: API server not ready yet (attempt 5) + utils.go:215: Error from API server: rpc call getHealth() on http://127.0.0.1:41544: Post "http://127.0.0.1:41544": dial tcp 127.0.0.1:41544: connect: connection refused + utils.go:215: API server not ready yet (attempt 6) + utils.go:215: Error from API server: rpc call getHealth() on http://127.0.0.1:41544: Post "http://127.0.0.1:41544": dial tcp 127.0.0.1:41544: connect: connection refused + utils.go:215: API server not ready yet (attempt 7) + utils.go:215: Error from API server: rpc call getHealth() on http://127.0.0.1:41544: Post "http://127.0.0.1:41544": dial tcp 127.0.0.1:41544: connect: connection refused + utils.go:215: API server not ready yet (attempt 8) + utils.go:215: Error from API server: rpc call getHealth() on http://127.0.0.1:41544: Post "http://127.0.0.1:41544": dial tcp 127.0.0.1:41544: connect: connection refused + utils.go:215: API server not ready yet (attempt 9) + utils.go:215: Error from API server: rpc call getHealth() on http://127.0.0.1:41544: Post "http://127.0.0.1:41544": dial tcp 127.0.0.1:41544: connect: connection refused + utils.go:215: API server not ready yet (attempt 10) + utils.go:215: Error from API server: rpc call getHealth() on http://127.0.0.1:41544: Post "http://127.0.0.1:41544": dial tcp 127.0.0.1:41544: connect: connection refused + utils.go:215: API server not ready yet (attempt 11) + utils.go:215: Error from API server: rpc call getHealth() on http://127.0.0.1:41544: Post "http://127.0.0.1:41544": dial tcp 127.0.0.1:41544: connect: connection refused + utils.go:215: API server not ready yet (attempt 12) + utils.go:215: Error from API server: rpc call getHealth() on http://127.0.0.1:41544: Post "http://127.0.0.1:41544": dial tcp 127.0.0.1:41544: connect: connection refused + utils.go:215: API server not ready yet (attempt 13) + utils.go:215: Error from API server: rpc call getHealth() on http://127.0.0.1:41544: Post "http://127.0.0.1:41544": dial tcp 127.0.0.1:41544: connect: connection refused + utils.go:215: API server not ready yet (attempt 14) + utils.go:215: Error from API server: rpc call getHealth() on http://127.0.0.1:41544: Post "http://127.0.0.1:41544": dial tcp 127.0.0.1:41544: connect: connection refused + utils.go:215: API server not ready yet (attempt 15) + utils.go:215: Error from API server: rpc call getHealth() on http://127.0.0.1:41544: Post "http://127.0.0.1:41544": dial tcp 127.0.0.1:41544: connect: connection refused + utils.go:215: API server not ready yet (attempt 16) + utils.go:215: Error from API server: rpc call getHealth() on http://127.0.0.1:41544: Post "http://127.0.0.1:41544": dial tcp 127.0.0.1:41544: connect: connection refused + utils.go:215: API server not ready yet (attempt 17) + utils.go:215: Error from API server: rpc call getHealth() on http://127.0.0.1:41544: Post "http://127.0.0.1:41544": dial tcp 127.0.0.1:41544: connect: connection refused + utils.go:215: API server not ready yet (attempt 18) + utils.go:215: Error from API server: rpc call getHealth() on http://127.0.0.1:41544: Post "http://127.0.0.1:41544": dial tcp 127.0.0.1:41544: connect: connection refused + utils.go:215: API server not ready yet (attempt 19) + utils.go:215: Error from API server: rpc call getHealth() on http://127.0.0.1:41544: Post "http://127.0.0.1:41544": dial tcp 127.0.0.1:41544: connect: connection refused + utils.go:215: API server not ready yet (attempt 20) + utils.go:215: Error from API server: rpc call getHealth() on http://127.0.0.1:41544: Post "http://127.0.0.1:41544": dial tcp 127.0.0.1:41544: connect: connection refused + utils.go:215: API server not ready yet (attempt 21) + utils.go:215: Error from API server: rpc call getHealth() on http://127.0.0.1:41544: Post "http://127.0.0.1:41544": dial tcp 127.0.0.1:41544: connect: connection refused + utils.go:215: API server not ready yet (attempt 22) + utils.go:215: Error from API server: rpc call getHealth() on http://127.0.0.1:41544: Post "http://127.0.0.1:41544": dial tcp 127.0.0.1:41544: connect: connection refused + utils.go:215: API server not ready yet (attempt 23) + utils.go:215: Error from API server: rpc call getHealth() on http://127.0.0.1:41544: Post "http://127.0.0.1:41544": dial tcp 127.0.0.1:41544: connect: connection refused + utils.go:215: API server not ready yet (attempt 24) + utils.go:215: Error from API server: rpc call getHealth() on http://127.0.0.1:41544: Post "http://127.0.0.1:41544": dial tcp 127.0.0.1:41544: connect: connection refused + utils.go:215: API server not ready yet (attempt 25) + utils.go:215: Error from API server: rpc call getHealth() on http://127.0.0.1:41544: Post "http://127.0.0.1:41544": dial tcp 127.0.0.1:41544: connect: connection refused + utils.go:215: API server not ready yet (attempt 26) + utils.go:215: Error from API server: rpc call getHealth() on http://127.0.0.1:41544: Post "http://127.0.0.1:41544": dial tcp 127.0.0.1:41544: connect: connection refused + utils.go:215: API server not ready yet (attempt 27) + utils.go:215: Error from API server: rpc call getHealth() on http://127.0.0.1:41544: Post "http://127.0.0.1:41544": dial tcp 127.0.0.1:41544: connect: connection refused + utils.go:215: API server not ready yet (attempt 28) + utils.go:215: Error from API server: rpc call getHealth() on http://127.0.0.1:41544: Post "http://127.0.0.1:41544": dial tcp 127.0.0.1:41544: connect: connection refused + utils.go:215: API server not ready yet (attempt 29) + utils.go:215: Error from API server: rpc call getHealth() on http://127.0.0.1:41544: Post "http://127.0.0.1:41544": dial tcp 127.0.0.1:41544: connect: connection refused + utils.go:215: API server not ready yet (attempt 30) + utils.go:215: Error from API server: rpc call getHealth() on http://127.0.0.1:41544: Post "http://127.0.0.1:41544": dial tcp 127.0.0.1:41544: connect: connection refused + utils.go:215: Cmd output: Error: program file does not exist: /Users/silaslenihan/Desktop/repos/chainlink-solana/contracts/target/deploy/contract-reader-interface.so + + Cmd error: + utils.go:215: + Error Trace: /Users/silaslenihan/Desktop/repos/chainlink-solana/pkg/solana/client/test_helpers.go:78 + /Users/silaslenihan/Desktop/repos/chainlink-solana/pkg/solana/utils/utils.go:215 + /Users/silaslenihan/Desktop/repos/chainlink-solana/pkg/solana/chainwriter/lookups_test.go:279 + Error: Should be true + Test: TestLookupTables + test_helpers.go:55: + Error Trace: /Users/silaslenihan/Desktop/repos/chainlink-solana/pkg/solana/client/test_helpers.go:55 + /Users/silaslenihan/.asdf/installs/golang/1.23.3/go/src/testing/testing.go:1176 + /Users/silaslenihan/.asdf/installs/golang/1.23.3/go/src/testing/testing.go:1354 + /Users/silaslenihan/.asdf/installs/golang/1.23.3/go/src/testing/testing.go:1684 + /Users/silaslenihan/.asdf/installs/golang/1.23.3/go/src/runtime/panic.go:629 + /Users/silaslenihan/.asdf/installs/golang/1.23.3/go/src/testing/testing.go:1006 + /Users/silaslenihan/Desktop/repos/chainlink-solana/pkg/solana/client/test_helpers.go:78 + /Users/silaslenihan/Desktop/repos/chainlink-solana/pkg/solana/utils/utils.go:215 + /Users/silaslenihan/Desktop/repos/chainlink-solana/pkg/solana/chainwriter/lookups_test.go:279 + Error: "exit status 1" does not contain "signal: killed" + Test: TestLookupTables + Messages: exit status 1 + test_helpers.go:56: solana-test-validator + stdout: Error: program file does not exist: /Users/silaslenihan/Desktop/repos/chainlink-solana/contracts/target/deploy/contract-reader-interface.so + + stderr: + diff --git a/pkg/solana/txm/txm.go b/pkg/solana/txm/txm.go index e9a099f9c..8c69e1c30 100644 --- a/pkg/solana/txm/txm.go +++ b/pkg/solana/txm/txm.go @@ -860,7 +860,7 @@ func (txm *Txm) ProcessError(sig solanaGo.Signature, resErr interface{}, simulat if simulation { return txState, NoFailure } - return Errored, errType + return txmutils.Errored, errType // transaction is already processed in the chain case strings.Contains(errStr, "AlreadyProcessed"): txm.lggr.Debugw("AlreadyProcessed", logValues...) @@ -869,39 +869,39 @@ func (txm *Txm) ProcessError(sig solanaGo.Signature, resErr interface{}, simulat if simulation { return txState, NoFailure } - return Errored, errType + return txmutils.Errored, errType // transaction will encounter execution error/revert case strings.Contains(errStr, "InstructionError"): txm.lggr.Debugw("InstructionError", logValues...) - return FatallyErrored, errType + return txmutils.FatallyErrored, errType // transaction contains an invalid account reference case strings.Contains(errStr, "InvalidAccountIndex"): txm.lggr.Debugw("InvalidAccountIndex", logValues...) - return FatallyErrored, errType + return txmutils.FatallyErrored, errType // transaction loads a writable account that cannot be written case strings.Contains(errStr, "InvalidWritableAccount"): txm.lggr.Debugw("InvalidWritableAccount", logValues...) - return FatallyErrored, errType + return txmutils.FatallyErrored, errType // address lookup table not found case strings.Contains(errStr, "AddressLookupTableNotFound"): txm.lggr.Debugw("AddressLookupTableNotFound", logValues...) - return FatallyErrored, errType + return txmutils.FatallyErrored, errType // attempted to lookup addresses from an invalid account case strings.Contains(errStr, "InvalidAddressLookupTableData"): txm.lggr.Debugw("InvalidAddressLookupTableData", logValues...) - return FatallyErrored, errType + return txmutils.FatallyErrored, errType // address table lookup uses an invalid index case strings.Contains(errStr, "InvalidAddressLookupTableIndex"): txm.lggr.Debugw("InvalidAddressLookupTableIndex", logValues...) - return FatallyErrored, errType + return txmutils.FatallyErrored, errType // attempt to debit an account but found no record of a prior credit. case strings.Contains(errStr, "AccountNotFound"): txm.lggr.Debugw("AccountNotFound", logValues...) - return FatallyErrored, errType + return txmutils.FatallyErrored, errType // attempt to load a program that does not exist case strings.Contains(errStr, "ProgramAccountNotFound"): txm.lggr.Debugw("ProgramAccountNotFound", logValues...) - return FatallyErrored, errType + return txmutils.FatallyErrored, errType // unrecognized errors (indicates more concerning failures) default: // if simulating, return TxFailSimOther if error unknown diff --git a/pkg/solana/txm/txm_unit_test.go b/pkg/solana/txm/txm_unit_test.go index 7dfec6c57..e8dfcb584 100644 --- a/pkg/solana/txm/txm_unit_test.go +++ b/pkg/solana/txm/txm_unit_test.go @@ -18,6 +18,7 @@ import ( "github.com/smartcontractkit/chainlink-solana/pkg/solana/fees" solanatxm "github.com/smartcontractkit/chainlink-solana/pkg/solana/txm" keyMocks "github.com/smartcontractkit/chainlink-solana/pkg/solana/txm/mocks" + txmutils "github.com/smartcontractkit/chainlink-solana/pkg/solana/txm/utils" "github.com/smartcontractkit/chainlink-common/pkg/logger" "github.com/smartcontractkit/chainlink-common/pkg/utils" @@ -174,12 +175,12 @@ func TestTxm_ProcessError(t *testing.T) { // returns no failure if BlockhashNotFound encountered during simulation txState, errType := txm.ProcessError(solana.Signature{}, err, true) require.Equal(t, solanatxm.NoFailure, errType) - require.Equal(t, solanatxm.NotFound, txState) // default enum value + require.Equal(t, txmutils.NotFound, txState) // default enum value // returns error if BlockhashNotFound encountered during normal processing txState, errType = txm.ProcessError(solana.Signature{}, err, false) require.Equal(t, solanatxm.TxFailRevert, errType) - require.Equal(t, solanatxm.Errored, txState) // default enum value + require.Equal(t, txmutils.Errored, txState) // default enum value }) t.Run("process AlreadyProcessed error", func(t *testing.T) { t.Parallel() @@ -191,12 +192,12 @@ func TestTxm_ProcessError(t *testing.T) { // returns no failure if AlreadyProcessed encountered during simulation txState, errType := txm.ProcessError(solana.Signature{}, err, true) require.Equal(t, solanatxm.NoFailure, errType) - require.Equal(t, solanatxm.NotFound, txState) // default enum value + require.Equal(t, txmutils.NotFound, txState) // default enum value // returns error if AlreadyProcessed encountered during normal processing txState, errType = txm.ProcessError(solana.Signature{}, err, false) require.Equal(t, solanatxm.TxFailRevert, errType) - require.Equal(t, solanatxm.Errored, txState) // default enum value + require.Equal(t, txmutils.Errored, txState) // default enum value }) t.Run("process fatal error cases", func(t *testing.T) { t.Parallel() @@ -212,12 +213,12 @@ func TestTxm_ProcessError(t *testing.T) { // returns fatal error if InstructionError encountered during simulation txState, errType := txm.ProcessError(solana.Signature{}, err, true) require.Equal(t, solanatxm.TxFailSimRevert, errType) - require.Equal(t, solanatxm.FatallyErrored, txState) // default enum value + require.Equal(t, txmutils.FatallyErrored, txState) // default enum value // returns fatal error if InstructionError encountered during normal processing txState, errType = txm.ProcessError(solana.Signature{}, err, false) require.Equal(t, solanatxm.TxFailRevert, errType) - require.Equal(t, solanatxm.FatallyErrored, txState) // default enum value + require.Equal(t, txmutils.FatallyErrored, txState) // default enum value }) } }) @@ -231,12 +232,12 @@ func TestTxm_ProcessError(t *testing.T) { // returns fatal error if InstructionError encountered during simulation txState, errType := txm.ProcessError(solana.Signature{}, err, true) require.Equal(t, solanatxm.TxFailSimOther, errType) - require.Equal(t, solanatxm.Errored, txState) // default enum value + require.Equal(t, txmutils.Errored, txState) // default enum value // returns fatal error if InstructionError encountered during normal processing txState, errType = txm.ProcessError(solana.Signature{}, err, false) require.Equal(t, solanatxm.TxFailRevert, errType) - require.Equal(t, solanatxm.Errored, txState) // default enum value + require.Equal(t, txmutils.Errored, txState) // default enum value }) } diff --git a/pkg/solana/utils/utils.go b/pkg/solana/utils/utils.go index 3353d40b3..974ca6813 100644 --- a/pkg/solana/utils/utils.go +++ b/pkg/solana/utils/utils.go @@ -7,6 +7,7 @@ import ( "os" "path/filepath" "runtime" + "strings" "testing" "time" @@ -210,6 +211,7 @@ func SetupTestValidatorWithAnchorPrograms(t *testing.T, pathToAnchorConfig strin flags := []string{} for k, v := range anchorData.Programs.Localnet { + k = strings.Replace(k, "-", "_", -1) flags = append(flags, "--upgradeable-program", v, filepath.Join(ContractsDir, k+".so"), upgradeAuthority) } url, _ := client.SetupLocalSolNodeWithFlags(t, flags...) From a7d0a2a750301839aa1dd9d1ff194889f74e543e Mon Sep 17 00:00:00 2001 From: Silas Lenihan Date: Thu, 12 Dec 2024 16:24:27 -0500 Subject: [PATCH 25/29] Removed write_test and moved logic to contract_reader_interface test program --- contracts/Anchor.toml | 3 +- contracts/Cargo.lock | 7 - .../InitializeLookupTable.go | 174 ++ .../InitializeLookupTable_test.go | 32 + .../contract_reader_interface/accounts.go | 76 + .../contract_reader_interface/instructions.go | 7 + contracts/pnpm-lock.yaml | 2194 ++++++++--------- .../contract-reader-interface/src/lib.rs | 41 + contracts/programs/write_test/Cargo.toml | 19 - contracts/programs/write_test/Xargo.toml | 2 - contracts/programs/write_test/src/lib.rs | 51 - gotest.log | 92 - integration-tests/go.sum | 4 + .../relayinterface/chain_components_test.go | 29 +- .../relayinterface}/lookups_test.go | 46 +- pkg/solana/chainwriter/chain_writer.go | 31 +- pkg/solana/chainwriter/chain_writer_test.go | 47 +- pkg/solana/chainwriter/helpers.go | 18 +- pkg/solana/chainwriter/lookups.go | 1 - pkg/solana/client/test_helpers.go | 2 +- pkg/solana/utils/utils.go | 23 +- 21 files changed, 1407 insertions(+), 1492 deletions(-) create mode 100644 contracts/generated/contract_reader_interface/InitializeLookupTable.go create mode 100644 contracts/generated/contract_reader_interface/InitializeLookupTable_test.go delete mode 100644 contracts/programs/write_test/Cargo.toml delete mode 100644 contracts/programs/write_test/Xargo.toml delete mode 100644 contracts/programs/write_test/src/lib.rs delete mode 100644 gotest.log rename {pkg/solana/chainwriter => integration-tests/relayinterface}/lookups_test.go (92%) diff --git a/contracts/Anchor.toml b/contracts/Anchor.toml index 7d18084b0..30b788caa 100644 --- a/contracts/Anchor.toml +++ b/contracts/Anchor.toml @@ -30,5 +30,4 @@ access_controller = "9xi644bRR8birboDGdTiwBq3C7VEeR7VuamRYYXCubUW" contract-reader-interface = "6AfuXF6HapDUhQfE4nQG9C1SGtA1YjP3icaJyRfU4RyE" log-read-test = "J1zQwrBNBngz26jRPNWsUSZMHJwBwpkoDitXRV95LdK4" ocr_2 = "cjg3oHmg9uuPsP8D6g29NWvhySJkdYdAo9D25PRbKXJ" # need to rename the idl to satisfy anchor.js... -store = "HEvSKofvBgfaexv23kMabbYqxasxU3mQ4ibBMEmJWHny" -write_test = "39vbQVpEMtZtg3e6ZSE7nBSzmNZptmW45WnLkbqEe4TU" \ No newline at end of file +store = "HEvSKofvBgfaexv23kMabbYqxasxU3mQ4ibBMEmJWHny" \ No newline at end of file diff --git a/contracts/Cargo.lock b/contracts/Cargo.lock index ab6454e91..0209f1dbb 100644 --- a/contracts/Cargo.lock +++ b/contracts/Cargo.lock @@ -2680,13 +2680,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "write-test" -version = "0.1.0" -dependencies = [ - "anchor-lang", -] - [[package]] name = "zerocopy" version = "0.7.32" diff --git a/contracts/generated/contract_reader_interface/InitializeLookupTable.go b/contracts/generated/contract_reader_interface/InitializeLookupTable.go new file mode 100644 index 000000000..5b339f169 --- /dev/null +++ b/contracts/generated/contract_reader_interface/InitializeLookupTable.go @@ -0,0 +1,174 @@ +// Code generated by https://github.com/gagliardetto/anchor-go. DO NOT EDIT. + +package contract_reader_interface + +import ( + "errors" + ag_binary "github.com/gagliardetto/binary" + ag_solanago "github.com/gagliardetto/solana-go" + ag_format "github.com/gagliardetto/solana-go/text/format" + ag_treeout "github.com/gagliardetto/treeout" +) + +// InitializeLookupTable is the `initializeLookupTable` instruction. +type InitializeLookupTable struct { + LookupTable *ag_solanago.PublicKey + + // [0] = [WRITE] writeDataAccount + // ··········· PDA for LookupTableDataAccount, derived from seeds and created by the System Program + // + // [1] = [WRITE, SIGNER] admin + // ··········· Admin account that pays for PDA creation and signs the transaction + // + // [2] = [] systemProgram + // ··········· System Program required for PDA creation + ag_solanago.AccountMetaSlice `bin:"-" borsh_skip:"true"` +} + +// NewInitializeLookupTableInstructionBuilder creates a new `InitializeLookupTable` instruction builder. +func NewInitializeLookupTableInstructionBuilder() *InitializeLookupTable { + nd := &InitializeLookupTable{ + AccountMetaSlice: make(ag_solanago.AccountMetaSlice, 3), + } + return nd +} + +// SetLookupTable sets the "lookupTable" parameter. +func (inst *InitializeLookupTable) SetLookupTable(lookupTable ag_solanago.PublicKey) *InitializeLookupTable { + inst.LookupTable = &lookupTable + return inst +} + +// SetWriteDataAccountAccount sets the "writeDataAccount" account. +// PDA for LookupTableDataAccount, derived from seeds and created by the System Program +func (inst *InitializeLookupTable) SetWriteDataAccountAccount(writeDataAccount ag_solanago.PublicKey) *InitializeLookupTable { + inst.AccountMetaSlice[0] = ag_solanago.Meta(writeDataAccount).WRITE() + return inst +} + +// GetWriteDataAccountAccount gets the "writeDataAccount" account. +// PDA for LookupTableDataAccount, derived from seeds and created by the System Program +func (inst *InitializeLookupTable) GetWriteDataAccountAccount() *ag_solanago.AccountMeta { + return inst.AccountMetaSlice[0] +} + +// SetAdminAccount sets the "admin" account. +// Admin account that pays for PDA creation and signs the transaction +func (inst *InitializeLookupTable) SetAdminAccount(admin ag_solanago.PublicKey) *InitializeLookupTable { + inst.AccountMetaSlice[1] = ag_solanago.Meta(admin).WRITE().SIGNER() + return inst +} + +// GetAdminAccount gets the "admin" account. +// Admin account that pays for PDA creation and signs the transaction +func (inst *InitializeLookupTable) GetAdminAccount() *ag_solanago.AccountMeta { + return inst.AccountMetaSlice[1] +} + +// SetSystemProgramAccount sets the "systemProgram" account. +// System Program required for PDA creation +func (inst *InitializeLookupTable) SetSystemProgramAccount(systemProgram ag_solanago.PublicKey) *InitializeLookupTable { + inst.AccountMetaSlice[2] = ag_solanago.Meta(systemProgram) + return inst +} + +// GetSystemProgramAccount gets the "systemProgram" account. +// System Program required for PDA creation +func (inst *InitializeLookupTable) GetSystemProgramAccount() *ag_solanago.AccountMeta { + return inst.AccountMetaSlice[2] +} + +func (inst InitializeLookupTable) Build() *Instruction { + return &Instruction{BaseVariant: ag_binary.BaseVariant{ + Impl: inst, + TypeID: Instruction_InitializeLookupTable, + }} +} + +// ValidateAndBuild validates the instruction parameters and accounts; +// if there is a validation error, it returns the error. +// Otherwise, it builds and returns the instruction. +func (inst InitializeLookupTable) ValidateAndBuild() (*Instruction, error) { + if err := inst.Validate(); err != nil { + return nil, err + } + return inst.Build(), nil +} + +func (inst *InitializeLookupTable) Validate() error { + // Check whether all (required) parameters are set: + { + if inst.LookupTable == nil { + return errors.New("LookupTable parameter is not set") + } + } + + // Check whether all (required) accounts are set: + { + if inst.AccountMetaSlice[0] == nil { + return errors.New("accounts.WriteDataAccount is not set") + } + if inst.AccountMetaSlice[1] == nil { + return errors.New("accounts.Admin is not set") + } + if inst.AccountMetaSlice[2] == nil { + return errors.New("accounts.SystemProgram is not set") + } + } + return nil +} + +func (inst *InitializeLookupTable) EncodeToTree(parent ag_treeout.Branches) { + parent.Child(ag_format.Program(ProgramName, ProgramID)). + // + ParentFunc(func(programBranch ag_treeout.Branches) { + programBranch.Child(ag_format.Instruction("InitializeLookupTable")). + // + ParentFunc(func(instructionBranch ag_treeout.Branches) { + + // Parameters of the instruction: + instructionBranch.Child("Params[len=1]").ParentFunc(func(paramsBranch ag_treeout.Branches) { + paramsBranch.Child(ag_format.Param("LookupTable", *inst.LookupTable)) + }) + + // Accounts of the instruction: + instructionBranch.Child("Accounts[len=3]").ParentFunc(func(accountsBranch ag_treeout.Branches) { + accountsBranch.Child(ag_format.Meta(" writeData", inst.AccountMetaSlice[0])) + accountsBranch.Child(ag_format.Meta(" admin", inst.AccountMetaSlice[1])) + accountsBranch.Child(ag_format.Meta("systemProgram", inst.AccountMetaSlice[2])) + }) + }) + }) +} + +func (obj InitializeLookupTable) MarshalWithEncoder(encoder *ag_binary.Encoder) (err error) { + // Serialize `LookupTable` param: + err = encoder.Encode(obj.LookupTable) + if err != nil { + return err + } + return nil +} +func (obj *InitializeLookupTable) UnmarshalWithDecoder(decoder *ag_binary.Decoder) (err error) { + // Deserialize `LookupTable`: + err = decoder.Decode(&obj.LookupTable) + if err != nil { + return err + } + return nil +} + +// NewInitializeLookupTableInstruction declares a new InitializeLookupTable instruction with the provided parameters and accounts. +func NewInitializeLookupTableInstruction( + // Parameters: + lookupTable ag_solanago.PublicKey, + // Accounts: + writeDataAccount ag_solanago.PublicKey, + admin ag_solanago.PublicKey, + systemProgram ag_solanago.PublicKey) *InitializeLookupTable { + return NewInitializeLookupTableInstructionBuilder(). + SetLookupTable(lookupTable). + SetWriteDataAccountAccount(writeDataAccount). + SetAdminAccount(admin). + SetSystemProgramAccount(systemProgram) +} diff --git a/contracts/generated/contract_reader_interface/InitializeLookupTable_test.go b/contracts/generated/contract_reader_interface/InitializeLookupTable_test.go new file mode 100644 index 000000000..e30e2204d --- /dev/null +++ b/contracts/generated/contract_reader_interface/InitializeLookupTable_test.go @@ -0,0 +1,32 @@ +// Code generated by https://github.com/gagliardetto/anchor-go. DO NOT EDIT. + +package contract_reader_interface + +import ( + "bytes" + ag_gofuzz "github.com/gagliardetto/gofuzz" + ag_require "github.com/stretchr/testify/require" + "strconv" + "testing" +) + +func TestEncodeDecode_InitializeLookupTable(t *testing.T) { + fu := ag_gofuzz.New().NilChance(0) + for i := 0; i < 1; i++ { + t.Run("InitializeLookupTable"+strconv.Itoa(i), func(t *testing.T) { + { + params := new(InitializeLookupTable) + fu.Fuzz(params) + params.AccountMetaSlice = nil + buf := new(bytes.Buffer) + err := encodeT(*params, buf) + ag_require.NoError(t, err) + got := new(InitializeLookupTable) + err = decodeT(got, buf.Bytes()) + got.AccountMetaSlice = nil + ag_require.NoError(t, err) + ag_require.Equal(t, params, got) + } + }) + } +} diff --git a/contracts/generated/contract_reader_interface/accounts.go b/contracts/generated/contract_reader_interface/accounts.go index edf383de1..d02ccb874 100644 --- a/contracts/generated/contract_reader_interface/accounts.go +++ b/contracts/generated/contract_reader_interface/accounts.go @@ -5,8 +5,84 @@ package contract_reader_interface import ( "fmt" ag_binary "github.com/gagliardetto/binary" + ag_solanago "github.com/gagliardetto/solana-go" ) +type LookupTableDataAccount struct { + Version uint8 + Administrator ag_solanago.PublicKey + PendingAdministrator ag_solanago.PublicKey + LookupTable ag_solanago.PublicKey +} + +var LookupTableDataAccountDiscriminator = [8]byte{220, 119, 44, 40, 237, 41, 223, 7} + +func (obj LookupTableDataAccount) MarshalWithEncoder(encoder *ag_binary.Encoder) (err error) { + // Write account discriminator: + err = encoder.WriteBytes(LookupTableDataAccountDiscriminator[:], false) + if err != nil { + return err + } + // Serialize `Version` param: + err = encoder.Encode(obj.Version) + if err != nil { + return err + } + // Serialize `Administrator` param: + err = encoder.Encode(obj.Administrator) + if err != nil { + return err + } + // Serialize `PendingAdministrator` param: + err = encoder.Encode(obj.PendingAdministrator) + if err != nil { + return err + } + // Serialize `LookupTable` param: + err = encoder.Encode(obj.LookupTable) + if err != nil { + return err + } + return nil +} + +func (obj *LookupTableDataAccount) UnmarshalWithDecoder(decoder *ag_binary.Decoder) (err error) { + // Read and check account discriminator: + { + discriminator, err := decoder.ReadTypeID() + if err != nil { + return err + } + if !discriminator.Equal(LookupTableDataAccountDiscriminator[:]) { + return fmt.Errorf( + "wrong discriminator: wanted %s, got %s", + "[220 119 44 40 237 41 223 7]", + fmt.Sprint(discriminator[:])) + } + } + // Deserialize `Version`: + err = decoder.Decode(&obj.Version) + if err != nil { + return err + } + // Deserialize `Administrator`: + err = decoder.Decode(&obj.Administrator) + if err != nil { + return err + } + // Deserialize `PendingAdministrator`: + err = decoder.Decode(&obj.PendingAdministrator) + if err != nil { + return err + } + // Deserialize `LookupTable`: + err = decoder.Decode(&obj.LookupTable) + if err != nil { + return err + } + return nil +} + type DataAccount struct { Idx uint64 Bump uint8 diff --git a/contracts/generated/contract_reader_interface/instructions.go b/contracts/generated/contract_reader_interface/instructions.go index 7eee5c9a6..c86960a41 100644 --- a/contracts/generated/contract_reader_interface/instructions.go +++ b/contracts/generated/contract_reader_interface/instructions.go @@ -29,6 +29,8 @@ func init() { var ( Instruction_Initialize = ag_binary.TypeID([8]byte{175, 175, 109, 31, 13, 152, 155, 237}) + + Instruction_InitializeLookupTable = ag_binary.TypeID([8]byte{149, 120, 10, 249, 212, 185, 177, 216}) ) // InstructionIDToName returns the name of the instruction given its ID. @@ -36,6 +38,8 @@ func InstructionIDToName(id ag_binary.TypeID) string { switch id { case Instruction_Initialize: return "Initialize" + case Instruction_InitializeLookupTable: + return "InitializeLookupTable" default: return "" } @@ -59,6 +63,9 @@ var InstructionImplDef = ag_binary.NewVariantDefinition( { "initialize", (*Initialize)(nil), }, + { + "initialize_lookup_table", (*InitializeLookupTable)(nil), + }, }, ) diff --git a/contracts/pnpm-lock.yaml b/contracts/pnpm-lock.yaml index b7cec1551..5a742745d 100644 --- a/contracts/pnpm-lock.yaml +++ b/contracts/pnpm-lock.yaml @@ -1,903 +1,81 @@ -lockfileVersion: '9.0' +lockfileVersion: '6.0' settings: autoInstallPeers: true excludeLinksFromLockfile: false -importers: - - .: - dependencies: - '@chainlink/solana-sdk': - specifier: link:../ts - version: link:../ts - '@coral-xyz/anchor': - specifier: ^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(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(bufferutil@4.0.8)(utf-8-validate@5.0.10) - '@types/chai': - specifier: ^4.2.22 - version: 4.3.12 - '@types/mocha': - specifier: ^9.0.0 - version: 9.1.1 - '@types/node': - specifier: ^14.14.37 - version: 14.18.63 - '@types/secp256k1': - specifier: ^4.0.3 - version: 4.0.6 - bn.js: - specifier: ^5.2.0 - version: 5.2.1 - borsh: - specifier: ^0.7.0 - version: 0.7.0 - chai: - specifier: ^4.3.4 - version: 4.4.1 - ethereum-cryptography: - specifier: ^0.1.3 - version: 0.1.3 - mocha: - specifier: ^9.0.0 - version: 9.2.2 - prettier: - specifier: ^2.5.1 - version: 2.8.8 - rpc-websockets: - specifier: <=7.10.0 - version: 7.10.0 - secp256k1: - specifier: ^4.0.2 - version: 4.0.3 - ts-mocha: - specifier: ^8.0.0 - version: 8.0.0(mocha@9.2.2) - typescript: - specifier: ^4.5.4 - version: 4.9.5 +dependencies: + '@chainlink/solana-sdk': + specifier: link:../ts + version: link:../ts + '@coral-xyz/anchor': + specifier: ^0.29.0 + version: 0.29.0 + '@solana/spl-token': + specifier: ^0.3.5 + version: 0.3.11(@solana/web3.js@1.92.3)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@4.9.5) + '@solana/web3.js': + specifier: ^1.50.1 <=1.92.3 + version: 1.92.3 + '@types/chai': + specifier: ^4.2.22 + version: 4.3.20 + '@types/mocha': + specifier: ^9.0.0 + version: 9.1.1 + '@types/node': + specifier: ^14.14.37 + version: 14.18.63 + '@types/secp256k1': + specifier: ^4.0.3 + version: 4.0.6 + bn.js: + specifier: ^5.2.0 + version: 5.2.1 + borsh: + specifier: ^0.7.0 + version: 0.7.0 + chai: + specifier: ^4.3.4 + version: 4.5.0 + ethereum-cryptography: + specifier: ^0.1.3 + version: 0.1.3 + mocha: + specifier: ^9.0.0 + version: 9.2.2 + prettier: + specifier: ^2.5.1 + version: 2.8.8 + rpc-websockets: + specifier: <=7.10.0 + version: 7.10.0 + secp256k1: + specifier: ^4.0.2 + version: 4.0.4 + ts-mocha: + specifier: ^8.0.0 + version: 8.0.0(mocha@9.2.2) + typescript: + specifier: ^4.5.4 + version: 4.9.5 packages: - '@babel/runtime@7.25.6': - resolution: {integrity: sha512-VBj9MYyDb9tuLq7yzqjgzt6Q+IBQLrGZfdjOekyEirZPHxXWoTSGUTMrpsfi58Up73d13NfYLv8HT9vmznjzhQ==} + /@babel/runtime@7.26.0: + resolution: {integrity: sha512-FDSOghenHTiToteC/QRlv2q3DhPZ/oOXTBoirfWNx1Cx3TMVcGWQtMMmQcSvb/JjpNeGzx8Pq/b4fKEJuWm1sw==} engines: {node: '>=6.9.0'} - - '@coral-xyz/anchor@0.29.0': - resolution: {integrity: sha512-eny6QNG0WOwqV0zQ7cs/b1tIuzZGmP7U7EcH+ogt4Gdbl8HDmIYVMh/9aTmYZPaFWjtUaI8qSn73uYEXWfATdA==} - engines: {node: '>=11'} - - '@coral-xyz/borsh@0.29.0': - resolution: {integrity: sha512-s7VFVa3a0oqpkuRloWVPdCK7hMbAMY270geZOGfCnaqexrP5dTIpbEHL33req6IYPPJ0hYa71cdvJ1h6V55/oQ==} - engines: {node: '>=10'} - peerDependencies: - '@solana/web3.js': ^1.68.0 - - '@noble/curves@1.6.0': - resolution: {integrity: sha512-TlaHRXDehJuRNR9TfZDNQ45mMEd5dwUwmicsafcIX4SsNiqnCHKjE/1alYPd/lDRVhxdhUAlv8uEhMCI5zjIJQ==} - engines: {node: ^14.21.3 || >=16} - - '@noble/hashes@1.5.0': - resolution: {integrity: sha512-1j6kQFb7QRru7eKN3ZDvRcP13rugwdxZqCjbiAVZfIJwgj2A65UmT4TgARXGlXgnRkORLTDTrO19ZErt7+QXgA==} - engines: {node: ^14.21.3 || >=16} - - '@solana/buffer-layout-utils@0.2.0': - resolution: {integrity: sha512-szG4sxgJGktbuZYDg2FfNmkMi0DYQoVjN2h7ta1W1hPrwzarcFLBq9UpX1UjNXsNpT9dn+chgprtWGioUAr4/g==} - engines: {node: '>= 10'} - - '@solana/buffer-layout@4.0.1': - resolution: {integrity: sha512-E1ImOIAD1tBZFRdjeM4/pzTiTApC0AOBGwyAMS4fwIodCWArzJ3DWdoh8cKxeFM2fElkxBh2Aqts1BPC373rHA==} - engines: {node: '>=5.10'} - - '@solana/codecs-core@2.0.0-experimental.8618508': - resolution: {integrity: sha512-JCz7mKjVKtfZxkuDtwMAUgA7YvJcA2BwpZaA1NOLcted4OMC4Prwa3DUe3f3181ixPYaRyptbF0Ikq2MbDkYEA==} - - '@solana/codecs-data-structures@2.0.0-experimental.8618508': - resolution: {integrity: sha512-sLpjL9sqzaDdkloBPV61Rht1tgaKq98BCtIKRuyscIrmVPu3wu0Bavk2n/QekmUzaTsj7K1pVSniM0YqCdnEBw==} - - '@solana/codecs-numbers@2.0.0-experimental.8618508': - resolution: {integrity: sha512-EXQKfzFr3CkKKNzKSZPOOOzchXsFe90TVONWsSnVkonO9z+nGKALE0/L9uBmIFGgdzhhU9QQVFvxBMclIDJo2Q==} - - '@solana/codecs-strings@2.0.0-experimental.8618508': - resolution: {integrity: sha512-b2yhinr1+oe+JDmnnsV0641KQqqDG8AQ16Z/x7GVWO+AWHMpRlHWVXOq8U1yhPMA4VXxl7i+D+C6ql0VGFp0GA==} - peerDependencies: - fastestsmallesttextencoderdecoder: ^1.0.22 - - '@solana/options@2.0.0-experimental.8618508': - resolution: {integrity: sha512-fy/nIRAMC3QHvnKi63KEd86Xr/zFBVxNW4nEpVEU2OT0gCEKwHY4Z55YHf7XujhyuM3PNpiBKg/YYw5QlRU4vg==} - - '@solana/spl-token-metadata@0.1.2': - resolution: {integrity: sha512-hJYnAJNkDrtkE2Q41YZhCpeOGU/0JgRFXbtrtOuGGeKc3pkEUHB9DDoxZAxx+XRno13GozUleyBi0qypz4c3bw==} - engines: {node: '>=16'} - peerDependencies: - '@solana/web3.js': ^1.87.6 - - '@solana/spl-token@0.3.11': - resolution: {integrity: sha512-bvohO3rIMSVL24Pb+I4EYTJ6cL82eFpInEXD/I8K8upOGjpqHsKUoAempR/RnUlI1qSFNyFlWJfu6MNUgfbCQQ==} - engines: {node: '>=16'} - peerDependencies: - '@solana/web3.js': ^1.88.0 - - '@solana/spl-type-length-value@0.1.0': - resolution: {integrity: sha512-JBMGB0oR4lPttOZ5XiUGyvylwLQjt1CPJa6qQ5oM+MBCndfjz2TKKkw0eATlLLcYmq1jBVsNlJ2cD6ns2GR7lA==} - engines: {node: '>=16'} - - '@solana/web3.js@1.92.3': - resolution: {integrity: sha512-NVBWvb9zdJIAx6X+caXaIICCEQfQaQ8ygykCjJW4u2z/sIKcvPj3ZIIllnx0MWMc3IxGq15ozGYDOQIMbwUcHw==} - - '@solana/web3.js@1.95.3': - resolution: {integrity: sha512-O6rPUN0w2fkNqx/Z3QJMB9L225Ex10PRDH8bTaIUPZXMPV0QP8ZpPvjQnXK+upUczlRgzHzd6SjKIha1p+I6og==} - - '@swc/helpers@0.5.13': - resolution: {integrity: sha512-UoKGxQ3r5kYI9dALKJapMmuK+1zWM/H17Z1+iwnNmzcJRnfFuevZs375TA5rW31pu4BS4NoSy1fRsexDXfWn5w==} - - '@types/chai@4.3.12': - resolution: {integrity: sha512-zNKDHG/1yxm8Il6uCCVsm+dRdEsJlFoDu73X17y09bId6UwoYww+vFBsAcRzl8knM1sab3Dp1VRikFQwDOtDDw==} - - '@types/connect@3.4.38': - resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==} - - '@types/json5@0.0.29': - resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} - - '@types/mocha@9.1.1': - resolution: {integrity: sha512-Z61JK7DKDtdKTWwLeElSEBcWGRLY8g95ic5FoQqI9CMx0ns/Ghep3B4DfcEimiKMvtamNVULVNKEsiwV3aQmXw==} - - '@types/node@12.20.55': - resolution: {integrity: sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==} - - '@types/node@14.18.63': - resolution: {integrity: sha512-fAtCfv4jJg+ExtXhvCkCqUKZ+4ok/JQk01qDKhL5BDDoS3AxKXhV5/MAVUZyQnSEd2GT92fkgZl0pz0Q0AzcIQ==} - - '@types/node@22.6.0': - resolution: {integrity: sha512-QyR8d5bmq+eR72TwQDfujwShHMcIrWIYsaQFtXRE58MHPTEKUNxjxvl0yS0qPMds5xbSDWtp7ZpvGFtd7dfMdQ==} - - '@types/pbkdf2@3.1.2': - resolution: {integrity: sha512-uRwJqmiXmh9++aSu1VNEn3iIxWOhd8AHXNSdlaLfdAAdSTY9jYVeGWnzejM3dvrkbqE3/hyQkQQ29IFATEGlew==} - - '@types/secp256k1@4.0.6': - resolution: {integrity: sha512-hHxJU6PAEUn0TP4S/ZOzuTUvJWuZ6eIKeNKb5RBpODvSl6hp1Wrw4s7ATY50rklRCScUDpHzVA/DQdSjJ3UoYQ==} - - '@types/uuid@8.3.4': - resolution: {integrity: sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw==} - - '@types/ws@7.4.7': - resolution: {integrity: sha512-JQbbmxZTZehdc2iszGKs5oC3NFnjeay7mtAWrdt7qNtAVK0g19muApzAy4bm9byz79xa2ZnO/BOBC2R8RC5Lww==} - - '@types/ws@8.5.12': - resolution: {integrity: sha512-3tPRkv1EtkDpzlgyKyI8pGsGZAGPEaXeu0DOj5DI25Ja91bdAYddYHbADRYVrZMRbfW+1l5YwXVDKohDJNQxkQ==} - - '@ungap/promise-all-settled@1.1.2': - resolution: {integrity: sha512-sL/cEvJWAnClXw0wHk85/2L0G6Sj8UB0Ctc1TEMbKSsmpRosqhwj9gWgFRZSrBr2f9tiXISwNhCPmlfqUqyb9Q==} - - JSONStream@1.3.5: - resolution: {integrity: sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ==} - hasBin: true - - agentkeepalive@4.5.0: - resolution: {integrity: sha512-5GG/5IbQQpC9FpkRGsSvZI5QYeSCzlJHdpBQntCsuTOxhKD8lqKhrleg2Yi7yvMIf82Ycmmqln9U8V9qwEiJew==} - engines: {node: '>= 8.0.0'} - - ansi-colors@4.1.1: - resolution: {integrity: sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==} - engines: {node: '>=6'} - - ansi-regex@5.0.1: - resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} - engines: {node: '>=8'} - - ansi-styles@4.3.0: - resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} - engines: {node: '>=8'} - - anymatch@3.1.3: - resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} - engines: {node: '>= 8'} - - argparse@2.0.1: - resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} - - arrify@1.0.1: - resolution: {integrity: sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA==} - engines: {node: '>=0.10.0'} - - assertion-error@1.1.0: - resolution: {integrity: sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==} - - balanced-match@1.0.2: - resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} - - base-x@3.0.10: - resolution: {integrity: sha512-7d0s06rR9rYaIWHkpfLIFICM/tkSVdoPC9qYAQRpxn9DdKNWNsKC0uk++akckyLq16Tx2WIinnZ6WRriAt6njQ==} - - base64-js@1.5.1: - resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} - - bigint-buffer@1.1.5: - resolution: {integrity: sha512-trfYco6AoZ+rKhKnxA0hgX0HAbVP/s808/EuDSe2JDzUnCp/xAsli35Orvk67UrTEcwuxZqYZDmfA2RXJgxVvA==} - engines: {node: '>= 10.0.0'} - - bignumber.js@9.1.2: - resolution: {integrity: sha512-2/mKyZH9K85bzOEfhXDBFZTGd1CTs+5IHpeFQo9luiBG7hghdC851Pj2WAhb6E3R6b9tZj/XKhbg4fum+Kepug==} - - binary-extensions@2.3.0: - resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} - engines: {node: '>=8'} - - bindings@1.5.0: - resolution: {integrity: sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==} - - blakejs@1.2.1: - resolution: {integrity: sha512-QXUSXI3QVc/gJME0dBpXrag1kbzOqCjCX8/b54ntNyW6sjtoqxqRk3LTmXzaJoh71zMsDCjM+47jS7XiwN/+fQ==} - - bn.js@4.12.0: - resolution: {integrity: sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==} - - bn.js@5.2.1: - resolution: {integrity: sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ==} - - borsh@0.7.0: - resolution: {integrity: sha512-CLCsZGIBCFnPtkNnieW/a8wmreDmfUtjU2m9yHrzPXIlNbqVs0AQrSatSG6vdNYUqdc83tkQi2eHfF98ubzQLA==} - - brace-expansion@1.1.11: - resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} - - braces@3.0.2: - resolution: {integrity: sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==} - engines: {node: '>=8'} - - brorand@1.1.0: - resolution: {integrity: sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w==} - - browser-stdout@1.3.1: - resolution: {integrity: sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==} - - browserify-aes@1.2.0: - resolution: {integrity: sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==} - - bs58@4.0.1: - resolution: {integrity: sha512-Ok3Wdf5vOIlBrgCvTq96gBkJw+JUEzdBgyaza5HLtPm7yTHkjRy8+JzNyHF7BHa0bNWOQIp3m5YF0nnFcOIKLw==} - - bs58check@2.1.2: - resolution: {integrity: sha512-0TS1jicxdU09dwJMNZtVAfzPi6Q6QeN0pM1Fkzrjn+XYHvzMKPU3pHVpva+769iNVSfIYWf7LJ6WR+BuuMf8cA==} - - buffer-from@1.1.2: - resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} - - buffer-layout@1.2.2: - resolution: {integrity: sha512-kWSuLN694+KTk8SrYvCqwP2WcgQjoRCiF5b4QDvkkz8EmgD+aWAIceGFKMIAdmF/pH+vpgNV3d3kAKorcdAmWA==} - engines: {node: '>=4.5'} - - buffer-xor@1.0.3: - resolution: {integrity: sha512-571s0T7nZWK6vB67HI5dyUF7wXiNcfaPPPTl6zYCNApANjIvYJTg7hlud/+cJpdAhS7dVzqMLmfhfHR3rAcOjQ==} - - buffer@6.0.3: - resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==} - - bufferutil@4.0.8: - resolution: {integrity: sha512-4T53u4PdgsXqKaIctwF8ifXlRTTmEPJ8iEPWFdGZvcf7sbwYo6FKFEX9eNNAnzFZ7EzJAQ3CJeOtCRA4rDp7Pw==} - engines: {node: '>=6.14.2'} - - camelcase@6.3.0: - resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==} - engines: {node: '>=10'} - - chai@4.4.1: - resolution: {integrity: sha512-13sOfMv2+DWduEU+/xbun3LScLoqN17nBeTLUsmDfKdoiC1fr0n9PU4guu4AhRcOVFk/sW8LyZWHuhWtQZiF+g==} - engines: {node: '>=4'} - - chalk@4.1.2: - resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} - engines: {node: '>=10'} - - check-error@1.0.3: - resolution: {integrity: sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==} - - chokidar@3.5.3: - resolution: {integrity: sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==} - engines: {node: '>= 8.10.0'} - - cipher-base@1.0.4: - resolution: {integrity: sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==} - - cliui@7.0.4: - resolution: {integrity: sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==} - - color-convert@2.0.1: - resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} - engines: {node: '>=7.0.0'} - - color-name@1.1.4: - resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} - - commander@2.20.3: - resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} - - concat-map@0.0.1: - resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} - - create-hash@1.2.0: - resolution: {integrity: sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==} - - create-hmac@1.1.7: - resolution: {integrity: sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==} - - cross-fetch@3.1.8: - resolution: {integrity: sha512-cvA+JwZoU0Xq+h6WkMvAUqPEYy92Obet6UdKLfW60qn99ftItKjB5T+BkyWOFWe2pUyfQ+IJHmpOTznqk1M6Kg==} - - crypto-hash@1.3.0: - resolution: {integrity: sha512-lyAZ0EMyjDkVvz8WOeVnuCPvKVBXcMv1l5SVqO1yC7PzTwrD/pPje/BIRbWhMoPe436U+Y2nD7f5bFx0kt+Sbg==} - engines: {node: '>=8'} - - debug@4.3.3: - resolution: {integrity: sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==} - engines: {node: '>=6.0'} - peerDependencies: - supports-color: '*' - peerDependenciesMeta: - supports-color: - optional: true - - decamelize@4.0.0: - resolution: {integrity: sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==} - engines: {node: '>=10'} - - deep-eql@4.1.3: - resolution: {integrity: sha512-WaEtAOpRA1MQ0eohqZjpGD8zdI0Ovsm8mmFhaDN8dvDZzyoUMcYDnf5Y6iu7HTXxf8JDS23qWa4a+hKCDyOPzw==} - engines: {node: '>=6'} - - delay@5.0.0: - resolution: {integrity: sha512-ReEBKkIfe4ya47wlPYf/gu5ib6yUG0/Aez0JQZQz94kiWtRQvZIQbTiehsnwHvLSWJnQdhVeqYue7Id1dKr0qw==} - engines: {node: '>=10'} - - diff@3.5.0: - resolution: {integrity: sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==} - engines: {node: '>=0.3.1'} - - diff@5.0.0: - resolution: {integrity: sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==} - engines: {node: '>=0.3.1'} - - dot-case@3.0.4: - resolution: {integrity: sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==} - - elliptic@6.5.5: - resolution: {integrity: sha512-7EjbcmUm17NQFu4Pmgmq2olYMj8nwMnpcddByChSUjArp8F5DQWcIcpriwO4ZToLNAJig0yiyjswfyGNje/ixw==} - - emoji-regex@8.0.0: - resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} - - es6-promise@4.2.8: - resolution: {integrity: sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==} - - es6-promisify@5.0.0: - resolution: {integrity: sha512-C+d6UdsYDk0lMebHNR4S2NybQMMngAOnOwYBQjTOiv0MkoJMP0Myw2mgpDLBcpfCmRLxyFqYhS/CfOENq4SJhQ==} - - escalade@3.1.2: - resolution: {integrity: sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==} - engines: {node: '>=6'} - - escape-string-regexp@4.0.0: - resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} - engines: {node: '>=10'} - - ethereum-cryptography@0.1.3: - resolution: {integrity: sha512-w8/4x1SGGzc+tO97TASLja6SLd3fRIK2tLVcV2Gx4IB21hE19atll5Cq9o3d0ZmAYC/8aw0ipieTSiekAea4SQ==} - - eventemitter3@4.0.7: - resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==} - - eventemitter3@5.0.1: - resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==} - - evp_bytestokey@1.0.3: - resolution: {integrity: sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA==} - - eyes@0.1.8: - resolution: {integrity: sha512-GipyPsXO1anza0AOZdy69Im7hGFCNB7Y/NGjDlZGJ3GJJLtwNSb2vrzYrTYJRrRloVx7pl+bhUaTB8yiccPvFQ==} - engines: {node: '> 0.1.90'} - - fast-stable-stringify@1.0.0: - resolution: {integrity: sha512-wpYMUmFu5f00Sm0cj2pfivpmawLZ0NKdviQ4w9zJeR8JVtOpOxHmLaJuj0vxvGqMJQWyP/COUkF75/57OKyRag==} - - fastestsmallesttextencoderdecoder@1.0.22: - resolution: {integrity: sha512-Pb8d48e+oIuY4MaM64Cd7OW1gt4nxCHs7/ddPPZ/Ic3sg8yVGM7O9wDvZ7us6ScaUupzM+pfBolwtYhN1IxBIw==} - - file-uri-to-path@1.0.0: - resolution: {integrity: sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==} - - fill-range@7.0.1: - resolution: {integrity: sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==} - engines: {node: '>=8'} - - find-up@5.0.0: - resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} - engines: {node: '>=10'} - - flat@5.0.2: - resolution: {integrity: sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==} - hasBin: true - - fs.realpath@1.0.0: - resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} - - fsevents@2.3.3: - resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} - engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} - os: [darwin] - - get-caller-file@2.0.5: - resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} - engines: {node: 6.* || 8.* || >= 10.*} - - get-func-name@2.0.2: - resolution: {integrity: sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==} - - glob-parent@5.1.2: - resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} - engines: {node: '>= 6'} - - glob@7.2.0: - resolution: {integrity: sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==} - deprecated: Glob versions prior to v9 are no longer supported - - growl@1.10.5: - resolution: {integrity: sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==} - engines: {node: '>=4.x'} - - has-flag@4.0.0: - resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} - engines: {node: '>=8'} - - hash-base@3.1.0: - resolution: {integrity: sha512-1nmYp/rhMDiE7AYkDw+lLwlAzz0AntGIe51F3RfFfEqyQ3feY2eI/NcwC6umIQVOASPMsWJLJScWKSSvzL9IVA==} - engines: {node: '>=4'} - - hash.js@1.1.7: - resolution: {integrity: sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==} - - he@1.2.0: - resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==} - hasBin: true - - hmac-drbg@1.0.1: - resolution: {integrity: sha512-Tti3gMqLdZfhOQY1Mzf/AanLiqh1WTiJgEj26ZuYQ9fbkLomzGchCws4FyrSd4VkpBfiNhaE1On+lOz894jvXg==} - - humanize-ms@1.2.1: - resolution: {integrity: sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==} - - ieee754@1.2.1: - resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} - - inflight@1.0.6: - resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} - deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. - - inherits@2.0.4: - resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} - - is-binary-path@2.1.0: - resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} - engines: {node: '>=8'} - - is-extglob@2.1.1: - resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} - engines: {node: '>=0.10.0'} - - is-fullwidth-code-point@3.0.0: - resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} - engines: {node: '>=8'} - - is-glob@4.0.3: - resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} - engines: {node: '>=0.10.0'} - - is-number@7.0.0: - resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} - engines: {node: '>=0.12.0'} - - is-plain-obj@2.1.0: - resolution: {integrity: sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==} - engines: {node: '>=8'} - - is-unicode-supported@0.1.0: - resolution: {integrity: sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==} - engines: {node: '>=10'} - - isexe@2.0.0: - resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} - - isomorphic-ws@4.0.1: - resolution: {integrity: sha512-BhBvN2MBpWTaSHdWRb/bwdZJ1WaehQ2L1KngkCkfLUGF0mAWAT1sQUQacEmQ0jXkFw/czDXPNQSL5u2/Krsz1w==} - peerDependencies: - ws: '*' - - jayson@4.1.2: - resolution: {integrity: sha512-5nzMWDHy6f+koZOuYsArh2AXs73NfWYVlFyJJuCedr93GpY+Ku8qq10ropSXVfHK+H0T6paA88ww+/dV+1fBNA==} - engines: {node: '>=8'} - hasBin: true - - js-yaml@4.1.0: - resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} - hasBin: true - - json-stringify-safe@5.0.1: - resolution: {integrity: sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==} - - json5@1.0.2: - resolution: {integrity: sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==} - hasBin: true - - jsonparse@1.3.1: - resolution: {integrity: sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==} - engines: {'0': node >= 0.2.0} - - keccak@3.0.4: - resolution: {integrity: sha512-3vKuW0jV8J3XNTzvfyicFR5qvxrSAGl7KIhvgOu5cmWwM7tZRj3fMbj/pfIf4be7aznbc+prBWGjywox/g2Y6Q==} - engines: {node: '>=10.0.0'} - - locate-path@6.0.0: - resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} - engines: {node: '>=10'} - - log-symbols@4.1.0: - resolution: {integrity: sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==} - engines: {node: '>=10'} - - loupe@2.3.7: - resolution: {integrity: sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==} - - lower-case@2.0.2: - resolution: {integrity: sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==} - - make-error@1.3.6: - resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==} - - md5.js@1.3.5: - resolution: {integrity: sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==} - - minimalistic-assert@1.0.1: - resolution: {integrity: sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==} - - minimalistic-crypto-utils@1.0.1: - resolution: {integrity: sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg==} - - minimatch@3.1.2: - resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} - - minimatch@4.2.1: - resolution: {integrity: sha512-9Uq1ChtSZO+Mxa/CL1eGizn2vRn3MlLgzhT0Iz8zaY8NdvxvB0d5QdPFmCKf7JKA9Lerx5vRrnwO03jsSfGG9g==} - engines: {node: '>=10'} - - minimist@1.2.8: - resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} - - mkdirp@0.5.6: - resolution: {integrity: sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==} - hasBin: true - - mocha@9.2.2: - resolution: {integrity: sha512-L6XC3EdwT6YrIk0yXpavvLkn8h+EU+Y5UcCHKECyMbdUIxyMuZj4bX4U9e1nvnvUUvQVsV2VHQr5zLdcUkhW/g==} - engines: {node: '>= 12.0.0'} - hasBin: true - - ms@2.1.2: - resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} - - ms@2.1.3: - resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} - - nanoid@3.3.1: - resolution: {integrity: sha512-n6Vs/3KGyxPQd6uO0eH4Bv0ojGSUvuLlIHtC3Y0kEO23YRge8H9x1GCzLn28YX0H66pMkxuaeESFq4tKISKwdw==} - engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} - hasBin: true - - no-case@3.0.4: - resolution: {integrity: sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==} - - node-addon-api@2.0.2: - resolution: {integrity: sha512-Ntyt4AIXyaLIuMHF6IOoTakB3K+RWxwtsHNRxllEoA6vPwP9o4866g6YWDLUdnucilZhmkxiHwHr11gAENw+QA==} - - node-fetch@2.7.0: - resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} - engines: {node: 4.x || >=6.0.0} - peerDependencies: - encoding: ^0.1.0 - peerDependenciesMeta: - encoding: - optional: true - - node-gyp-build@4.8.2: - resolution: {integrity: sha512-IRUxE4BVsHWXkV/SFOut4qTlagw2aM8T5/vnTsmrHJvVoKueJHRc/JaFND7QDDc61kLYUJ6qlZM3sqTSyx2dTw==} - hasBin: true - - normalize-path@3.0.0: - resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} - engines: {node: '>=0.10.0'} - - once@1.4.0: - resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} - - p-limit@3.1.0: - resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} - engines: {node: '>=10'} - - p-locate@5.0.0: - resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} - engines: {node: '>=10'} - - pako@2.1.0: - resolution: {integrity: sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==} - - path-exists@4.0.0: - resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} - engines: {node: '>=8'} - - path-is-absolute@1.0.1: - resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} - engines: {node: '>=0.10.0'} - - pathval@1.1.1: - resolution: {integrity: sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==} - - pbkdf2@3.1.2: - resolution: {integrity: sha512-iuh7L6jA7JEGu2WxDwtQP1ddOpaJNC4KlDEFfdQajSGgGPNi4OyDc2R7QnbY2bR9QjBVGwgvTdNJZoE7RaxUMA==} - engines: {node: '>=0.12'} - - picomatch@2.3.1: - resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} - engines: {node: '>=8.6'} - - prettier@2.8.8: - resolution: {integrity: sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==} - engines: {node: '>=10.13.0'} - hasBin: true - - randombytes@2.1.0: - resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==} - - readable-stream@3.6.2: - resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} - engines: {node: '>= 6'} - - readdirp@3.6.0: - resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} - engines: {node: '>=8.10.0'} - - regenerator-runtime@0.14.1: - resolution: {integrity: sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==} - - require-directory@2.1.1: - resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} - engines: {node: '>=0.10.0'} - - ripemd160@2.0.2: - resolution: {integrity: sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA==} - - rpc-websockets@7.10.0: - resolution: {integrity: sha512-cemZ6RiDtYZpPiBzYijdOrkQQzmBCmug0E9SdRH2gIUNT15ql4mwCYWIp0VnSZq6Qrw/JkGUygp4PrK1y9KfwQ==} - - rpc-websockets@8.0.1: - resolution: {integrity: sha512-PptrPRK40uQvifq5sCcObmqInVcZXhy+RRrirzdE5KUPvDI47y1wPvfckD2QzqngOU9xaPW/dT+G+b+wj6M1MQ==} - - rpc-websockets@9.0.2: - resolution: {integrity: sha512-YzggvfItxMY3Lwuax5rC18inhbjJv9Py7JXRHxTIi94JOLrqBsSsUUc5bbl5W6c11tXhdfpDPK0KzBhoGe8jjw==} - - safe-buffer@5.2.1: - resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} - - scrypt-js@3.0.1: - resolution: {integrity: sha512-cdwTTnqPu0Hyvf5in5asVdZocVDTNRmR7XEcJuIzMjJeSHybHl7vpB66AzwTaIg6CLSbtjcxc8fqcySfnTkccA==} - - secp256k1@4.0.3: - resolution: {integrity: sha512-NLZVf+ROMxwtEj3Xa562qgv2BK5e2WNmXPiOdVIPLgs6lyTzMvBq0aWTYMI5XCP9jZMVKOcqZLw/Wc4vDkuxhA==} - engines: {node: '>=10.0.0'} - - serialize-javascript@6.0.0: - resolution: {integrity: sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==} - - setimmediate@1.0.5: - resolution: {integrity: sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==} - - sha.js@2.4.11: - resolution: {integrity: sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==} - hasBin: true - - snake-case@3.0.4: - resolution: {integrity: sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg==} - - source-map-support@0.5.21: - resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} - - source-map@0.6.1: - resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} - engines: {node: '>=0.10.0'} - - string-width@4.2.3: - resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} - engines: {node: '>=8'} - - string_decoder@1.3.0: - resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} - - strip-ansi@6.0.1: - resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} - engines: {node: '>=8'} - - strip-bom@3.0.0: - resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} - engines: {node: '>=4'} - - strip-json-comments@3.1.1: - resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} - engines: {node: '>=8'} - - superstruct@0.15.5: - resolution: {integrity: sha512-4AOeU+P5UuE/4nOUkmcQdW5y7i9ndt1cQd/3iUe+LTz3RxESf/W/5lg4B74HbDMMv8PHnPnGCQFH45kBcrQYoQ==} - - superstruct@1.0.4: - resolution: {integrity: sha512-7JpaAoX2NGyoFlI9NBh66BQXGONc+uE+MRS5i2iOBKuS4e+ccgMDjATgZldkah+33DakBxDHiss9kvUcGAO8UQ==} - engines: {node: '>=14.0.0'} - - superstruct@2.0.2: - resolution: {integrity: sha512-uV+TFRZdXsqXTL2pRvujROjdZQ4RAlBUS5BTh9IGm+jTqQntYThciG/qu57Gs69yjnVUSqdxF9YLmSnpupBW9A==} - engines: {node: '>=14.0.0'} - - supports-color@7.2.0: - resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} - engines: {node: '>=8'} - - supports-color@8.1.1: - resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==} - engines: {node: '>=10'} - - text-encoding-utf-8@1.0.2: - resolution: {integrity: sha512-8bw4MY9WjdsD2aMtO0OzOCY3pXGYNx2d2FfHRVUKkiCPDWjKuOlhLVASS+pD7VkLTVjW268LYJHwsnPFlBpbAg==} - - through@2.3.8: - resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==} - - to-regex-range@5.0.1: - resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} - engines: {node: '>=8.0'} - - toml@3.0.0: - resolution: {integrity: sha512-y/mWCZinnvxjTKYhJ+pYxwD0mRLVvOtdS2Awbgxln6iEnt4rk0yBxeSBHkGJcPucRiG0e55mwWp+g/05rsrd6w==} - - tr46@0.0.3: - resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} - - ts-mocha@8.0.0: - resolution: {integrity: sha512-Kou1yxTlubLnD5C3unlCVO7nh0HERTezjoVhVw/M5S1SqoUec0WgllQvPk3vzPMc6by8m6xD1uR1yRf8lnVUbA==} - engines: {node: '>= 6.X.X'} - hasBin: true - peerDependencies: - mocha: ^3.X.X || ^4.X.X || ^5.X.X || ^6.X.X || ^7.X.X || ^8.X.X - - ts-node@7.0.1: - resolution: {integrity: sha512-BVwVbPJRspzNh2yfslyT1PSbl5uIk03EZlb493RKHN4qej/D06n1cEhjlOJG69oFsE7OT8XjpTUcYf6pKTLMhw==} - engines: {node: '>=4.2.0'} - hasBin: true - - tsconfig-paths@3.15.0: - resolution: {integrity: sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==} - - tslib@2.7.0: - resolution: {integrity: sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==} - - type-detect@4.0.8: - resolution: {integrity: sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==} - engines: {node: '>=4'} - - typescript@4.9.5: - resolution: {integrity: sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==} - engines: {node: '>=4.2.0'} - hasBin: true - - undici-types@6.19.8: - resolution: {integrity: sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==} - - utf-8-validate@5.0.10: - resolution: {integrity: sha512-Z6czzLq4u8fPOyx7TU6X3dvUZVvoJmxSQ+IcrlmagKhilxlhZgxPK6C5Jqbkw1IDUmFTM+cz9QDnnLTwDz/2gQ==} - engines: {node: '>=6.14.2'} - - util-deprecate@1.0.2: - resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} - - uuid@8.3.2: - resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==} - hasBin: true - - webidl-conversions@3.0.1: - resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} - - whatwg-url@5.0.0: - resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} - - which@2.0.2: - resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} - engines: {node: '>= 8'} - hasBin: true - - workerpool@6.2.0: - resolution: {integrity: sha512-Rsk5qQHJ9eowMH28Jwhe8HEbmdYDX4lwoMWshiCXugjtHqMD9ZbiqSDLxcsfdqsETPzVUtX5s1Z5kStiIM6l4A==} - - wrap-ansi@7.0.0: - resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} - engines: {node: '>=10'} - - wrappy@1.0.2: - resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} - - ws@7.5.10: - resolution: {integrity: sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==} - engines: {node: '>=8.3.0'} - peerDependencies: - bufferutil: ^4.0.1 - utf-8-validate: ^5.0.2 - peerDependenciesMeta: - bufferutil: - optional: true - utf-8-validate: - optional: true - - ws@8.18.0: - resolution: {integrity: sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==} - engines: {node: '>=10.0.0'} - peerDependencies: - bufferutil: ^4.0.1 - utf-8-validate: '>=5.0.2' - peerDependenciesMeta: - bufferutil: - optional: true - utf-8-validate: - optional: true - - y18n@5.0.8: - resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} - engines: {node: '>=10'} - - yargs-parser@20.2.4: - resolution: {integrity: sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==} - engines: {node: '>=10'} - - yargs-parser@20.2.9: - resolution: {integrity: sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==} - engines: {node: '>=10'} - - yargs-unparser@2.0.0: - resolution: {integrity: sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==} - engines: {node: '>=10'} - - yargs@16.2.0: - resolution: {integrity: sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==} - engines: {node: '>=10'} - - yn@2.0.0: - resolution: {integrity: sha512-uTv8J/wiWTgUTg+9vLTi//leUl5vDQS6uii/emeTb2ssY7vl6QWf2fFbIIGjnhjvbdKlU0ed7QPgY1htTC86jQ==} - engines: {node: '>=4'} - - yocto-queue@0.1.0: - resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} - engines: {node: '>=10'} - -snapshots: - - '@babel/runtime@7.25.6': dependencies: regenerator-runtime: 0.14.1 + dev: false - '@coral-xyz/anchor@0.29.0(bufferutil@4.0.8)(utf-8-validate@5.0.10)': + /@coral-xyz/anchor@0.29.0: + resolution: {integrity: sha512-eny6QNG0WOwqV0zQ7cs/b1tIuzZGmP7U7EcH+ogt4Gdbl8HDmIYVMh/9aTmYZPaFWjtUaI8qSn73uYEXWfATdA==} + engines: {node: '>=11'} dependencies: - '@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(bufferutil@4.0.8)(utf-8-validate@5.0.10) + '@coral-xyz/borsh': 0.29.0(@solana/web3.js@1.92.3) + '@noble/hashes': 1.6.1 + '@solana/web3.js': 1.92.3 bn.js: 5.2.1 bs58: 4.0.1 buffer-layout: 1.2.2 @@ -913,112 +91,179 @@ snapshots: - bufferutil - encoding - utf-8-validate + dev: false - '@coral-xyz/borsh@0.29.0(@solana/web3.js@1.95.3(bufferutil@4.0.8)(utf-8-validate@5.0.10))': + /@coral-xyz/borsh@0.29.0(@solana/web3.js@1.92.3): + resolution: {integrity: sha512-s7VFVa3a0oqpkuRloWVPdCK7hMbAMY270geZOGfCnaqexrP5dTIpbEHL33req6IYPPJ0hYa71cdvJ1h6V55/oQ==} + engines: {node: '>=10'} + peerDependencies: + '@solana/web3.js': ^1.68.0 dependencies: - '@solana/web3.js': 1.95.3(bufferutil@4.0.8)(utf-8-validate@5.0.10) + '@solana/web3.js': 1.92.3 bn.js: 5.2.1 buffer-layout: 1.2.2 + dev: false - '@noble/curves@1.6.0': + /@noble/curves@1.7.0: + resolution: {integrity: sha512-UTMhXK9SeDhFJVrHeUJ5uZlI6ajXg10O6Ddocf9S6GjbSBVZsJo88HzKwXznNfGpMTRDyJkqMjNDPYgf0qFWnw==} + engines: {node: ^14.21.3 || >=16} dependencies: - '@noble/hashes': 1.5.0 + '@noble/hashes': 1.6.0 + dev: false + + /@noble/hashes@1.6.0: + resolution: {integrity: sha512-YUULf0Uk4/mAA89w+k3+yUYh6NrEvxZa5T6SY3wlMvE2chHkxFUUIDI8/XW1QSC357iA5pSnqt7XEhvFOqmDyQ==} + engines: {node: ^14.21.3 || >=16} + dev: false - '@noble/hashes@1.5.0': {} + /@noble/hashes@1.6.1: + resolution: {integrity: sha512-pq5D8h10hHBjyqX+cfBm0i8JUXJ0UhczFc4r74zbuT9XgewFo2E3J1cOaGtdZynILNmQ685YWGzGE1Zv6io50w==} + engines: {node: ^14.21.3 || >=16} + dev: false - '@solana/buffer-layout-utils@0.2.0(bufferutil@4.0.8)(utf-8-validate@5.0.10)': + /@solana/buffer-layout-utils@0.2.0: + resolution: {integrity: sha512-szG4sxgJGktbuZYDg2FfNmkMi0DYQoVjN2h7ta1W1hPrwzarcFLBq9UpX1UjNXsNpT9dn+chgprtWGioUAr4/g==} + engines: {node: '>= 10'} dependencies: '@solana/buffer-layout': 4.0.1 - '@solana/web3.js': 1.95.3(bufferutil@4.0.8)(utf-8-validate@5.0.10) + '@solana/web3.js': 1.92.3 bigint-buffer: 1.1.5 bignumber.js: 9.1.2 transitivePeerDependencies: - bufferutil - encoding - utf-8-validate + dev: false - '@solana/buffer-layout@4.0.1': + /@solana/buffer-layout@4.0.1: + resolution: {integrity: sha512-E1ImOIAD1tBZFRdjeM4/pzTiTApC0AOBGwyAMS4fwIodCWArzJ3DWdoh8cKxeFM2fElkxBh2Aqts1BPC373rHA==} + engines: {node: '>=5.10'} dependencies: buffer: 6.0.3 + dev: false - '@solana/codecs-core@2.0.0-experimental.8618508': {} - - '@solana/codecs-data-structures@2.0.0-experimental.8618508': + /@solana/codecs-core@2.0.0-rc.1(typescript@4.9.5): + resolution: {integrity: sha512-bauxqMfSs8EHD0JKESaNmNuNvkvHSuN3bbWAF5RjOfDu2PugxHrvRebmYauvSumZ3cTfQ4HJJX6PG5rN852qyQ==} + peerDependencies: + typescript: '>=5' dependencies: - '@solana/codecs-core': 2.0.0-experimental.8618508 - '@solana/codecs-numbers': 2.0.0-experimental.8618508 + '@solana/errors': 2.0.0-rc.1(typescript@4.9.5) + typescript: 4.9.5 + dev: false - '@solana/codecs-numbers@2.0.0-experimental.8618508': + /@solana/codecs-data-structures@2.0.0-rc.1(typescript@4.9.5): + resolution: {integrity: sha512-rinCv0RrAVJ9rE/rmaibWJQxMwC5lSaORSZuwjopSUE6T0nb/MVg6Z1siNCXhh/HFTOg0l8bNvZHgBcN/yvXog==} + peerDependencies: + typescript: '>=5' dependencies: - '@solana/codecs-core': 2.0.0-experimental.8618508 + '@solana/codecs-core': 2.0.0-rc.1(typescript@4.9.5) + '@solana/codecs-numbers': 2.0.0-rc.1(typescript@4.9.5) + '@solana/errors': 2.0.0-rc.1(typescript@4.9.5) + typescript: 4.9.5 + dev: false - '@solana/codecs-strings@2.0.0-experimental.8618508(fastestsmallesttextencoderdecoder@1.0.22)': + /@solana/codecs-numbers@2.0.0-rc.1(typescript@4.9.5): + resolution: {integrity: sha512-J5i5mOkvukXn8E3Z7sGIPxsThRCgSdgTWJDQeZvucQ9PT6Y3HiVXJ0pcWiOWAoQ3RX8e/f4I3IC+wE6pZiJzDQ==} + peerDependencies: + typescript: '>=5' dependencies: - '@solana/codecs-core': 2.0.0-experimental.8618508 - '@solana/codecs-numbers': 2.0.0-experimental.8618508 - fastestsmallesttextencoderdecoder: 1.0.22 + '@solana/codecs-core': 2.0.0-rc.1(typescript@4.9.5) + '@solana/errors': 2.0.0-rc.1(typescript@4.9.5) + typescript: 4.9.5 + dev: false - '@solana/options@2.0.0-experimental.8618508': + /@solana/codecs-strings@2.0.0-rc.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@4.9.5): + resolution: {integrity: sha512-9/wPhw8TbGRTt6mHC4Zz1RqOnuPTqq1Nb4EyuvpZ39GW6O2t2Q7Q0XxiB3+BdoEjwA2XgPw6e2iRfvYgqty44g==} + peerDependencies: + fastestsmallesttextencoderdecoder: ^1.0.22 + typescript: '>=5' dependencies: - '@solana/codecs-core': 2.0.0-experimental.8618508 - '@solana/codecs-numbers': 2.0.0-experimental.8618508 + '@solana/codecs-core': 2.0.0-rc.1(typescript@4.9.5) + '@solana/codecs-numbers': 2.0.0-rc.1(typescript@4.9.5) + '@solana/errors': 2.0.0-rc.1(typescript@4.9.5) + fastestsmallesttextencoderdecoder: 1.0.22 + typescript: 4.9.5 + dev: false - '@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 - '@solana/codecs-numbers': 2.0.0-experimental.8618508 - '@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(bufferutil@4.0.8)(utf-8-validate@5.0.10) + /@solana/codecs@2.0.0-rc.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@4.9.5): + resolution: {integrity: sha512-qxoR7VybNJixV51L0G1RD2boZTcxmwUWnKCaJJExQ5qNKwbpSyDdWfFJfM5JhGyKe9DnPVOZB+JHWXnpbZBqrQ==} + peerDependencies: + typescript: '>=5' + dependencies: + '@solana/codecs-core': 2.0.0-rc.1(typescript@4.9.5) + '@solana/codecs-data-structures': 2.0.0-rc.1(typescript@4.9.5) + '@solana/codecs-numbers': 2.0.0-rc.1(typescript@4.9.5) + '@solana/codecs-strings': 2.0.0-rc.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@4.9.5) + '@solana/options': 2.0.0-rc.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@4.9.5) + typescript: 4.9.5 transitivePeerDependencies: - fastestsmallesttextencoderdecoder + dev: false - '@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)': + /@solana/errors@2.0.0-rc.1(typescript@4.9.5): + resolution: {integrity: sha512-ejNvQ2oJ7+bcFAYWj225lyRkHnixuAeb7RQCixm+5mH4n1IA4Qya/9Bmfy5RAAHQzxK43clu3kZmL5eF9VGtYQ==} + hasBin: true + peerDependencies: + typescript: '>=5' dependencies: - '@solana/buffer-layout': 4.0.1 - '@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 + chalk: 5.3.0 + commander: 12.1.0 + typescript: 4.9.5 + dev: false + + /@solana/options@2.0.0-rc.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@4.9.5): + resolution: {integrity: sha512-mLUcR9mZ3qfHlmMnREdIFPf9dpMc/Bl66tLSOOWxw4ml5xMT2ohFn7WGqoKcu/UHkT9CrC6+amEdqCNvUqI7AA==} + peerDependencies: + typescript: '>=5' + dependencies: + '@solana/codecs-core': 2.0.0-rc.1(typescript@4.9.5) + '@solana/codecs-data-structures': 2.0.0-rc.1(typescript@4.9.5) + '@solana/codecs-numbers': 2.0.0-rc.1(typescript@4.9.5) + '@solana/codecs-strings': 2.0.0-rc.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@4.9.5) + '@solana/errors': 2.0.0-rc.1(typescript@4.9.5) + typescript: 4.9.5 transitivePeerDependencies: - - bufferutil - - encoding - fastestsmallesttextencoderdecoder - - utf-8-validate + dev: false - '@solana/spl-type-length-value@0.1.0': + /@solana/spl-token-metadata@0.1.6(@solana/web3.js@1.92.3)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@4.9.5): + resolution: {integrity: sha512-7sMt1rsm/zQOQcUWllQX9mD2O6KhSAtY1hFR2hfFwgqfFWzSY9E9GDvFVNYUI1F0iQKcm6HmePU9QbKRXTEBiA==} + engines: {node: '>=16'} + peerDependencies: + '@solana/web3.js': ^1.95.3 dependencies: - buffer: 6.0.3 + '@solana/codecs': 2.0.0-rc.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@4.9.5) + '@solana/web3.js': 1.92.3 + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder + - typescript + dev: false - '@solana/web3.js@1.92.3(bufferutil@4.0.8)(utf-8-validate@5.0.10)': + /@solana/spl-token@0.3.11(@solana/web3.js@1.92.3)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@4.9.5): + resolution: {integrity: sha512-bvohO3rIMSVL24Pb+I4EYTJ6cL82eFpInEXD/I8K8upOGjpqHsKUoAempR/RnUlI1qSFNyFlWJfu6MNUgfbCQQ==} + engines: {node: '>=16'} + peerDependencies: + '@solana/web3.js': ^1.88.0 dependencies: - '@babel/runtime': 7.25.6 - '@noble/curves': 1.6.0 - '@noble/hashes': 1.5.0 '@solana/buffer-layout': 4.0.1 - agentkeepalive: 4.5.0 - bigint-buffer: 1.1.5 - bn.js: 5.2.1 - borsh: 0.7.0 - bs58: 4.0.1 + '@solana/buffer-layout-utils': 0.2.0 + '@solana/spl-token-metadata': 0.1.6(@solana/web3.js@1.92.3)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@4.9.5) + '@solana/web3.js': 1.92.3 buffer: 6.0.3 - fast-stable-stringify: 1.0.0 - 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 transitivePeerDependencies: - bufferutil - encoding + - fastestsmallesttextencoderdecoder + - typescript - utf-8-validate + dev: false - '@solana/web3.js@1.95.3(bufferutil@4.0.8)(utf-8-validate@5.0.10)': + /@solana/web3.js@1.92.3: + resolution: {integrity: sha512-NVBWvb9zdJIAx6X+caXaIICCEQfQaQ8ygykCjJW4u2z/sIKcvPj3ZIIllnx0MWMc3IxGq15ozGYDOQIMbwUcHw==} dependencies: - '@babel/runtime': 7.25.6 - '@noble/curves': 1.6.0 - '@noble/hashes': 1.5.0 + '@babel/runtime': 7.26.0 + '@noble/curves': 1.7.0 + '@noble/hashes': 1.6.1 '@solana/buffer-layout': 4.0.1 agentkeepalive: 4.5.0 bigint-buffer: 1.1.5 @@ -1027,191 +272,294 @@ snapshots: bs58: 4.0.1 buffer: 6.0.3 fast-stable-stringify: 1.0.0 - jayson: 4.1.2(bufferutil@4.0.8)(utf-8-validate@5.0.10) + jayson: 4.1.3 node-fetch: 2.7.0 - rpc-websockets: 9.0.2 - superstruct: 2.0.2 + rpc-websockets: 8.0.2 + superstruct: 1.0.4 transitivePeerDependencies: - bufferutil - encoding - utf-8-validate + dev: false - '@swc/helpers@0.5.13': - dependencies: - tslib: 2.7.0 - - '@types/chai@4.3.12': {} + /@types/chai@4.3.20: + resolution: {integrity: sha512-/pC9HAB5I/xMlc5FP77qjCnI16ChlJfW0tGa0IUcFn38VJrTV6DeZ60NU5KZBtaOZqjdpwTWohz5HU1RrhiYxQ==} + dev: false - '@types/connect@3.4.38': + /@types/connect@3.4.38: + resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==} dependencies: - '@types/node': 22.6.0 + '@types/node': 14.18.63 + dev: false - '@types/json5@0.0.29': + /@types/json5@0.0.29: + resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} + requiresBuild: true + dev: false optional: true - '@types/mocha@9.1.1': {} - - '@types/node@12.20.55': {} - - '@types/node@14.18.63': {} + /@types/mocha@9.1.1: + resolution: {integrity: sha512-Z61JK7DKDtdKTWwLeElSEBcWGRLY8g95ic5FoQqI9CMx0ns/Ghep3B4DfcEimiKMvtamNVULVNKEsiwV3aQmXw==} + dev: false - '@types/node@22.6.0': - dependencies: - undici-types: 6.19.8 + /@types/node@12.20.55: + resolution: {integrity: sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==} + dev: false - '@types/pbkdf2@3.1.2': - dependencies: - '@types/node': 22.6.0 + /@types/node@14.18.63: + resolution: {integrity: sha512-fAtCfv4jJg+ExtXhvCkCqUKZ+4ok/JQk01qDKhL5BDDoS3AxKXhV5/MAVUZyQnSEd2GT92fkgZl0pz0Q0AzcIQ==} + dev: false - '@types/secp256k1@4.0.6': + /@types/pbkdf2@3.1.2: + resolution: {integrity: sha512-uRwJqmiXmh9++aSu1VNEn3iIxWOhd8AHXNSdlaLfdAAdSTY9jYVeGWnzejM3dvrkbqE3/hyQkQQ29IFATEGlew==} dependencies: - '@types/node': 22.6.0 + '@types/node': 14.18.63 + dev: false - '@types/uuid@8.3.4': {} - - '@types/ws@7.4.7': + /@types/secp256k1@4.0.6: + resolution: {integrity: sha512-hHxJU6PAEUn0TP4S/ZOzuTUvJWuZ6eIKeNKb5RBpODvSl6hp1Wrw4s7ATY50rklRCScUDpHzVA/DQdSjJ3UoYQ==} dependencies: - '@types/node': 22.6.0 + '@types/node': 14.18.63 + dev: false - '@types/ws@8.5.12': + /@types/ws@7.4.7: + resolution: {integrity: sha512-JQbbmxZTZehdc2iszGKs5oC3NFnjeay7mtAWrdt7qNtAVK0g19muApzAy4bm9byz79xa2ZnO/BOBC2R8RC5Lww==} dependencies: - '@types/node': 22.6.0 + '@types/node': 14.18.63 + dev: false - '@ungap/promise-all-settled@1.1.2': {} + /@ungap/promise-all-settled@1.1.2: + resolution: {integrity: sha512-sL/cEvJWAnClXw0wHk85/2L0G6Sj8UB0Ctc1TEMbKSsmpRosqhwj9gWgFRZSrBr2f9tiXISwNhCPmlfqUqyb9Q==} + dev: false - JSONStream@1.3.5: + /JSONStream@1.3.5: + resolution: {integrity: sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ==} + hasBin: true dependencies: jsonparse: 1.3.1 through: 2.3.8 + dev: false - agentkeepalive@4.5.0: + /agentkeepalive@4.5.0: + resolution: {integrity: sha512-5GG/5IbQQpC9FpkRGsSvZI5QYeSCzlJHdpBQntCsuTOxhKD8lqKhrleg2Yi7yvMIf82Ycmmqln9U8V9qwEiJew==} + engines: {node: '>= 8.0.0'} dependencies: humanize-ms: 1.2.1 + dev: false - ansi-colors@4.1.1: {} + /ansi-colors@4.1.1: + resolution: {integrity: sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==} + engines: {node: '>=6'} + dev: false - ansi-regex@5.0.1: {} + /ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + dev: false - ansi-styles@4.3.0: + /ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} dependencies: color-convert: 2.0.1 + dev: false - anymatch@3.1.3: + /anymatch@3.1.3: + resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} + engines: {node: '>= 8'} dependencies: normalize-path: 3.0.0 picomatch: 2.3.1 + dev: false - argparse@2.0.1: {} + /argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + dev: false - arrify@1.0.1: {} + /arrify@1.0.1: + resolution: {integrity: sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA==} + engines: {node: '>=0.10.0'} + dev: false - assertion-error@1.1.0: {} + /assertion-error@1.1.0: + resolution: {integrity: sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==} + dev: false - balanced-match@1.0.2: {} + /balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + dev: false - base-x@3.0.10: + /base-x@3.0.10: + resolution: {integrity: sha512-7d0s06rR9rYaIWHkpfLIFICM/tkSVdoPC9qYAQRpxn9DdKNWNsKC0uk++akckyLq16Tx2WIinnZ6WRriAt6njQ==} dependencies: safe-buffer: 5.2.1 + dev: false - base64-js@1.5.1: {} + /base64-js@1.5.1: + resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + dev: false - bigint-buffer@1.1.5: + /bigint-buffer@1.1.5: + resolution: {integrity: sha512-trfYco6AoZ+rKhKnxA0hgX0HAbVP/s808/EuDSe2JDzUnCp/xAsli35Orvk67UrTEcwuxZqYZDmfA2RXJgxVvA==} + engines: {node: '>= 10.0.0'} + requiresBuild: true dependencies: bindings: 1.5.0 + dev: false - bignumber.js@9.1.2: {} + /bignumber.js@9.1.2: + resolution: {integrity: sha512-2/mKyZH9K85bzOEfhXDBFZTGd1CTs+5IHpeFQo9luiBG7hghdC851Pj2WAhb6E3R6b9tZj/XKhbg4fum+Kepug==} + dev: false - binary-extensions@2.3.0: {} + /binary-extensions@2.3.0: + resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} + engines: {node: '>=8'} + dev: false - bindings@1.5.0: + /bindings@1.5.0: + resolution: {integrity: sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==} dependencies: file-uri-to-path: 1.0.0 + dev: false - blakejs@1.2.1: {} + /blakejs@1.2.1: + resolution: {integrity: sha512-QXUSXI3QVc/gJME0dBpXrag1kbzOqCjCX8/b54ntNyW6sjtoqxqRk3LTmXzaJoh71zMsDCjM+47jS7XiwN/+fQ==} + dev: false - bn.js@4.12.0: {} + /bn.js@4.12.1: + resolution: {integrity: sha512-k8TVBiPkPJT9uHLdOKfFpqcfprwBFOAAXXozRubr7R7PfIuKvQlzcI4M0pALeqXN09vdaMbUdUj+pass+uULAg==} + dev: false - bn.js@5.2.1: {} + /bn.js@5.2.1: + resolution: {integrity: sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ==} + dev: false - borsh@0.7.0: + /borsh@0.7.0: + resolution: {integrity: sha512-CLCsZGIBCFnPtkNnieW/a8wmreDmfUtjU2m9yHrzPXIlNbqVs0AQrSatSG6vdNYUqdc83tkQi2eHfF98ubzQLA==} dependencies: bn.js: 5.2.1 bs58: 4.0.1 text-encoding-utf-8: 1.0.2 + dev: false - brace-expansion@1.1.11: + /brace-expansion@1.1.11: + resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} dependencies: balanced-match: 1.0.2 concat-map: 0.0.1 + dev: false - braces@3.0.2: + /braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} dependencies: - fill-range: 7.0.1 + fill-range: 7.1.1 + dev: false - brorand@1.1.0: {} + /brorand@1.1.0: + resolution: {integrity: sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w==} + dev: false - browser-stdout@1.3.1: {} + /browser-stdout@1.3.1: + resolution: {integrity: sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==} + dev: false - browserify-aes@1.2.0: + /browserify-aes@1.2.0: + resolution: {integrity: sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==} dependencies: buffer-xor: 1.0.3 - cipher-base: 1.0.4 + cipher-base: 1.0.6 create-hash: 1.2.0 evp_bytestokey: 1.0.3 inherits: 2.0.4 safe-buffer: 5.2.1 + dev: false - bs58@4.0.1: + /bs58@4.0.1: + resolution: {integrity: sha512-Ok3Wdf5vOIlBrgCvTq96gBkJw+JUEzdBgyaza5HLtPm7yTHkjRy8+JzNyHF7BHa0bNWOQIp3m5YF0nnFcOIKLw==} dependencies: base-x: 3.0.10 + dev: false - bs58check@2.1.2: + /bs58check@2.1.2: + resolution: {integrity: sha512-0TS1jicxdU09dwJMNZtVAfzPi6Q6QeN0pM1Fkzrjn+XYHvzMKPU3pHVpva+769iNVSfIYWf7LJ6WR+BuuMf8cA==} dependencies: bs58: 4.0.1 create-hash: 1.2.0 safe-buffer: 5.2.1 + dev: false - buffer-from@1.1.2: {} + /buffer-from@1.1.2: + resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + dev: false - buffer-layout@1.2.2: {} + /buffer-layout@1.2.2: + resolution: {integrity: sha512-kWSuLN694+KTk8SrYvCqwP2WcgQjoRCiF5b4QDvkkz8EmgD+aWAIceGFKMIAdmF/pH+vpgNV3d3kAKorcdAmWA==} + engines: {node: '>=4.5'} + dev: false - buffer-xor@1.0.3: {} + /buffer-xor@1.0.3: + resolution: {integrity: sha512-571s0T7nZWK6vB67HI5dyUF7wXiNcfaPPPTl6zYCNApANjIvYJTg7hlud/+cJpdAhS7dVzqMLmfhfHR3rAcOjQ==} + dev: false - buffer@6.0.3: + /buffer@6.0.3: + resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==} dependencies: base64-js: 1.5.1 ieee754: 1.2.1 + dev: false - bufferutil@4.0.8: + /bufferutil@4.0.8: + resolution: {integrity: sha512-4T53u4PdgsXqKaIctwF8ifXlRTTmEPJ8iEPWFdGZvcf7sbwYo6FKFEX9eNNAnzFZ7EzJAQ3CJeOtCRA4rDp7Pw==} + engines: {node: '>=6.14.2'} + requiresBuild: true dependencies: - node-gyp-build: 4.8.2 - optional: true + node-gyp-build: 4.8.4 + dev: false - camelcase@6.3.0: {} + /camelcase@6.3.0: + resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==} + engines: {node: '>=10'} + dev: false - chai@4.4.1: + /chai@4.5.0: + resolution: {integrity: sha512-RITGBfijLkBddZvnn8jdqoTypxvqbOLYQkGGxXzeFjVHvudaPw0HNFD9x928/eUwYWd2dPCugVqspGALTZZQKw==} + engines: {node: '>=4'} dependencies: assertion-error: 1.1.0 check-error: 1.0.3 - deep-eql: 4.1.3 + deep-eql: 4.1.4 get-func-name: 2.0.2 loupe: 2.3.7 pathval: 1.1.1 - type-detect: 4.0.8 + type-detect: 4.1.0 + dev: false - chalk@4.1.2: + /chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} dependencies: ansi-styles: 4.3.0 supports-color: 7.2.0 + dev: false - check-error@1.0.3: + /chalk@5.3.0: + resolution: {integrity: sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==} + engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} + dev: false + + /check-error@1.0.3: + resolution: {integrity: sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==} dependencies: get-func-name: 2.0.2 + dev: false - chokidar@3.5.3: + /chokidar@3.5.3: + resolution: {integrity: sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==} + engines: {node: '>= 8.10.0'} dependencies: anymatch: 3.1.3 - braces: 3.0.2 + braces: 3.0.3 glob-parent: 5.1.2 is-binary-path: 2.1.0 is-glob: 4.0.3 @@ -1219,99 +567,167 @@ snapshots: readdirp: 3.6.0 optionalDependencies: fsevents: 2.3.3 + dev: false - cipher-base@1.0.4: + /cipher-base@1.0.6: + resolution: {integrity: sha512-3Ek9H3X6pj5TgenXYtNWdaBon1tgYCaebd+XPg0keyjEbEfkD4KkmAxkQ/i1vYvxdcT5nscLBfq9VJRmCBcFSw==} + engines: {node: '>= 0.10'} dependencies: inherits: 2.0.4 safe-buffer: 5.2.1 + dev: false - cliui@7.0.4: + /cliui@7.0.4: + resolution: {integrity: sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==} dependencies: string-width: 4.2.3 strip-ansi: 6.0.1 wrap-ansi: 7.0.0 + dev: false - color-convert@2.0.1: + /color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} dependencies: color-name: 1.1.4 + dev: false + + /color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + dev: false - color-name@1.1.4: {} + /commander@12.1.0: + resolution: {integrity: sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==} + engines: {node: '>=18'} + dev: false - commander@2.20.3: {} + /commander@2.20.3: + resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} + dev: false - concat-map@0.0.1: {} + /concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + dev: false - create-hash@1.2.0: + /create-hash@1.2.0: + resolution: {integrity: sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==} dependencies: - cipher-base: 1.0.4 + cipher-base: 1.0.6 inherits: 2.0.4 md5.js: 1.3.5 ripemd160: 2.0.2 sha.js: 2.4.11 + dev: false - create-hmac@1.1.7: + /create-hmac@1.1.7: + resolution: {integrity: sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==} dependencies: - cipher-base: 1.0.4 + cipher-base: 1.0.6 create-hash: 1.2.0 inherits: 2.0.4 ripemd160: 2.0.2 safe-buffer: 5.2.1 sha.js: 2.4.11 + dev: false - cross-fetch@3.1.8: + /cross-fetch@3.1.8: + resolution: {integrity: sha512-cvA+JwZoU0Xq+h6WkMvAUqPEYy92Obet6UdKLfW60qn99ftItKjB5T+BkyWOFWe2pUyfQ+IJHmpOTznqk1M6Kg==} dependencies: node-fetch: 2.7.0 transitivePeerDependencies: - encoding + dev: false - crypto-hash@1.3.0: {} + /crypto-hash@1.3.0: + resolution: {integrity: sha512-lyAZ0EMyjDkVvz8WOeVnuCPvKVBXcMv1l5SVqO1yC7PzTwrD/pPje/BIRbWhMoPe436U+Y2nD7f5bFx0kt+Sbg==} + engines: {node: '>=8'} + dev: false - debug@4.3.3(supports-color@8.1.1): + /debug@4.3.3(supports-color@8.1.1): + resolution: {integrity: sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true dependencies: ms: 2.1.2 - optionalDependencies: supports-color: 8.1.1 + dev: false - decamelize@4.0.0: {} + /decamelize@4.0.0: + resolution: {integrity: sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==} + engines: {node: '>=10'} + dev: false - deep-eql@4.1.3: + /deep-eql@4.1.4: + resolution: {integrity: sha512-SUwdGfqdKOwxCPeVYjwSyRpJ7Z+fhpwIAtmCUdZIWZ/YP5R9WAsyuSgpLVDi9bjWoN2LXHNss/dk3urXtdQxGg==} + engines: {node: '>=6'} dependencies: - type-detect: 4.0.8 + type-detect: 4.1.0 + dev: false - delay@5.0.0: {} + /delay@5.0.0: + resolution: {integrity: sha512-ReEBKkIfe4ya47wlPYf/gu5ib6yUG0/Aez0JQZQz94kiWtRQvZIQbTiehsnwHvLSWJnQdhVeqYue7Id1dKr0qw==} + engines: {node: '>=10'} + dev: false - diff@3.5.0: {} + /diff@3.5.0: + resolution: {integrity: sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==} + engines: {node: '>=0.3.1'} + dev: false - diff@5.0.0: {} + /diff@5.0.0: + resolution: {integrity: sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==} + engines: {node: '>=0.3.1'} + dev: false - dot-case@3.0.4: + /dot-case@3.0.4: + resolution: {integrity: sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==} dependencies: no-case: 3.0.4 - tslib: 2.7.0 + tslib: 2.8.1 + dev: false - elliptic@6.5.5: + /elliptic@6.6.1: + resolution: {integrity: sha512-RaddvvMatK2LJHqFJ+YA4WysVN5Ita9E35botqIYspQ4TkRAlCicdzKOjlyv/1Za5RyTNn7di//eEV0uTAfe3g==} dependencies: - bn.js: 4.12.0 + bn.js: 4.12.1 brorand: 1.1.0 hash.js: 1.1.7 hmac-drbg: 1.0.1 inherits: 2.0.4 minimalistic-assert: 1.0.1 minimalistic-crypto-utils: 1.0.1 + dev: false - emoji-regex@8.0.0: {} + /emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + dev: false - es6-promise@4.2.8: {} + /es6-promise@4.2.8: + resolution: {integrity: sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==} + dev: false - es6-promisify@5.0.0: + /es6-promisify@5.0.0: + resolution: {integrity: sha512-C+d6UdsYDk0lMebHNR4S2NybQMMngAOnOwYBQjTOiv0MkoJMP0Myw2mgpDLBcpfCmRLxyFqYhS/CfOENq4SJhQ==} dependencies: es6-promise: 4.2.8 + dev: false - escalade@3.1.2: {} + /escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + dev: false - escape-string-regexp@4.0.0: {} + /escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + dev: false - ethereum-cryptography@0.1.3: + /ethereum-cryptography@0.1.3: + resolution: {integrity: sha512-w8/4x1SGGzc+tO97TASLja6SLd3fRIK2tLVcV2Gx4IB21hE19atll5Cq9o3d0ZmAYC/8aw0ipieTSiekAea4SQ==} dependencies: '@types/pbkdf2': 3.1.2 '@types/secp256k1': 4.0.6 @@ -1326,51 +742,89 @@ snapshots: randombytes: 2.1.0 safe-buffer: 5.2.1 scrypt-js: 3.0.1 - secp256k1: 4.0.3 + secp256k1: 4.0.4 setimmediate: 1.0.5 + dev: false - eventemitter3@4.0.7: {} - - eventemitter3@5.0.1: {} + /eventemitter3@4.0.7: + resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==} + dev: false - evp_bytestokey@1.0.3: + /evp_bytestokey@1.0.3: + resolution: {integrity: sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA==} dependencies: md5.js: 1.3.5 safe-buffer: 5.2.1 + dev: false - eyes@0.1.8: {} + /eyes@0.1.8: + resolution: {integrity: sha512-GipyPsXO1anza0AOZdy69Im7hGFCNB7Y/NGjDlZGJ3GJJLtwNSb2vrzYrTYJRrRloVx7pl+bhUaTB8yiccPvFQ==} + engines: {node: '> 0.1.90'} + dev: false - fast-stable-stringify@1.0.0: {} + /fast-stable-stringify@1.0.0: + resolution: {integrity: sha512-wpYMUmFu5f00Sm0cj2pfivpmawLZ0NKdviQ4w9zJeR8JVtOpOxHmLaJuj0vxvGqMJQWyP/COUkF75/57OKyRag==} + dev: false - fastestsmallesttextencoderdecoder@1.0.22: {} + /fastestsmallesttextencoderdecoder@1.0.22: + resolution: {integrity: sha512-Pb8d48e+oIuY4MaM64Cd7OW1gt4nxCHs7/ddPPZ/Ic3sg8yVGM7O9wDvZ7us6ScaUupzM+pfBolwtYhN1IxBIw==} + dev: false - file-uri-to-path@1.0.0: {} + /file-uri-to-path@1.0.0: + resolution: {integrity: sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==} + dev: false - fill-range@7.0.1: + /fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} dependencies: to-regex-range: 5.0.1 + dev: false - find-up@5.0.0: + /find-up@5.0.0: + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} + engines: {node: '>=10'} dependencies: locate-path: 6.0.0 path-exists: 4.0.0 + dev: false - flat@5.0.2: {} + /flat@5.0.2: + resolution: {integrity: sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==} + hasBin: true + dev: false - fs.realpath@1.0.0: {} + /fs.realpath@1.0.0: + resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} + dev: false - fsevents@2.3.3: + /fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + requiresBuild: true + dev: false optional: true - get-caller-file@2.0.5: {} + /get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + dev: false - get-func-name@2.0.2: {} + /get-func-name@2.0.2: + resolution: {integrity: sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==} + dev: false - glob-parent@5.1.2: + /glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} dependencies: is-glob: 4.0.3 + dev: false - glob@7.2.0: + /glob@7.2.0: + resolution: {integrity: sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==} + deprecated: Glob versions prior to v9 are no longer supported dependencies: fs.realpath: 1.0.0 inflight: 1.0.6 @@ -1378,68 +832,124 @@ snapshots: minimatch: 3.1.2 once: 1.4.0 path-is-absolute: 1.0.1 + dev: false - growl@1.10.5: {} + /growl@1.10.5: + resolution: {integrity: sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==} + engines: {node: '>=4.x'} + dev: false - has-flag@4.0.0: {} + /has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + dev: false - hash-base@3.1.0: + /hash-base@3.1.0: + resolution: {integrity: sha512-1nmYp/rhMDiE7AYkDw+lLwlAzz0AntGIe51F3RfFfEqyQ3feY2eI/NcwC6umIQVOASPMsWJLJScWKSSvzL9IVA==} + engines: {node: '>=4'} dependencies: inherits: 2.0.4 readable-stream: 3.6.2 safe-buffer: 5.2.1 + dev: false - hash.js@1.1.7: + /hash.js@1.1.7: + resolution: {integrity: sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==} dependencies: inherits: 2.0.4 minimalistic-assert: 1.0.1 + dev: false - he@1.2.0: {} + /he@1.2.0: + resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==} + hasBin: true + dev: false - hmac-drbg@1.0.1: + /hmac-drbg@1.0.1: + resolution: {integrity: sha512-Tti3gMqLdZfhOQY1Mzf/AanLiqh1WTiJgEj26ZuYQ9fbkLomzGchCws4FyrSd4VkpBfiNhaE1On+lOz894jvXg==} dependencies: hash.js: 1.1.7 minimalistic-assert: 1.0.1 minimalistic-crypto-utils: 1.0.1 + dev: false - humanize-ms@1.2.1: + /humanize-ms@1.2.1: + resolution: {integrity: sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==} dependencies: ms: 2.1.3 + dev: false - ieee754@1.2.1: {} + /ieee754@1.2.1: + resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} + dev: false - inflight@1.0.6: + /inflight@1.0.6: + resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} + deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. dependencies: once: 1.4.0 wrappy: 1.0.2 + dev: false - inherits@2.0.4: {} + /inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + dev: false - is-binary-path@2.1.0: + /is-binary-path@2.1.0: + resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} + engines: {node: '>=8'} dependencies: binary-extensions: 2.3.0 + dev: false - is-extglob@2.1.1: {} + /is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + dev: false - is-fullwidth-code-point@3.0.0: {} + /is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + dev: false - is-glob@4.0.3: + /is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} dependencies: is-extglob: 2.1.1 + dev: false - is-number@7.0.0: {} + /is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + dev: false - is-plain-obj@2.1.0: {} + /is-plain-obj@2.1.0: + resolution: {integrity: sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==} + engines: {node: '>=8'} + dev: false - is-unicode-supported@0.1.0: {} + /is-unicode-supported@0.1.0: + resolution: {integrity: sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==} + engines: {node: '>=10'} + dev: false - isexe@2.0.0: {} + /isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + dev: false - isomorphic-ws@4.0.1(ws@7.5.10(bufferutil@4.0.8)(utf-8-validate@5.0.10)): + /isomorphic-ws@4.0.1(ws@7.5.10): + resolution: {integrity: sha512-BhBvN2MBpWTaSHdWRb/bwdZJ1WaehQ2L1KngkCkfLUGF0mAWAT1sQUQacEmQ0jXkFw/czDXPNQSL5u2/Krsz1w==} + peerDependencies: + ws: '*' dependencies: - ws: 7.5.10(bufferutil@4.0.8)(utf-8-validate@5.0.10) + ws: 7.5.10 + dev: false - jayson@4.1.2(bufferutil@4.0.8)(utf-8-validate@5.0.10): + /jayson@4.1.3: + resolution: {integrity: sha512-LtXh5aYZodBZ9Fc3j6f2w+MTNcnxteMOrb+QgIouguGOulWi0lieEkOUg+HkjjFs0DGoWDds6bi4E9hpNFLulQ==} + engines: {node: '>=8'} + hasBin: true dependencies: '@types/connect': 3.4.38 '@types/node': 12.20.55 @@ -1449,77 +959,125 @@ snapshots: delay: 5.0.0 es6-promisify: 5.0.0 eyes: 0.1.8 - isomorphic-ws: 4.0.1(ws@7.5.10(bufferutil@4.0.8)(utf-8-validate@5.0.10)) + isomorphic-ws: 4.0.1(ws@7.5.10) json-stringify-safe: 5.0.1 uuid: 8.3.2 - ws: 7.5.10(bufferutil@4.0.8)(utf-8-validate@5.0.10) + ws: 7.5.10 transitivePeerDependencies: - bufferutil - utf-8-validate + dev: false - js-yaml@4.1.0: + /js-yaml@4.1.0: + resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} + hasBin: true dependencies: argparse: 2.0.1 + dev: false - json-stringify-safe@5.0.1: {} + /json-stringify-safe@5.0.1: + resolution: {integrity: sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==} + dev: false - json5@1.0.2: + /json5@1.0.2: + resolution: {integrity: sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==} + hasBin: true + requiresBuild: true dependencies: minimist: 1.2.8 + dev: false optional: true - jsonparse@1.3.1: {} + /jsonparse@1.3.1: + resolution: {integrity: sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==} + engines: {'0': node >= 0.2.0} + dev: false - keccak@3.0.4: + /keccak@3.0.4: + resolution: {integrity: sha512-3vKuW0jV8J3XNTzvfyicFR5qvxrSAGl7KIhvgOu5cmWwM7tZRj3fMbj/pfIf4be7aznbc+prBWGjywox/g2Y6Q==} + engines: {node: '>=10.0.0'} + requiresBuild: true dependencies: node-addon-api: 2.0.2 - node-gyp-build: 4.8.2 + node-gyp-build: 4.8.4 readable-stream: 3.6.2 + dev: false - locate-path@6.0.0: + /locate-path@6.0.0: + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + engines: {node: '>=10'} dependencies: p-locate: 5.0.0 + dev: false - log-symbols@4.1.0: + /log-symbols@4.1.0: + resolution: {integrity: sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==} + engines: {node: '>=10'} dependencies: chalk: 4.1.2 is-unicode-supported: 0.1.0 + dev: false - loupe@2.3.7: + /loupe@2.3.7: + resolution: {integrity: sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==} dependencies: get-func-name: 2.0.2 + dev: false - lower-case@2.0.2: + /lower-case@2.0.2: + resolution: {integrity: sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==} dependencies: - tslib: 2.7.0 + tslib: 2.8.1 + dev: false - make-error@1.3.6: {} + /make-error@1.3.6: + resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==} + dev: false - md5.js@1.3.5: + /md5.js@1.3.5: + resolution: {integrity: sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==} dependencies: hash-base: 3.1.0 inherits: 2.0.4 safe-buffer: 5.2.1 + dev: false - minimalistic-assert@1.0.1: {} + /minimalistic-assert@1.0.1: + resolution: {integrity: sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==} + dev: false - minimalistic-crypto-utils@1.0.1: {} + /minimalistic-crypto-utils@1.0.1: + resolution: {integrity: sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg==} + dev: false - minimatch@3.1.2: + /minimatch@3.1.2: + resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} dependencies: brace-expansion: 1.1.11 + dev: false - minimatch@4.2.1: + /minimatch@4.2.1: + resolution: {integrity: sha512-9Uq1ChtSZO+Mxa/CL1eGizn2vRn3MlLgzhT0Iz8zaY8NdvxvB0d5QdPFmCKf7JKA9Lerx5vRrnwO03jsSfGG9g==} + engines: {node: '>=10'} dependencies: brace-expansion: 1.1.11 + dev: false - minimist@1.2.8: {} + /minimist@1.2.8: + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + dev: false - mkdirp@0.5.6: + /mkdirp@0.5.6: + resolution: {integrity: sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==} + hasBin: true dependencies: minimist: 1.2.8 + dev: false - mocha@9.2.2: + /mocha@9.2.2: + resolution: {integrity: sha512-L6XC3EdwT6YrIk0yXpavvLkn8h+EU+Y5UcCHKECyMbdUIxyMuZj4bX4U9e1nvnvUUvQVsV2VHQr5zLdcUkhW/g==} + engines: {node: '>= 12.0.0'} + hasBin: true dependencies: '@ungap/promise-all-settled': 1.1.2 ansi-colors: 4.1.1 @@ -1545,94 +1103,171 @@ snapshots: yargs: 16.2.0 yargs-parser: 20.2.4 yargs-unparser: 2.0.0 + dev: false - ms@2.1.2: {} + /ms@2.1.2: + resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} + dev: false - ms@2.1.3: {} + /ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + dev: false - nanoid@3.3.1: {} + /nanoid@3.3.1: + resolution: {integrity: sha512-n6Vs/3KGyxPQd6uO0eH4Bv0ojGSUvuLlIHtC3Y0kEO23YRge8H9x1GCzLn28YX0H66pMkxuaeESFq4tKISKwdw==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + dev: false - no-case@3.0.4: + /no-case@3.0.4: + resolution: {integrity: sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==} dependencies: lower-case: 2.0.2 - tslib: 2.7.0 + tslib: 2.8.1 + dev: false + + /node-addon-api@2.0.2: + resolution: {integrity: sha512-Ntyt4AIXyaLIuMHF6IOoTakB3K+RWxwtsHNRxllEoA6vPwP9o4866g6YWDLUdnucilZhmkxiHwHr11gAENw+QA==} + dev: false - node-addon-api@2.0.2: {} + /node-addon-api@5.1.0: + resolution: {integrity: sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA==} + dev: false - node-fetch@2.7.0: + /node-fetch@2.7.0: + resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} + engines: {node: 4.x || >=6.0.0} + peerDependencies: + encoding: ^0.1.0 + peerDependenciesMeta: + encoding: + optional: true dependencies: whatwg-url: 5.0.0 + dev: false - node-gyp-build@4.8.2: {} + /node-gyp-build@4.8.4: + resolution: {integrity: sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==} + hasBin: true + dev: false - normalize-path@3.0.0: {} + /normalize-path@3.0.0: + resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} + engines: {node: '>=0.10.0'} + dev: false - once@1.4.0: + /once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} dependencies: wrappy: 1.0.2 + dev: false - p-limit@3.1.0: + /p-limit@3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} dependencies: yocto-queue: 0.1.0 + dev: false - p-locate@5.0.0: + /p-locate@5.0.0: + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + engines: {node: '>=10'} dependencies: p-limit: 3.1.0 + dev: false - pako@2.1.0: {} + /pako@2.1.0: + resolution: {integrity: sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==} + dev: false - path-exists@4.0.0: {} + /path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + dev: false - path-is-absolute@1.0.1: {} + /path-is-absolute@1.0.1: + resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} + engines: {node: '>=0.10.0'} + dev: false - pathval@1.1.1: {} + /pathval@1.1.1: + resolution: {integrity: sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==} + dev: false - pbkdf2@3.1.2: + /pbkdf2@3.1.2: + resolution: {integrity: sha512-iuh7L6jA7JEGu2WxDwtQP1ddOpaJNC4KlDEFfdQajSGgGPNi4OyDc2R7QnbY2bR9QjBVGwgvTdNJZoE7RaxUMA==} + engines: {node: '>=0.12'} dependencies: create-hash: 1.2.0 create-hmac: 1.1.7 ripemd160: 2.0.2 safe-buffer: 5.2.1 sha.js: 2.4.11 + dev: false - picomatch@2.3.1: {} + /picomatch@2.3.1: + resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} + engines: {node: '>=8.6'} + dev: false - prettier@2.8.8: {} + /prettier@2.8.8: + resolution: {integrity: sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==} + engines: {node: '>=10.13.0'} + hasBin: true + dev: false - randombytes@2.1.0: + /randombytes@2.1.0: + resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==} dependencies: safe-buffer: 5.2.1 + dev: false - readable-stream@3.6.2: + /readable-stream@3.6.2: + resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} + engines: {node: '>= 6'} dependencies: inherits: 2.0.4 string_decoder: 1.3.0 util-deprecate: 1.0.2 + dev: false - readdirp@3.6.0: + /readdirp@3.6.0: + resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} + engines: {node: '>=8.10.0'} dependencies: picomatch: 2.3.1 + dev: false - regenerator-runtime@0.14.1: {} + /regenerator-runtime@0.14.1: + resolution: {integrity: sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==} + dev: false - require-directory@2.1.1: {} + /require-directory@2.1.1: + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} + engines: {node: '>=0.10.0'} + dev: false - ripemd160@2.0.2: + /ripemd160@2.0.2: + resolution: {integrity: sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA==} dependencies: hash-base: 3.1.0 inherits: 2.0.4 + dev: false - rpc-websockets@7.10.0: + /rpc-websockets@7.10.0: + resolution: {integrity: sha512-cemZ6RiDtYZpPiBzYijdOrkQQzmBCmug0E9SdRH2gIUNT15ql4mwCYWIp0VnSZq6Qrw/JkGUygp4PrK1y9KfwQ==} dependencies: - '@babel/runtime': 7.25.6 + '@babel/runtime': 7.26.0 eventemitter3: 4.0.7 uuid: 8.3.2 ws: 8.18.0(bufferutil@4.0.8)(utf-8-validate@5.0.10) optionalDependencies: bufferutil: 4.0.8 utf-8-validate: 5.0.10 + dev: false - rpc-websockets@8.0.1: + /rpc-websockets@8.0.2: + resolution: {integrity: sha512-QZ8lneJTtIZTf9JBcdUn/im2qDynWRYPKtmF6P9DqtdzqSLebcllYWVQr5aQacAp7LBYPReOW9Ses98dNfO7cA==} dependencies: eventemitter3: 4.0.7 uuid: 8.3.2 @@ -1640,106 +1275,160 @@ snapshots: optionalDependencies: bufferutil: 4.0.8 utf-8-validate: 5.0.10 + dev: false - rpc-websockets@9.0.2: - dependencies: - '@swc/helpers': 0.5.13 - '@types/uuid': 8.3.4 - '@types/ws': 8.5.12 - buffer: 6.0.3 - eventemitter3: 5.0.1 - uuid: 8.3.2 - ws: 8.18.0(bufferutil@4.0.8)(utf-8-validate@5.0.10) - optionalDependencies: - bufferutil: 4.0.8 - utf-8-validate: 5.0.10 - - safe-buffer@5.2.1: {} + /safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + dev: false - scrypt-js@3.0.1: {} + /scrypt-js@3.0.1: + resolution: {integrity: sha512-cdwTTnqPu0Hyvf5in5asVdZocVDTNRmR7XEcJuIzMjJeSHybHl7vpB66AzwTaIg6CLSbtjcxc8fqcySfnTkccA==} + dev: false - secp256k1@4.0.3: + /secp256k1@4.0.4: + resolution: {integrity: sha512-6JfvwvjUOn8F/jUoBY2Q1v5WY5XS+rj8qSe0v8Y4ezH4InLgTEeOOPQsRll9OV429Pvo6BCHGavIyJfr3TAhsw==} + engines: {node: '>=18.0.0'} + requiresBuild: true dependencies: - elliptic: 6.5.5 - node-addon-api: 2.0.2 - node-gyp-build: 4.8.2 + elliptic: 6.6.1 + node-addon-api: 5.1.0 + node-gyp-build: 4.8.4 + dev: false - serialize-javascript@6.0.0: + /serialize-javascript@6.0.0: + resolution: {integrity: sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==} dependencies: randombytes: 2.1.0 + dev: false - setimmediate@1.0.5: {} + /setimmediate@1.0.5: + resolution: {integrity: sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==} + dev: false - sha.js@2.4.11: + /sha.js@2.4.11: + resolution: {integrity: sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==} + hasBin: true dependencies: inherits: 2.0.4 safe-buffer: 5.2.1 + dev: false - snake-case@3.0.4: + /snake-case@3.0.4: + resolution: {integrity: sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg==} dependencies: dot-case: 3.0.4 - tslib: 2.7.0 + tslib: 2.8.1 + dev: false - source-map-support@0.5.21: + /source-map-support@0.5.21: + resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} dependencies: buffer-from: 1.1.2 source-map: 0.6.1 + dev: false - source-map@0.6.1: {} + /source-map@0.6.1: + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + engines: {node: '>=0.10.0'} + dev: false - string-width@4.2.3: + /string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} dependencies: emoji-regex: 8.0.0 is-fullwidth-code-point: 3.0.0 strip-ansi: 6.0.1 + dev: false - string_decoder@1.3.0: + /string_decoder@1.3.0: + resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} dependencies: safe-buffer: 5.2.1 + dev: false - strip-ansi@6.0.1: + /strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} dependencies: ansi-regex: 5.0.1 + dev: false - strip-bom@3.0.0: + /strip-bom@3.0.0: + resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} + engines: {node: '>=4'} + requiresBuild: true + dev: false optional: true - strip-json-comments@3.1.1: {} - - superstruct@0.15.5: {} + /strip-json-comments@3.1.1: + resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} + engines: {node: '>=8'} + dev: false - superstruct@1.0.4: {} + /superstruct@0.15.5: + resolution: {integrity: sha512-4AOeU+P5UuE/4nOUkmcQdW5y7i9ndt1cQd/3iUe+LTz3RxESf/W/5lg4B74HbDMMv8PHnPnGCQFH45kBcrQYoQ==} + dev: false - superstruct@2.0.2: {} + /superstruct@1.0.4: + resolution: {integrity: sha512-7JpaAoX2NGyoFlI9NBh66BQXGONc+uE+MRS5i2iOBKuS4e+ccgMDjATgZldkah+33DakBxDHiss9kvUcGAO8UQ==} + engines: {node: '>=14.0.0'} + dev: false - supports-color@7.2.0: + /supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} dependencies: has-flag: 4.0.0 + dev: false - supports-color@8.1.1: + /supports-color@8.1.1: + resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==} + engines: {node: '>=10'} dependencies: has-flag: 4.0.0 + dev: false - text-encoding-utf-8@1.0.2: {} + /text-encoding-utf-8@1.0.2: + resolution: {integrity: sha512-8bw4MY9WjdsD2aMtO0OzOCY3pXGYNx2d2FfHRVUKkiCPDWjKuOlhLVASS+pD7VkLTVjW268LYJHwsnPFlBpbAg==} + dev: false - through@2.3.8: {} + /through@2.3.8: + resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==} + dev: false - to-regex-range@5.0.1: + /to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} dependencies: is-number: 7.0.0 + dev: false - toml@3.0.0: {} + /toml@3.0.0: + resolution: {integrity: sha512-y/mWCZinnvxjTKYhJ+pYxwD0mRLVvOtdS2Awbgxln6iEnt4rk0yBxeSBHkGJcPucRiG0e55mwWp+g/05rsrd6w==} + dev: false - tr46@0.0.3: {} + /tr46@0.0.3: + resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} + dev: false - ts-mocha@8.0.0(mocha@9.2.2): + /ts-mocha@8.0.0(mocha@9.2.2): + resolution: {integrity: sha512-Kou1yxTlubLnD5C3unlCVO7nh0HERTezjoVhVw/M5S1SqoUec0WgllQvPk3vzPMc6by8m6xD1uR1yRf8lnVUbA==} + engines: {node: '>= 6.X.X'} + hasBin: true + peerDependencies: + mocha: ^3.X.X || ^4.X.X || ^5.X.X || ^6.X.X || ^7.X.X || ^8.X.X dependencies: mocha: 9.2.2 ts-node: 7.0.1 optionalDependencies: tsconfig-paths: 3.15.0 + dev: false - ts-node@7.0.1: + /ts-node@7.0.1: + resolution: {integrity: sha512-BVwVbPJRspzNh2yfslyT1PSbl5uIk03EZlb493RKHN4qej/D06n1cEhjlOJG69oFsE7OT8XjpTUcYf6pKTLMhw==} + engines: {node: '>=4.2.0'} + hasBin: true dependencies: arrify: 1.0.1 buffer-from: 1.1.2 @@ -1749,86 +1438,155 @@ snapshots: mkdirp: 0.5.6 source-map-support: 0.5.21 yn: 2.0.0 + dev: false - tsconfig-paths@3.15.0: + /tsconfig-paths@3.15.0: + resolution: {integrity: sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==} + requiresBuild: true dependencies: '@types/json5': 0.0.29 json5: 1.0.2 minimist: 1.2.8 strip-bom: 3.0.0 + dev: false optional: true - tslib@2.7.0: {} - - type-detect@4.0.8: {} + /tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + dev: false - typescript@4.9.5: {} + /type-detect@4.1.0: + resolution: {integrity: sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw==} + engines: {node: '>=4'} + dev: false - undici-types@6.19.8: {} + /typescript@4.9.5: + resolution: {integrity: sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==} + engines: {node: '>=4.2.0'} + hasBin: true + dev: false - utf-8-validate@5.0.10: + /utf-8-validate@5.0.10: + resolution: {integrity: sha512-Z6czzLq4u8fPOyx7TU6X3dvUZVvoJmxSQ+IcrlmagKhilxlhZgxPK6C5Jqbkw1IDUmFTM+cz9QDnnLTwDz/2gQ==} + engines: {node: '>=6.14.2'} + requiresBuild: true dependencies: - node-gyp-build: 4.8.2 - optional: true + node-gyp-build: 4.8.4 + dev: false - util-deprecate@1.0.2: {} + /util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + dev: false - uuid@8.3.2: {} + /uuid@8.3.2: + resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==} + hasBin: true + dev: false - webidl-conversions@3.0.1: {} + /webidl-conversions@3.0.1: + resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} + dev: false - whatwg-url@5.0.0: + /whatwg-url@5.0.0: + resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} dependencies: tr46: 0.0.3 webidl-conversions: 3.0.1 + dev: false - which@2.0.2: + /which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true dependencies: isexe: 2.0.0 + dev: false - workerpool@6.2.0: {} + /workerpool@6.2.0: + resolution: {integrity: sha512-Rsk5qQHJ9eowMH28Jwhe8HEbmdYDX4lwoMWshiCXugjtHqMD9ZbiqSDLxcsfdqsETPzVUtX5s1Z5kStiIM6l4A==} + dev: false - wrap-ansi@7.0.0: + /wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} dependencies: ansi-styles: 4.3.0 string-width: 4.2.3 strip-ansi: 6.0.1 + dev: false - wrappy@1.0.2: {} + /wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + dev: false - 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@7.5.10: + resolution: {integrity: sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==} + engines: {node: '>=8.3.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: ^5.0.2 + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + dev: false - ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@5.0.10): - optionalDependencies: + /ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@5.0.10): + resolution: {integrity: sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + dependencies: bufferutil: 4.0.8 utf-8-validate: 5.0.10 + dev: false - y18n@5.0.8: {} - - yargs-parser@20.2.4: {} + /y18n@5.0.8: + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} + engines: {node: '>=10'} + dev: false - yargs-parser@20.2.9: {} + /yargs-parser@20.2.4: + resolution: {integrity: sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==} + engines: {node: '>=10'} + dev: false - yargs-unparser@2.0.0: + /yargs-unparser@2.0.0: + resolution: {integrity: sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==} + engines: {node: '>=10'} dependencies: camelcase: 6.3.0 decamelize: 4.0.0 flat: 5.0.2 is-plain-obj: 2.1.0 + dev: false - yargs@16.2.0: + /yargs@16.2.0: + resolution: {integrity: sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==} + engines: {node: '>=10'} dependencies: cliui: 7.0.4 - escalade: 3.1.2 + escalade: 3.2.0 get-caller-file: 2.0.5 require-directory: 2.1.1 string-width: 4.2.3 y18n: 5.0.8 - yargs-parser: 20.2.9 + yargs-parser: 20.2.4 + dev: false - yn@2.0.0: {} + /yn@2.0.0: + resolution: {integrity: sha512-uTv8J/wiWTgUTg+9vLTi//leUl5vDQS6uii/emeTb2ssY7vl6QWf2fFbIIGjnhjvbdKlU0ed7QPgY1htTC86jQ==} + engines: {node: '>=4'} + dev: false - yocto-queue@0.1.0: {} + /yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + dev: false diff --git a/contracts/programs/contract-reader-interface/src/lib.rs b/contracts/programs/contract-reader-interface/src/lib.rs index 838190fe9..b02b68888 100644 --- a/contracts/programs/contract-reader-interface/src/lib.rs +++ b/contracts/programs/contract-reader-interface/src/lib.rs @@ -17,6 +17,19 @@ pub mod contract_reader_interface { Ok(()) } + + pub fn initialize_lookup_table( + ctx: Context, + lookup_table: Pubkey, + ) -> Result<()> { + let account = &mut ctx.accounts.write_data_account; + account.version = 1; + account.administrator = ctx.accounts.admin.key(); + account.pending_administrator = Pubkey::default(); + account.lookup_table = lookup_table; + + Ok(()) + } } #[derive(Accounts)] @@ -37,6 +50,34 @@ pub struct Initialize<'info> { pub system_program: Program<'info, System>, } +#[derive(Accounts)] +pub struct InitializeLookupTableData<'info> { + /// PDA for LookupTableDataAccount, derived from seeds and created by the System Program + #[account( + init, + payer = admin, + space = size_of::() + 8, + seeds = [b"data"], + bump + )] + pub write_data_account: Account<'info, LookupTableDataAccount>, + + /// Admin account that pays for PDA creation and signs the transaction + #[account(mut)] + pub admin: Signer<'info>, + + /// System Program required for PDA creation + pub system_program: Program<'info, System>, +} + +#[account] +pub struct LookupTableDataAccount { + pub version: u8, // Version of the data account + pub administrator: Pubkey, // Administrator public key + pub pending_administrator: Pubkey, // Pending administrator public key + pub lookup_table: Pubkey, // Address of the lookup table +} + #[account] pub struct DataAccount { pub idx: u64, diff --git a/contracts/programs/write_test/Cargo.toml b/contracts/programs/write_test/Cargo.toml deleted file mode 100644 index ee46888c6..000000000 --- a/contracts/programs/write_test/Cargo.toml +++ /dev/null @@ -1,19 +0,0 @@ -[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 deleted file mode 100644 index 475fb71ed..000000000 --- a/contracts/programs/write_test/Xargo.toml +++ /dev/null @@ -1,2 +0,0 @@ -[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 deleted file mode 100644 index 8d8fa3cac..000000000 --- a/contracts/programs/write_test/src/lib.rs +++ /dev/null @@ -1,51 +0,0 @@ -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/gotest.log b/gotest.log deleted file mode 100644 index 41b099eb4..000000000 --- a/gotest.log +++ /dev/null @@ -1,92 +0,0 @@ -📦 github.com/smartcontractkit/chainlink-solana/pkg/solana/chainwriter -exit status 1 - ❌ TestLookupTables (30.04s) - ports.go:37: found open port: 41544 - ports.go:37: found open port: 39418 - utils.go:215: API server not ready yet (attempt 1) - utils.go:215: Error from API server: rpc call getHealth() on http://127.0.0.1:41544: Post "http://127.0.0.1:41544": dial tcp 127.0.0.1:41544: connect: connection refused - utils.go:215: API server not ready yet (attempt 2) - utils.go:215: Error from API server: rpc call getHealth() on http://127.0.0.1:41544: Post "http://127.0.0.1:41544": dial tcp 127.0.0.1:41544: connect: connection refused - utils.go:215: API server not ready yet (attempt 3) - utils.go:215: Error from API server: rpc call getHealth() on http://127.0.0.1:41544: Post "http://127.0.0.1:41544": dial tcp 127.0.0.1:41544: connect: connection refused - utils.go:215: API server not ready yet (attempt 4) - utils.go:215: Error from API server: rpc call getHealth() on http://127.0.0.1:41544: Post "http://127.0.0.1:41544": dial tcp 127.0.0.1:41544: connect: connection refused - utils.go:215: API server not ready yet (attempt 5) - utils.go:215: Error from API server: rpc call getHealth() on http://127.0.0.1:41544: Post "http://127.0.0.1:41544": dial tcp 127.0.0.1:41544: connect: connection refused - utils.go:215: API server not ready yet (attempt 6) - utils.go:215: Error from API server: rpc call getHealth() on http://127.0.0.1:41544: Post "http://127.0.0.1:41544": dial tcp 127.0.0.1:41544: connect: connection refused - utils.go:215: API server not ready yet (attempt 7) - utils.go:215: Error from API server: rpc call getHealth() on http://127.0.0.1:41544: Post "http://127.0.0.1:41544": dial tcp 127.0.0.1:41544: connect: connection refused - utils.go:215: API server not ready yet (attempt 8) - utils.go:215: Error from API server: rpc call getHealth() on http://127.0.0.1:41544: Post "http://127.0.0.1:41544": dial tcp 127.0.0.1:41544: connect: connection refused - utils.go:215: API server not ready yet (attempt 9) - utils.go:215: Error from API server: rpc call getHealth() on http://127.0.0.1:41544: Post "http://127.0.0.1:41544": dial tcp 127.0.0.1:41544: connect: connection refused - utils.go:215: API server not ready yet (attempt 10) - utils.go:215: Error from API server: rpc call getHealth() on http://127.0.0.1:41544: Post "http://127.0.0.1:41544": dial tcp 127.0.0.1:41544: connect: connection refused - utils.go:215: API server not ready yet (attempt 11) - utils.go:215: Error from API server: rpc call getHealth() on http://127.0.0.1:41544: Post "http://127.0.0.1:41544": dial tcp 127.0.0.1:41544: connect: connection refused - utils.go:215: API server not ready yet (attempt 12) - utils.go:215: Error from API server: rpc call getHealth() on http://127.0.0.1:41544: Post "http://127.0.0.1:41544": dial tcp 127.0.0.1:41544: connect: connection refused - utils.go:215: API server not ready yet (attempt 13) - utils.go:215: Error from API server: rpc call getHealth() on http://127.0.0.1:41544: Post "http://127.0.0.1:41544": dial tcp 127.0.0.1:41544: connect: connection refused - utils.go:215: API server not ready yet (attempt 14) - utils.go:215: Error from API server: rpc call getHealth() on http://127.0.0.1:41544: Post "http://127.0.0.1:41544": dial tcp 127.0.0.1:41544: connect: connection refused - utils.go:215: API server not ready yet (attempt 15) - utils.go:215: Error from API server: rpc call getHealth() on http://127.0.0.1:41544: Post "http://127.0.0.1:41544": dial tcp 127.0.0.1:41544: connect: connection refused - utils.go:215: API server not ready yet (attempt 16) - utils.go:215: Error from API server: rpc call getHealth() on http://127.0.0.1:41544: Post "http://127.0.0.1:41544": dial tcp 127.0.0.1:41544: connect: connection refused - utils.go:215: API server not ready yet (attempt 17) - utils.go:215: Error from API server: rpc call getHealth() on http://127.0.0.1:41544: Post "http://127.0.0.1:41544": dial tcp 127.0.0.1:41544: connect: connection refused - utils.go:215: API server not ready yet (attempt 18) - utils.go:215: Error from API server: rpc call getHealth() on http://127.0.0.1:41544: Post "http://127.0.0.1:41544": dial tcp 127.0.0.1:41544: connect: connection refused - utils.go:215: API server not ready yet (attempt 19) - utils.go:215: Error from API server: rpc call getHealth() on http://127.0.0.1:41544: Post "http://127.0.0.1:41544": dial tcp 127.0.0.1:41544: connect: connection refused - utils.go:215: API server not ready yet (attempt 20) - utils.go:215: Error from API server: rpc call getHealth() on http://127.0.0.1:41544: Post "http://127.0.0.1:41544": dial tcp 127.0.0.1:41544: connect: connection refused - utils.go:215: API server not ready yet (attempt 21) - utils.go:215: Error from API server: rpc call getHealth() on http://127.0.0.1:41544: Post "http://127.0.0.1:41544": dial tcp 127.0.0.1:41544: connect: connection refused - utils.go:215: API server not ready yet (attempt 22) - utils.go:215: Error from API server: rpc call getHealth() on http://127.0.0.1:41544: Post "http://127.0.0.1:41544": dial tcp 127.0.0.1:41544: connect: connection refused - utils.go:215: API server not ready yet (attempt 23) - utils.go:215: Error from API server: rpc call getHealth() on http://127.0.0.1:41544: Post "http://127.0.0.1:41544": dial tcp 127.0.0.1:41544: connect: connection refused - utils.go:215: API server not ready yet (attempt 24) - utils.go:215: Error from API server: rpc call getHealth() on http://127.0.0.1:41544: Post "http://127.0.0.1:41544": dial tcp 127.0.0.1:41544: connect: connection refused - utils.go:215: API server not ready yet (attempt 25) - utils.go:215: Error from API server: rpc call getHealth() on http://127.0.0.1:41544: Post "http://127.0.0.1:41544": dial tcp 127.0.0.1:41544: connect: connection refused - utils.go:215: API server not ready yet (attempt 26) - utils.go:215: Error from API server: rpc call getHealth() on http://127.0.0.1:41544: Post "http://127.0.0.1:41544": dial tcp 127.0.0.1:41544: connect: connection refused - utils.go:215: API server not ready yet (attempt 27) - utils.go:215: Error from API server: rpc call getHealth() on http://127.0.0.1:41544: Post "http://127.0.0.1:41544": dial tcp 127.0.0.1:41544: connect: connection refused - utils.go:215: API server not ready yet (attempt 28) - utils.go:215: Error from API server: rpc call getHealth() on http://127.0.0.1:41544: Post "http://127.0.0.1:41544": dial tcp 127.0.0.1:41544: connect: connection refused - utils.go:215: API server not ready yet (attempt 29) - utils.go:215: Error from API server: rpc call getHealth() on http://127.0.0.1:41544: Post "http://127.0.0.1:41544": dial tcp 127.0.0.1:41544: connect: connection refused - utils.go:215: API server not ready yet (attempt 30) - utils.go:215: Error from API server: rpc call getHealth() on http://127.0.0.1:41544: Post "http://127.0.0.1:41544": dial tcp 127.0.0.1:41544: connect: connection refused - utils.go:215: Cmd output: Error: program file does not exist: /Users/silaslenihan/Desktop/repos/chainlink-solana/contracts/target/deploy/contract-reader-interface.so - - Cmd error: - utils.go:215: - Error Trace: /Users/silaslenihan/Desktop/repos/chainlink-solana/pkg/solana/client/test_helpers.go:78 - /Users/silaslenihan/Desktop/repos/chainlink-solana/pkg/solana/utils/utils.go:215 - /Users/silaslenihan/Desktop/repos/chainlink-solana/pkg/solana/chainwriter/lookups_test.go:279 - Error: Should be true - Test: TestLookupTables - test_helpers.go:55: - Error Trace: /Users/silaslenihan/Desktop/repos/chainlink-solana/pkg/solana/client/test_helpers.go:55 - /Users/silaslenihan/.asdf/installs/golang/1.23.3/go/src/testing/testing.go:1176 - /Users/silaslenihan/.asdf/installs/golang/1.23.3/go/src/testing/testing.go:1354 - /Users/silaslenihan/.asdf/installs/golang/1.23.3/go/src/testing/testing.go:1684 - /Users/silaslenihan/.asdf/installs/golang/1.23.3/go/src/runtime/panic.go:629 - /Users/silaslenihan/.asdf/installs/golang/1.23.3/go/src/testing/testing.go:1006 - /Users/silaslenihan/Desktop/repos/chainlink-solana/pkg/solana/client/test_helpers.go:78 - /Users/silaslenihan/Desktop/repos/chainlink-solana/pkg/solana/utils/utils.go:215 - /Users/silaslenihan/Desktop/repos/chainlink-solana/pkg/solana/chainwriter/lookups_test.go:279 - Error: "exit status 1" does not contain "signal: killed" - Test: TestLookupTables - Messages: exit status 1 - test_helpers.go:56: solana-test-validator - stdout: Error: program file does not exist: /Users/silaslenihan/Desktop/repos/chainlink-solana/contracts/target/deploy/contract-reader-interface.so - - stderr: - diff --git a/integration-tests/go.sum b/integration-tests/go.sum index 056380557..3dd59b75e 100644 --- a/integration-tests/go.sum +++ b/integration-tests/go.sum @@ -318,6 +318,8 @@ github.com/cometbft/cometbft-db v0.8.0 h1:vUMDaH3ApkX8m0KZvOFFy9b5DZHBAjsnEuo9AK github.com/cometbft/cometbft-db v0.8.0/go.mod h1:6ASCP4pfhmrCBpfk01/9E1SI29nD3HfVHrY4PG8x5c0= github.com/confio/ics23/go v0.9.0 h1:cWs+wdbS2KRPZezoaaj+qBleXgUk5WOQFMP3CQFGTr4= github.com/confio/ics23/go v0.9.0/go.mod h1:4LPZ2NYqnYIVRklaozjNR1FScgDJ2s5Xrp+e/mYVRak= +github.com/confluentinc/confluent-kafka-go/v2 v2.3.0 h1:icCHutJouWlQREayFwCc7lxDAhws08td+W3/gdqgZts= +github.com/confluentinc/confluent-kafka-go/v2 v2.3.0/go.mod h1:/VTy8iEpe6mD9pkCH5BhijlUl8ulUXymKv1Qig5Rgb8= github.com/consensys/bavard v0.1.13 h1:oLhMLOFGTLdlda/kma4VOJazblc7IM5y5QPd2A/YjhQ= github.com/consensys/bavard v0.1.13/go.mod h1:9ItSMtA/dXMAiL7BG6bqW2m3NdSEObYWoH223nGHukI= github.com/consensys/gnark-crypto v0.12.1 h1:lHH39WuuFgVHONRl3J0LRBtuYdQTumFSDtJF7HpyG8M= @@ -1186,6 +1188,8 @@ github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 h1:N/ElC8H3+5X github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= github.com/regen-network/protobuf v1.3.3-alpha.regen.1 h1:OHEc+q5iIAXpqiqFKeLpu5NwTIkVXUs48vFMwzqpqY4= github.com/regen-network/protobuf v1.3.3-alpha.regen.1/go.mod h1:2DjTFR1HhMQhiWC5sZ4OhQ3+NtdbZ6oBDKQwq5Ou+FI= +github.com/riferrei/srclient v0.5.4 h1:dfwyR5u23QF7beuVl2WemUY2KXh5+Sc4DHKyPXBNYuc= +github.com/riferrei/srclient v0.5.4/go.mod h1:vbkLmWcgYa7JgfPvuy/+K8fTS0p1bApqadxrxi/S1MI= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis= github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= diff --git a/integration-tests/relayinterface/chain_components_test.go b/integration-tests/relayinterface/chain_components_test.go index cd47d224e..8b06b94c5 100644 --- a/integration-tests/relayinterface/chain_components_test.go +++ b/integration-tests/relayinterface/chain_components_test.go @@ -31,8 +31,8 @@ import ( "github.com/smartcontractkit/chainlink-solana/integration-tests/solclient" "github.com/smartcontractkit/chainlink-solana/integration-tests/utils" "github.com/smartcontractkit/chainlink-solana/pkg/solana/chainreader" - "github.com/smartcontractkit/chainlink-solana/pkg/solana/client" "github.com/smartcontractkit/chainlink-solana/pkg/solana/config" + solanautils "github.com/smartcontractkit/chainlink-solana/pkg/solana/utils" ) func TestChainComponents(t *testing.T) { @@ -248,13 +248,13 @@ func (h *helper) Init(t *testing.T) { privateKey, err := solana.PrivateKeyFromBase58(solclient.DefaultPrivateKeysSolValidator[1]) require.NoError(t, err) - h.rpcURL, h.wsURL = setupTestValidator(t, privateKey.PublicKey().String()) + h.rpcURL, h.wsURL = solanautils.SetupTestValidatorWithAnchorPrograms(t, privateKey.PublicKey().String(), []string{"contract-reader-interface"}) h.wsClient, err = ws.Connect(tests.Context(t), h.wsURL) h.rpcClient = rpc.New(h.rpcURL) require.NoError(t, err) - client.FundTestAccounts(t, []solana.PublicKey{privateKey.PublicKey()}, h.rpcURL) + solanautils.FundAccounts(t, []solana.PrivateKey{privateKey}, h.rpcClient) pubkey, err := solana.PublicKeyFromBase58(programPubKey) require.NoError(t, err) @@ -400,26 +400,3 @@ func (h *helper) waitForTX(t *testing.T, sig solana.Signature, commitment rpc.Co } const programPubKey = "6AfuXF6HapDUhQfE4nQG9C1SGtA1YjP3icaJyRfU4RyE" - -// upgradeAuthority is admin solana.PrivateKey as string -func setupTestValidator(t *testing.T, upgradeAuthority string) (string, string) { - t.Helper() - - soPath := filepath.Join(utils.ContractsDir, "contract_reader_interface.so") - - _, err := os.Stat(soPath) - if err != nil { - t.Log(err.Error()) - t.FailNow() - } - - flags := []string{ - "--warp-slot", "42", - "--upgradeable-program", - programPubKey, - soPath, - upgradeAuthority, - } - - return client.SetupLocalSolNodeWithFlags(t, flags...) -} diff --git a/pkg/solana/chainwriter/lookups_test.go b/integration-tests/relayinterface/lookups_test.go similarity index 92% rename from pkg/solana/chainwriter/lookups_test.go rename to integration-tests/relayinterface/lookups_test.go index 53972feac..1d7e9f799 100644 --- a/pkg/solana/chainwriter/lookups_test.go +++ b/integration-tests/relayinterface/lookups_test.go @@ -1,4 +1,4 @@ -package chainwriter_test +package relayinterface import ( "context" @@ -22,22 +22,6 @@ import ( "github.com/smartcontractkit/chainlink-solana/pkg/solana/utils" ) -type TestArgs struct { - Inner []InnerArgs -} - -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 := chainwriter.GetRandomPubKey(t) @@ -63,8 +47,8 @@ func TestAccountLookups(t *testing.T) { ctx := tests.Context(t) t.Run("AccountLookup resolves valid address with just one address", func(t *testing.T) { expectedAddr := chainwriter.GetRandomPubKey(t) - testArgs := TestArgs{ - Inner: []InnerArgs{ + testArgs := chainwriter.TestArgs{ + Inner: []chainwriter.InnerArgs{ {Address: expectedAddr.Bytes()}, }, } @@ -91,8 +75,8 @@ func TestAccountLookups(t *testing.T) { expectedAddr1 := chainwriter.GetRandomPubKey(t) expectedAddr2 := chainwriter.GetRandomPubKey(t) - testArgs := TestArgs{ - Inner: []InnerArgs{ + testArgs := chainwriter.TestArgs{ + Inner: []chainwriter.InnerArgs{ {Address: expectedAddr1.Bytes()}, {Address: expectedAddr2.Bytes()}, }, @@ -126,8 +110,8 @@ func TestAccountLookups(t *testing.T) { t.Run("AccountLookup fails when address isn't in args", func(t *testing.T) { expectedAddr := chainwriter.GetRandomPubKey(t) - testArgs := TestArgs{ - Inner: []InnerArgs{ + testArgs := chainwriter.TestArgs{ + Inner: []chainwriter.InnerArgs{ {Address: expectedAddr.Bytes()}, }, } @@ -276,10 +260,10 @@ func TestLookupTables(t *testing.T) { sender, err := solana.NewRandomPrivateKey() require.NoError(t, err) - url := utils.SetupTestValidatorWithAnchorPrograms(t, utils.PathToAnchorConfig, sender.PublicKey().String()) + url, _ := utils.SetupTestValidatorWithAnchorPrograms(t, sender.PublicKey().String(), []string{"contract-reader-interface"}) rpcClient := rpc.New(url) - utils.FundAccounts(ctx, []solana.PrivateKey{sender}, rpcClient, t) + utils.FundAccounts(t, []solana.PrivateKey{sender}, rpcClient) cfg := config.NewDefault() solanaClient, err := client.NewClient(url, cfg, 5*time.Second, nil) @@ -291,7 +275,7 @@ func TestLookupTables(t *testing.T) { txm := txm.NewTxm("localnet", loader, nil, cfg, mkey, lggr) - cw, err := chainwriter.NewSolanaChainWriterService(solanaClient, txm, nil, chainwriter.ChainWriterConfig{}) + cw, err := chainwriter.NewSolanaChainWriterService(nil, solanaClient, txm, nil, chainwriter.ChainWriterConfig{}) t.Run("StaticLookup table resolves properly", func(t *testing.T) { pubKeys := chainwriter.CreateTestPubKeys(t, 8) @@ -384,8 +368,8 @@ func TestLookupTables(t *testing.T) { StaticLookupTables: nil, } - testArgs := TestArgs{ - Inner: []InnerArgs{ + testArgs := chainwriter.TestArgs{ + Inner: []chainwriter.InnerArgs{ {Address: table.Bytes()}, }, } @@ -401,8 +385,8 @@ func TestLookupTables(t *testing.T) { }) t.Run("Derived lookup table resolves properly with PDALookup address", func(t *testing.T) { - // Deployed write_test contract - programID := solana.MustPublicKeyFromBase58("39vbQVpEMtZtg3e6ZSE7nBSzmNZptmW45WnLkbqEe4TU") + // Deployed contract_reader_interface contract + programID := solana.MustPublicKeyFromBase58("6AfuXF6HapDUhQfE4nQG9C1SGtA1YjP3icaJyRfU4RyE") lookupKeys := chainwriter.CreateTestPubKeys(t, 5) lookupTable := chainwriter.CreateTestLookupTable(ctx, t, rpcClient, sender, lookupKeys) @@ -426,7 +410,7 @@ func TestLookupTables(t *testing.T) { IsSigner: false, IsWritable: false, InternalField: chainwriter.InternalField{ - Type: reflect.TypeOf(DataAccount{}), + Type: reflect.TypeOf(chainwriter.DataAccount{}), Location: "LookupTable", }, }, diff --git a/pkg/solana/chainwriter/chain_writer.go b/pkg/solana/chainwriter/chain_writer.go index 4fcc5caa0..8c82038f1 100644 --- a/pkg/solana/chainwriter/chain_writer.go +++ b/pkg/solana/chainwriter/chain_writer.go @@ -10,6 +10,7 @@ import ( commoncodec "github.com/smartcontractkit/chainlink-common/pkg/codec" "github.com/smartcontractkit/chainlink-common/pkg/codec/encodings/binary" + "github.com/smartcontractkit/chainlink-common/pkg/logger" "github.com/smartcontractkit/chainlink-common/pkg/services" "github.com/smartcontractkit/chainlink-common/pkg/types" @@ -19,14 +20,25 @@ import ( "github.com/smartcontractkit/chainlink-solana/pkg/solana/txm" ) +const ServiceName = "SolanaChainWriter" + type SolanaChainWriterService struct { + lggr logger.Logger reader client.Reader txm txm.TxManager ge fees.Estimator config ChainWriterConfig + codecs map[string]types.Codec + + services.StateMachine } +var ( + _ services.Service = &SolanaChainWriterService{} + _ types.ContractWriter = &SolanaChainWriterService{} +) + // nolint // ignoring naming suggestion type ChainWriterConfig struct { Programs map[string]ProgramConfig @@ -47,13 +59,14 @@ type MethodConfig struct { DebugIDLocation string } -func NewSolanaChainWriterService(reader client.Reader, txm txm.TxManager, ge fees.Estimator, config ChainWriterConfig) (*SolanaChainWriterService, error) { +func NewSolanaChainWriterService(logger logger.Logger, reader client.Reader, txm txm.TxManager, 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{ + lggr: logger, reader: reader, txm: txm, ge: ge, @@ -291,22 +304,26 @@ func (s *SolanaChainWriterService) GetFeeComponents(ctx context.Context) (*types }, nil } -func (s *SolanaChainWriterService) Start(context.Context) error { - return nil +func (s *SolanaChainWriterService) Start(_ context.Context) error { + return s.StartOnce(ServiceName, func() error { + return nil + }) } func (s *SolanaChainWriterService) Close() error { - return nil + return s.StopOnce(ServiceName, func() error { + return nil + }) } func (s *SolanaChainWriterService) HealthReport() map[string]error { - return nil + return map[string]error{s.Name(): s.Healthy()} } func (s *SolanaChainWriterService) Name() string { - return "" + return s.lggr.Name() } func (s *SolanaChainWriterService) Ready() error { - return nil + return s.StateMachine.Ready() } diff --git a/pkg/solana/chainwriter/chain_writer_test.go b/pkg/solana/chainwriter/chain_writer_test.go index d931fb6d8..1490b519e 100644 --- a/pkg/solana/chainwriter/chain_writer_test.go +++ b/pkg/solana/chainwriter/chain_writer_test.go @@ -18,13 +18,14 @@ import ( "github.com/smartcontractkit/chainlink-common/pkg/types" "github.com/smartcontractkit/chainlink-common/pkg/utils/tests" + "github.com/smartcontractkit/chainlink-solana/pkg/monitoring/testutils" "github.com/smartcontractkit/chainlink-solana/pkg/solana/chainwriter" clientmocks "github.com/smartcontractkit/chainlink-solana/pkg/solana/client/mocks" feemocks "github.com/smartcontractkit/chainlink-solana/pkg/solana/fees/mocks" txmMocks "github.com/smartcontractkit/chainlink-solana/pkg/solana/txm/mocks" ) -var writeTestIdlJSON = `{"version": "0.1.0","name": "write_test","instructions": [{"name": "initialize","accounts": [{"name": "dataAccount","isMut": true,"isSigner": false,"docs": ["PDA account, derived from seeds and created by the System Program in this instruction"]},{"name": "admin","isMut": true,"isSigner": true,"docs": ["Admin account that pays for PDA creation and signs the transaction"]},{"name": "systemProgram","isMut": false,"isSigner": false,"docs": ["System Program is required for PDA creation"]}],"args": [{"name": "lookupTable","type": "publicKey"}]}],"accounts": [{"name": "DataAccount","type": {"kind": "struct","fields": [{"name": "version","type": "u8"},{"name": "administrator","type": "publicKey"},{"name": "pendingAdministrator","type": "publicKey"},{"name": "lookupTable","type": "publicKey"}]}}]}` +var testContractIDLJson = `{"version":"0.1.0","name":"contract_reader_interface","instructions":[{"name":"initialize","accounts":[{"name":"data","isMut":true,"isSigner":false},{"name":"signer","isMut":true,"isSigner":true},{"name":"systemProgram","isMut":false,"isSigner":false}],"args":[{"name":"testIdx","type":"u64"},{"name":"value","type":"u64"}]},{"name":"initializeLookupTable","accounts":[{"name":"writeDataAccount","isMut":true,"isSigner":false,"docs":["PDA for LookupTableDataAccount, derived from seeds and created by the System Program"]},{"name":"admin","isMut":true,"isSigner":true,"docs":["Admin account that pays for PDA creation and signs the transaction"]},{"name":"systemProgram","isMut":false,"isSigner":false,"docs":["System Program required for PDA creation"]}],"args":[{"name":"lookupTable","type":"publicKey"}]}],"accounts":[{"name":"LookupTableDataAccount","type":{"kind":"struct","fields":[{"name":"version","type":"u8"},{"name":"administrator","type":"publicKey"},{"name":"pendingAdministrator","type":"publicKey"},{"name":"lookupTable","type":"publicKey"}]}},{"name":"DataAccount","type":{"kind":"struct","fields":[{"name":"idx","type":"u64"},{"name":"bump","type":"u8"},{"name":"u64Value","type":"u64"},{"name":"u64Slice","type":{"vec":"u64"}}]}}]}` func TestChainWriter_GetAddresses(t *testing.T) { ctx := tests.Context(t) @@ -37,7 +38,7 @@ func TestChainWriter_GetAddresses(t *testing.T) { txm := txmMocks.NewTxManager(t) // initialize chain writer - cw, err := chainwriter.NewSolanaChainWriterService(rw, txm, ge, chainwriter.ChainWriterConfig{}) + cw, err := chainwriter.NewSolanaChainWriterService(testutils.NewNullLogger(), rw, txm, ge, chainwriter.ChainWriterConfig{}) require.NoError(t, err) // expected account meta for constant account @@ -91,7 +92,7 @@ func TestChainWriter_GetAddresses(t *testing.T) { IsSigner: derivedTablePdaLookupMeta.IsSigner, IsWritable: derivedTablePdaLookupMeta.IsWritable, InternalField: chainwriter.InternalField{ - Type: reflect.TypeOf(DataAccount{}), + Type: reflect.TypeOf(chainwriter.DataAccount{}), Location: "LookupTable", }, }, @@ -237,7 +238,7 @@ func TestChainWriter_FilterLookupTableAddresses(t *testing.T) { txm := txmMocks.NewTxManager(t) // initialize chain writer - cw, err := chainwriter.NewSolanaChainWriterService(rw, txm, ge, chainwriter.ChainWriterConfig{}) + cw, err := chainwriter.NewSolanaChainWriterService(testutils.NewNullLogger(), rw, txm, ge, chainwriter.ChainWriterConfig{}) require.NoError(t, err) programID := chainwriter.GetRandomPubKey(t) @@ -278,7 +279,7 @@ func TestChainWriter_FilterLookupTableAddresses(t *testing.T) { IsSigner: true, IsWritable: true, InternalField: chainwriter.InternalField{ - Type: reflect.TypeOf(DataAccount{}), + Type: reflect.TypeOf(chainwriter.DataAccount{}), Location: "LookupTable", }, }, @@ -295,7 +296,7 @@ func TestChainWriter_FilterLookupTableAddresses(t *testing.T) { IsSigner: true, IsWritable: true, InternalField: chainwriter.InternalField{ - Type: reflect.TypeOf(DataAccount{}), + Type: reflect.TypeOf(chainwriter.DataAccount{}), Location: "LookupTable", }, }, @@ -415,11 +416,11 @@ func TestChainWriter_SubmitTransaction(t *testing.T) { cwConfig := chainwriter.ChainWriterConfig{ Programs: map[string]chainwriter.ProgramConfig{ - "39vbQVpEMtZtg3e6ZSE7nBSzmNZptmW45WnLkbqEe4TU": { + "6AfuXF6HapDUhQfE4nQG9C1SGtA1YjP3icaJyRfU4RyE": { Methods: map[string]chainwriter.MethodConfig{ - "initialize": { + "initializeLookupTable": { FromAddress: admin.String(), - ChainSpecificName: "initialize", + ChainSpecificName: "initializeLookupTable", LookupTables: chainwriter.LookupTables{ DerivedLookupTables: []chainwriter.DerivedLookupTable{ { @@ -434,7 +435,7 @@ func TestChainWriter_SubmitTransaction(t *testing.T) { IsSigner: false, IsWritable: false, InternalField: chainwriter.InternalField{ - Type: reflect.TypeOf(DataAccount{}), + Type: reflect.TypeOf(chainwriter.DataAccount{}), Location: "LookupTable", }, }, @@ -474,19 +475,19 @@ func TestChainWriter_SubmitTransaction(t *testing.T) { }, }, }, - IDL: writeTestIdlJSON, + IDL: testContractIDLJson, }, }, } // initialize chain writer - cw, err := chainwriter.NewSolanaChainWriterService(rw, txm, ge, cwConfig) + cw, err := chainwriter.NewSolanaChainWriterService(testutils.NewNullLogger(), rw, txm, ge, cwConfig) require.NoError(t, err) t.Run("fails with invalid ABI", func(t *testing.T) { invalidCWConfig := chainwriter.ChainWriterConfig{ Programs: map[string]chainwriter.ProgramConfig{ - "write_test": { + "invalid_program": { Methods: map[string]chainwriter.MethodConfig{ "invalid": { ChainSpecificName: "invalid", @@ -497,28 +498,28 @@ func TestChainWriter_SubmitTransaction(t *testing.T) { }, } - _, err := chainwriter.NewSolanaChainWriterService(rw, txm, ge, invalidCWConfig) + _, err := chainwriter.NewSolanaChainWriterService(testutils.NewNullLogger(), rw, txm, ge, invalidCWConfig) require.Error(t, err) }) t.Run("fails to encode payload if args with missing values provided", func(t *testing.T) { txID := uuid.NewString() args := map[string]interface{}{} - submitErr := cw.SubmitTransaction(ctx, "39vbQVpEMtZtg3e6ZSE7nBSzmNZptmW45WnLkbqEe4TU", "initialize", args, txID, programID.String(), nil, nil) + submitErr := cw.SubmitTransaction(ctx, "6AfuXF6HapDUhQfE4nQG9C1SGtA1YjP3icaJyRfU4RyE", "initializeLookupTable", args, txID, programID.String(), nil, nil) require.Error(t, submitErr) }) t.Run("fails if invalid contract name provided", func(t *testing.T) { txID := uuid.NewString() args := map[string]interface{}{} - submitErr := cw.SubmitTransaction(ctx, "write_test", "initialize", args, txID, programID.String(), nil, nil) + submitErr := cw.SubmitTransaction(ctx, "contract_reader_interface", "initializeLookupTable", args, txID, programID.String(), nil, nil) require.Error(t, submitErr) }) t.Run("fails if invalid method provided", func(t *testing.T) { txID := uuid.NewString() args := map[string]interface{}{} - submitErr := cw.SubmitTransaction(ctx, "39vbQVpEMtZtg3e6ZSE7nBSzmNZptmW45WnLkbqEe4TU", "badMethod", args, txID, programID.String(), nil, nil) + submitErr := cw.SubmitTransaction(ctx, "6AfuXF6HapDUhQfE4nQG9C1SGtA1YjP3icaJyRfU4RyE", "badMethod", args, txID, programID.String(), nil, nil) require.Error(t, submitErr) }) @@ -526,7 +527,7 @@ func TestChainWriter_SubmitTransaction(t *testing.T) { recentBlockHash := solana.Hash{} rw.On("LatestBlockhash", mock.Anything).Return(&rpc.GetLatestBlockhashResult{Value: &rpc.LatestBlockhashResult{Blockhash: recentBlockHash, LastValidBlockHeight: uint64(100)}}, nil).Once() txID := uuid.NewString() - configProgramID := solana.MustPublicKeyFromBase58("39vbQVpEMtZtg3e6ZSE7nBSzmNZptmW45WnLkbqEe4TU") + configProgramID := solana.MustPublicKeyFromBase58("6AfuXF6HapDUhQfE4nQG9C1SGtA1YjP3icaJyRfU4RyE") txm.On("Enqueue", mock.Anything, account1.String(), mock.MatchedBy(func(tx *solana.Transaction) bool { // match transaction fields to ensure it was built as expected @@ -549,7 +550,7 @@ func TestChainWriter_SubmitTransaction(t *testing.T) { "seed1": seed1, "seed2": seed2, } - submitErr := cw.SubmitTransaction(ctx, "39vbQVpEMtZtg3e6ZSE7nBSzmNZptmW45WnLkbqEe4TU", "initialize", args, txID, programID.String(), nil, nil) + submitErr := cw.SubmitTransaction(ctx, "6AfuXF6HapDUhQfE4nQG9C1SGtA1YjP3icaJyRfU4RyE", "initializeLookupTable", args, txID, programID.String(), nil, nil) require.NoError(t, submitErr) }) } @@ -565,7 +566,7 @@ func TestChainWriter_GetTransactionStatus(t *testing.T) { txm := txmMocks.NewTxManager(t) // initialize chain writer - cw, err := chainwriter.NewSolanaChainWriterService(rw, txm, ge, chainwriter.ChainWriterConfig{}) + cw, err := chainwriter.NewSolanaChainWriterService(testutils.NewNullLogger(), rw, txm, ge, chainwriter.ChainWriterConfig{}) require.NoError(t, err) t.Run("returns unknown with error if ID not found", func(t *testing.T) { @@ -628,7 +629,7 @@ func TestChainWriter_GetFeeComponents(t *testing.T) { // mock txm txm := txmMocks.NewTxManager(t) - cw, err := chainwriter.NewSolanaChainWriterService(rw, txm, ge, chainwriter.ChainWriterConfig{}) + cw, err := chainwriter.NewSolanaChainWriterService(testutils.NewNullLogger(), rw, txm, ge, chainwriter.ChainWriterConfig{}) require.NoError(t, err) t.Run("returns valid compute unit price", func(t *testing.T) { @@ -639,7 +640,7 @@ func TestChainWriter_GetFeeComponents(t *testing.T) { }) t.Run("fails if gas estimator not set", func(t *testing.T) { - cwNoEstimator, err := chainwriter.NewSolanaChainWriterService(rw, txm, nil, chainwriter.ChainWriterConfig{}) + cwNoEstimator, err := chainwriter.NewSolanaChainWriterService(testutils.NewNullLogger(), rw, txm, nil, chainwriter.ChainWriterConfig{}) require.NoError(t, err) _, err = cwNoEstimator.GetFeeComponents(ctx) require.Error(t, err) @@ -661,7 +662,7 @@ func mustFindPdaProgramAddress(t *testing.T, seeds [][]byte, programID solana.Pu func mockDataAccountLookupTable(t *testing.T, rw *clientmocks.ReaderWriter, pda solana.PublicKey) solana.PublicKey { lookupTablePubkey := chainwriter.GetRandomPubKey(t) - dataAccount := DataAccount{ + dataAccount := chainwriter.DataAccount{ Discriminator: [8]byte{}, Version: 1, Administrator: chainwriter.GetRandomPubKey(t), diff --git a/pkg/solana/chainwriter/helpers.go b/pkg/solana/chainwriter/helpers.go index bc256c60a..22a2d1c67 100644 --- a/pkg/solana/chainwriter/helpers.go +++ b/pkg/solana/chainwriter/helpers.go @@ -16,6 +16,22 @@ import ( "github.com/smartcontractkit/chainlink-solana/pkg/solana/utils" ) +type TestArgs struct { + Inner []InnerArgs +} + +type InnerArgs struct { + Address []byte +} + +type DataAccount struct { + Discriminator [8]byte + Version uint8 + Administrator solana.PublicKey + PendingAdministrator solana.PublicKey + LookupTable solana.PublicKey +} + // GetValuesAtLocation parses through nested types and arrays to find all locations of values func GetValuesAtLocation(args any, location string) ([][]byte, error) { var vals [][]byte @@ -139,7 +155,7 @@ func InitializeDataAccount( pda, _, err := solana.FindProgramAddress([][]byte{[]byte("data")}, programID) require.NoError(t, err) - discriminator := GetDiscriminator("initialize") + discriminator := GetDiscriminator("initialize_lookup_table") instructionData := append(discriminator[:], lookupTable.Bytes()...) diff --git a/pkg/solana/chainwriter/lookups.go b/pkg/solana/chainwriter/lookups.go index 1947b060d..37de7a567 100644 --- a/pkg/solana/chainwriter/lookups.go +++ b/pkg/solana/chainwriter/lookups.go @@ -162,7 +162,6 @@ func (pda PDALookups) Resolve(ctx context.Context, args any, derivedTableMap map Encoding: "base64", Commitment: rpc.CommitmentFinalized, }) - fmt.Printf("Accounts Info: %+v", accountInfo) 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) diff --git a/pkg/solana/client/test_helpers.go b/pkg/solana/client/test_helpers.go index 8d5ab4f88..1659cfacd 100644 --- a/pkg/solana/client/test_helpers.go +++ b/pkg/solana/client/test_helpers.go @@ -66,7 +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) + t.Logf("Cmd output: %s\nCmd error: %s\n", stdOut.String(), stdErr.String()) continue } ready = true diff --git a/pkg/solana/utils/utils.go b/pkg/solana/utils/utils.go index 974ca6813..87fce3ac7 100644 --- a/pkg/solana/utils/utils.go +++ b/pkg/solana/utils/utils.go @@ -16,6 +16,8 @@ import ( "github.com/pelletier/go-toml/v2" "github.com/stretchr/testify/require" + "github.com/smartcontractkit/chainlink-common/pkg/utils/tests" + "github.com/smartcontractkit/chainlink-solana/pkg/solana/client" "github.com/smartcontractkit/chainlink-solana/pkg/solana/internal" ) @@ -159,7 +161,8 @@ func NewExtendLookupTableInstruction( ) } -func FundAccounts(ctx context.Context, accounts []solana.PrivateKey, solanaGoClient *rpc.Client, t *testing.T) { +func FundAccounts(t *testing.T, accounts []solana.PrivateKey, solanaGoClient *rpc.Client) { + ctx := tests.Context(t) sigs := []solana.Signature{} for _, v := range accounts { sig, err := solanaGoClient.RequestAirdrop(ctx, v.PublicKey(), 1000*solana.LAMPORTS_PER_SOL, rpc.CommitmentFinalized) @@ -193,11 +196,7 @@ 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 { +func SetupTestValidatorWithAnchorPrograms(t *testing.T, upgradeAuthority string, programs []string) (string, string) { anchorData := struct { Programs struct { Localnet map[string]string @@ -205,15 +204,17 @@ func SetupTestValidatorWithAnchorPrograms(t *testing.T, pathToAnchorConfig strin }{} // upload programs to validator - anchorBytes, err := os.ReadFile(pathToAnchorConfig) + 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 := []string{"--warp-slot", "42"} + for i := range programs { + k := programs[i] + v := anchorData.Programs.Localnet[k] k = strings.Replace(k, "-", "_", -1) flags = append(flags, "--upgradeable-program", v, filepath.Join(ContractsDir, k+".so"), upgradeAuthority) } - url, _ := client.SetupLocalSolNodeWithFlags(t, flags...) - return url + rpcURL, wsURL := client.SetupLocalSolNodeWithFlags(t, flags...) + return rpcURL, wsURL } From dd3321a91d5438cb8a2d04c68cef56850192fb67 Mon Sep 17 00:00:00 2001 From: Silas Lenihan Date: Fri, 13 Dec 2024 14:59:09 -0500 Subject: [PATCH 26/29] addressed feedback comments --- .../localnet/write_test-keypair.json | 6 ----- integration-tests/go.sum | 2 ++ .../relayinterface/lookups_test.go | 9 ++++---- pkg/solana/chainwriter/ccip_example_config.go | 13 +++-------- pkg/solana/chainwriter/chain_writer.go | 7 +----- pkg/solana/chainwriter/chain_writer_test.go | 17 +++++++------- pkg/solana/chainwriter/helpers.go | 22 +------------------ pkg/solana/chainwriter/lookups.go | 14 ++++++++---- 8 files changed, 29 insertions(+), 61 deletions(-) delete mode 100644 contracts/artifacts/localnet/write_test-keypair.json diff --git a/contracts/artifacts/localnet/write_test-keypair.json b/contracts/artifacts/localnet/write_test-keypair.json deleted file mode 100644 index dfb18e9c4..000000000 --- a/contracts/artifacts/localnet/write_test-keypair.json +++ /dev/null @@ -1,6 +0,0 @@ -[ - 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 -] diff --git a/integration-tests/go.sum b/integration-tests/go.sum index 3dd59b75e..7f20fb5c2 100644 --- a/integration-tests/go.sum +++ b/integration-tests/go.sum @@ -955,6 +955,8 @@ github.com/libp2p/go-buffer-pool v0.1.0 h1:oK4mSFcQz7cTQIfqbe4MIj9gLW+mnanjyFtc6 github.com/libp2p/go-buffer-pool v0.1.0/go.mod h1:N+vh8gMqimBzdKkSMVuydVDq+UV5QTWy5HSiZacSbPg= github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de h1:9TO3cAIGXtEhnIaL+V+BEER86oLrvS+kWobKpbJuye0= github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de/go.mod h1:zAbeS9B/r2mtpb6U+EI2rYA5OAXxsYw6wTamcNW+zcE= +github.com/linkedin/goavro/v2 v2.12.0 h1:rIQQSj8jdAUlKQh6DttK8wCRv4t4QO09g1C4aBWXslg= +github.com/linkedin/goavro/v2 v2.12.0/go.mod h1:KXx+erlq+RPlGSPmLF7xGo6SAbh8sCQ53x064+ioxhk= github.com/linxGnu/grocksdb v1.7.16 h1:Q2co1xrpdkr5Hx3Fp+f+f7fRGhQFQhvi/+226dtLmA8= github.com/linxGnu/grocksdb v1.7.16/go.mod h1:JkS7pl5qWpGpuVb3bPqTz8nC12X3YtPZT+Xq7+QfQo4= github.com/lithammer/dedent v1.1.0 h1:VNzHMVCBNG1j0fh3OrsFRkVUwStdDArbgBWoPAffktY= diff --git a/integration-tests/relayinterface/lookups_test.go b/integration-tests/relayinterface/lookups_test.go index 1d7e9f799..1b91dc8df 100644 --- a/integration-tests/relayinterface/lookups_test.go +++ b/integration-tests/relayinterface/lookups_test.go @@ -1,7 +1,6 @@ package relayinterface import ( - "context" "reflect" "testing" "time" @@ -153,7 +152,7 @@ func TestPDALookups(t *testing.T) { IsWritable: true, } - ctx := context.Background() + ctx := tests.Context(t) result, err := pdaLookup.Resolve(ctx, nil, nil, nil) require.NoError(t, err) require.Equal(t, expectedMeta, result) @@ -184,7 +183,7 @@ func TestPDALookups(t *testing.T) { IsWritable: true, } - ctx := context.Background() + ctx := tests.Context(t) args := map[string]interface{}{ "test_seed": seed1, "another_seed": seed2, @@ -206,7 +205,7 @@ func TestPDALookups(t *testing.T) { IsWritable: true, } - ctx := context.Background() + ctx := tests.Context(t) args := map[string]interface{}{ "test_seed": []byte("data"), } @@ -242,7 +241,7 @@ func TestPDALookups(t *testing.T) { IsWritable: true, } - ctx := context.Background() + ctx := tests.Context(t) args := map[string]interface{}{ "test_seed": seed1, "another_seed": seed2, diff --git a/pkg/solana/chainwriter/ccip_example_config.go b/pkg/solana/chainwriter/ccip_example_config.go index 89038fd6a..acdaf3d35 100644 --- a/pkg/solana/chainwriter/ccip_example_config.go +++ b/pkg/solana/chainwriter/ccip_example_config.go @@ -2,8 +2,6 @@ package chainwriter import ( "fmt" - - commoncodec "github.com/smartcontractkit/chainlink-common/pkg/codec" ) func TestConfig() { @@ -22,14 +20,9 @@ func TestConfig() { 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 := 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"}, - }, - }, - ChainSpecificName: "execute", + FromAddress: userAddress, + InputModifications: nil, + 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) diff --git a/pkg/solana/chainwriter/chain_writer.go b/pkg/solana/chainwriter/chain_writer.go index 8c82038f1..e07842201 100644 --- a/pkg/solana/chainwriter/chain_writer.go +++ b/pkg/solana/chainwriter/chain_writer.go @@ -251,7 +251,7 @@ func (s *SolanaChainWriterService) SubmitTransaction(ctx context.Context, contra } // Prepare transaction - programID, err := solana.PublicKeyFromBase58(contractName) + programID, err := solana.PublicKeyFromBase58(toAddress) if err != nil { return errorWithDebugID(fmt.Errorf("error parsing program ID: %w", err), debugID) } @@ -281,11 +281,6 @@ func (s *SolanaChainWriterService) SubmitTransaction(ctx context.Context, contra return nil } -var ( - _ services.Service = &SolanaChainWriterService{} - _ types.ContractWriter = &SolanaChainWriterService{} -) - // 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 s.txm.GetTransactionStatus(ctx, transactionID) diff --git a/pkg/solana/chainwriter/chain_writer_test.go b/pkg/solana/chainwriter/chain_writer_test.go index 1490b519e..c7fab54e0 100644 --- a/pkg/solana/chainwriter/chain_writer_test.go +++ b/pkg/solana/chainwriter/chain_writer_test.go @@ -25,7 +25,7 @@ import ( txmMocks "github.com/smartcontractkit/chainlink-solana/pkg/solana/txm/mocks" ) -var testContractIDLJson = `{"version":"0.1.0","name":"contract_reader_interface","instructions":[{"name":"initialize","accounts":[{"name":"data","isMut":true,"isSigner":false},{"name":"signer","isMut":true,"isSigner":true},{"name":"systemProgram","isMut":false,"isSigner":false}],"args":[{"name":"testIdx","type":"u64"},{"name":"value","type":"u64"}]},{"name":"initializeLookupTable","accounts":[{"name":"writeDataAccount","isMut":true,"isSigner":false,"docs":["PDA for LookupTableDataAccount, derived from seeds and created by the System Program"]},{"name":"admin","isMut":true,"isSigner":true,"docs":["Admin account that pays for PDA creation and signs the transaction"]},{"name":"systemProgram","isMut":false,"isSigner":false,"docs":["System Program required for PDA creation"]}],"args":[{"name":"lookupTable","type":"publicKey"}]}],"accounts":[{"name":"LookupTableDataAccount","type":{"kind":"struct","fields":[{"name":"version","type":"u8"},{"name":"administrator","type":"publicKey"},{"name":"pendingAdministrator","type":"publicKey"},{"name":"lookupTable","type":"publicKey"}]}},{"name":"DataAccount","type":{"kind":"struct","fields":[{"name":"idx","type":"u64"},{"name":"bump","type":"u8"},{"name":"u64Value","type":"u64"},{"name":"u64Slice","type":{"vec":"u64"}}]}}]}` +var testContractIDLJson = `{"version":"0.1.0","name":"contractReaderInterface","instructions":[{"name":"initialize","accounts":[{"name":"data","isMut":true,"isSigner":false},{"name":"signer","isMut":true,"isSigner":true},{"name":"systemProgram","isMut":false,"isSigner":false}],"args":[{"name":"testIdx","type":"u64"},{"name":"value","type":"u64"}]},{"name":"initializeLookupTable","accounts":[{"name":"writeDataAccount","isMut":true,"isSigner":false,"docs":["PDA for LookupTableDataAccount, derived from seeds and created by the System Program"]},{"name":"admin","isMut":true,"isSigner":true,"docs":["Admin account that pays for PDA creation and signs the transaction"]},{"name":"systemProgram","isMut":false,"isSigner":false,"docs":["System Program required for PDA creation"]}],"args":[{"name":"lookupTable","type":"publicKey"}]}],"accounts":[{"name":"LookupTableDataAccount","type":{"kind":"struct","fields":[{"name":"version","type":"u8"},{"name":"administrator","type":"publicKey"},{"name":"pendingAdministrator","type":"publicKey"},{"name":"lookupTable","type":"publicKey"}]}},{"name":"DataAccount","type":{"kind":"struct","fields":[{"name":"idx","type":"u64"},{"name":"bump","type":"u8"},{"name":"u64Value","type":"u64"},{"name":"u64Slice","type":{"vec":"u64"}}]}}]}` func TestChainWriter_GetAddresses(t *testing.T) { ctx := tests.Context(t) @@ -401,7 +401,7 @@ func TestChainWriter_SubmitTransaction(t *testing.T) { // create lookup table addresses seed2 := []byte("seed2") - programID := chainwriter.GetRandomPubKey(t) + programID := solana.MustPublicKeyFromBase58("6AfuXF6HapDUhQfE4nQG9C1SGtA1YjP3icaJyRfU4RyE") derivedTablePda := mustFindPdaProgramAddress(t, [][]byte{seed2}, programID) // mock data account response from program derivedLookupTablePubkey := mockDataAccountLookupTable(t, rw, derivedTablePda) @@ -416,7 +416,7 @@ func TestChainWriter_SubmitTransaction(t *testing.T) { cwConfig := chainwriter.ChainWriterConfig{ Programs: map[string]chainwriter.ProgramConfig{ - "6AfuXF6HapDUhQfE4nQG9C1SGtA1YjP3icaJyRfU4RyE": { + "contractReaderInterface": { Methods: map[string]chainwriter.MethodConfig{ "initializeLookupTable": { FromAddress: admin.String(), @@ -505,21 +505,21 @@ func TestChainWriter_SubmitTransaction(t *testing.T) { t.Run("fails to encode payload if args with missing values provided", func(t *testing.T) { txID := uuid.NewString() args := map[string]interface{}{} - submitErr := cw.SubmitTransaction(ctx, "6AfuXF6HapDUhQfE4nQG9C1SGtA1YjP3icaJyRfU4RyE", "initializeLookupTable", args, txID, programID.String(), nil, nil) + submitErr := cw.SubmitTransaction(ctx, "contractReaderInterface", "initializeLookupTable", args, txID, programID.String(), nil, nil) require.Error(t, submitErr) }) t.Run("fails if invalid contract name provided", func(t *testing.T) { txID := uuid.NewString() args := map[string]interface{}{} - submitErr := cw.SubmitTransaction(ctx, "contract_reader_interface", "initializeLookupTable", args, txID, programID.String(), nil, nil) + submitErr := cw.SubmitTransaction(ctx, "badContract", "initializeLookupTable", args, txID, programID.String(), nil, nil) require.Error(t, submitErr) }) t.Run("fails if invalid method provided", func(t *testing.T) { txID := uuid.NewString() args := map[string]interface{}{} - submitErr := cw.SubmitTransaction(ctx, "6AfuXF6HapDUhQfE4nQG9C1SGtA1YjP3icaJyRfU4RyE", "badMethod", args, txID, programID.String(), nil, nil) + submitErr := cw.SubmitTransaction(ctx, "contractReaderInterface", "badMethod", args, txID, programID.String(), nil, nil) require.Error(t, submitErr) }) @@ -527,7 +527,6 @@ func TestChainWriter_SubmitTransaction(t *testing.T) { recentBlockHash := solana.Hash{} rw.On("LatestBlockhash", mock.Anything).Return(&rpc.GetLatestBlockhashResult{Value: &rpc.LatestBlockhashResult{Blockhash: recentBlockHash, LastValidBlockHeight: uint64(100)}}, nil).Once() txID := uuid.NewString() - configProgramID := solana.MustPublicKeyFromBase58("6AfuXF6HapDUhQfE4nQG9C1SGtA1YjP3icaJyRfU4RyE") txm.On("Enqueue", mock.Anything, account1.String(), mock.MatchedBy(func(tx *solana.Transaction) bool { // match transaction fields to ensure it was built as expected @@ -538,7 +537,7 @@ func TestChainWriter_SubmitTransaction(t *testing.T) { require.Equal(t, account1, tx.Message.AccountKeys[1]) // account constant require.Equal(t, account2, tx.Message.AccountKeys[2]) // account lookup require.Equal(t, account3, tx.Message.AccountKeys[3]) // pda lookup - require.Equal(t, configProgramID, tx.Message.AccountKeys[4]) // instruction program ID + require.Equal(t, programID, tx.Message.AccountKeys[4]) // instruction program ID require.Len(t, tx.Message.AddressTableLookups, 1) // address table look contains entry require.Equal(t, derivedLookupTablePubkey, tx.Message.AddressTableLookups[0].AccountKey) // address table return true @@ -550,7 +549,7 @@ func TestChainWriter_SubmitTransaction(t *testing.T) { "seed1": seed1, "seed2": seed2, } - submitErr := cw.SubmitTransaction(ctx, "6AfuXF6HapDUhQfE4nQG9C1SGtA1YjP3icaJyRfU4RyE", "initializeLookupTable", args, txID, programID.String(), nil, nil) + submitErr := cw.SubmitTransaction(ctx, "contractReaderInterface", "initializeLookupTable", args, txID, programID.String(), nil, nil) require.NoError(t, submitErr) }) } diff --git a/pkg/solana/chainwriter/helpers.go b/pkg/solana/chainwriter/helpers.go index 22a2d1c67..8b7276276 100644 --- a/pkg/solana/chainwriter/helpers.go +++ b/pkg/solana/chainwriter/helpers.go @@ -56,7 +56,7 @@ func GetValuesAtLocation(args any, location string) ([][]byte, error) { } func GetDebugIDAtLocation(args any, location string) (string, error) { - debugIDList, err := GetValueAtLocation(args, location) + debugIDList, err := GetValuesAtLocation(args, location) if err != nil { return "", err } @@ -67,26 +67,6 @@ func GetDebugIDAtLocation(args any, location string) (string, error) { return debugID, nil } -func GetValueAtLocation(args any, location string) ([][]byte, error) { - path := strings.Split(location, ".") - - valueList, err := traversePath(args, path) - if err != nil { - return nil, err - } - - var values [][]byte - for _, value := range valueList { - byteArray, ok := value.([]byte) - if !ok { - return nil, fmt.Errorf("invalid value format at path: %s", location) - } - values = append(values, byteArray) - } - - return values, nil -} - func errorWithDebugID(err error, debugID string) error { if debugID == "" { return err diff --git a/pkg/solana/chainwriter/lookups.go b/pkg/solana/chainwriter/lookups.go index 37de7a567..7f2b45afe 100644 --- a/pkg/solana/chainwriter/lookups.go +++ b/pkg/solana/chainwriter/lookups.go @@ -212,6 +212,7 @@ func decodeBorshIntoType(data []byte, typ reflect.Type) (interface{}, 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, reader client.Reader) ([][]byte, error) { var seedBytes [][]byte + maxSeedLength := 32 for _, seed := range lookup.Seeds { if lookupSeed, ok := seed.(AccountLookup); ok { @@ -220,7 +221,13 @@ func getSeedBytes(ctx context.Context, lookup PDALookups, args any, derivedTable if err != nil { return nil, fmt.Errorf("error getting address seed: %w", err) } - seedBytes = append(seedBytes, bytes...) + // validate seed length + for _, b := range bytes { + if len(b) > maxSeedLength { + return nil, fmt.Errorf("seed byte array exceeds maximum length of %d: got %d bytes", maxSeedLength, len(b)) + } + seedBytes = append(seedBytes, b) + } } else { // Get address seeds from the lookup seedAddresses, err := GetAddresses(ctx, args, []Lookup{seed}, derivedTableMap, reader) @@ -240,10 +247,9 @@ func getSeedBytes(ctx context.Context, lookup PDALookups, args any, derivedTable // generatePDAs generates program-derived addresses (PDAs) from public keys and seeds. func generatePDAs(publicKeys []*solana.AccountMeta, seeds [][]byte, lookup PDALookups) ([]*solana.AccountMeta, error) { - if len(seeds) > 1 && len(publicKeys) > 1 { - return nil, fmt.Errorf("multiple public keys and multiple seeds are not allowed") + if len(seeds) > 16 { + return nil, fmt.Errorf("seed maximum exceeded: %d", len(seeds)) } - var addresses []*solana.AccountMeta for _, publicKeyMeta := range publicKeys { address, _, err := solana.FindProgramAddress(seeds, publicKeyMeta.PublicKey) From d656123c7ad91a373bc7634d148c68e74a4b74a7 Mon Sep 17 00:00:00 2001 From: Silas Lenihan Date: Thu, 19 Dec 2024 13:15:15 -0500 Subject: [PATCH 27/29] solved conflicts between TXM changes and unit test changes --- pkg/solana/chain.go | 2 +- pkg/solana/chainwriter/chain_writer.go | 2 +- pkg/solana/chainwriter/chain_writer_test.go | 2 +- pkg/solana/chainwriter/lookups.go | 6 ++--- pkg/solana/relay.go | 2 +- pkg/solana/transmitter_test.go | 2 +- pkg/solana/txm/mocks/tx_manager.go | 25 +++++++++--------- pkg/solana/txm/pendingtx.go | 7 +++--- pkg/solana/txm/pendingtx_test.go | 28 ++++++++++----------- pkg/solana/txm/txm.go | 24 +++++++++--------- pkg/solana/txm/txm_internal_test.go | 4 +-- 11 files changed, 53 insertions(+), 51 deletions(-) diff --git a/pkg/solana/chain.go b/pkg/solana/chain.go index 722f99bf5..0ccdf0e46 100644 --- a/pkg/solana/chain.go +++ b/pkg/solana/chain.go @@ -577,7 +577,7 @@ func (c *chain) sendTx(ctx context.Context, from, to string, amount *big.Int, ba chainTxm := c.TxManager() err = chainTxm.Enqueue(ctx, "", tx, nil, blockhash.Value.LastValidBlockHeight, - txm.SetComputeUnitLimit(500), // reduce from default 200K limit - should only take 450 compute units + txmutils.SetComputeUnitLimit(500), // reduce from default 200K limit - should only take 450 compute units // no fee bumping and no additional fee - makes validating balance accurate txmutils.SetComputeUnitPriceMax(0), txmutils.SetComputeUnitPriceMin(0), diff --git a/pkg/solana/chainwriter/chain_writer.go b/pkg/solana/chainwriter/chain_writer.go index e07842201..8616b9f62 100644 --- a/pkg/solana/chainwriter/chain_writer.go +++ b/pkg/solana/chainwriter/chain_writer.go @@ -274,7 +274,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, blockhash.Value.LastValidBlockHeight); err != nil { return errorWithDebugID(fmt.Errorf("error enqueuing transaction: %w", err), debugID) } diff --git a/pkg/solana/chainwriter/chain_writer_test.go b/pkg/solana/chainwriter/chain_writer_test.go index c7fab54e0..ef7b399af 100644 --- a/pkg/solana/chainwriter/chain_writer_test.go +++ b/pkg/solana/chainwriter/chain_writer_test.go @@ -541,7 +541,7 @@ func TestChainWriter_SubmitTransaction(t *testing.T) { require.Len(t, tx.Message.AddressTableLookups, 1) // address table look contains entry require.Equal(t, derivedLookupTablePubkey, tx.Message.AddressTableLookups[0].AccountKey) // address table return true - }), &txID).Return(nil).Once() + }), &txID, mock.Anything).Return(nil).Once() args := map[string]interface{}{ "lookupTable": chainwriter.GetRandomPubKey(t).Bytes(), diff --git a/pkg/solana/chainwriter/lookups.go b/pkg/solana/chainwriter/lookups.go index 7f2b45afe..9f1071c46 100644 --- a/pkg/solana/chainwriter/lookups.go +++ b/pkg/solana/chainwriter/lookups.go @@ -295,7 +295,7 @@ func (s *SolanaChainWriterService) ResolveLookupTables(ctx context.Context, args return nil, nil, fmt.Errorf("invalid static lookup table address: %s, error: %w", staticTable, err) } - addressses, err := getLookupTableAddress(ctx, s.reader, tableAddress) + addressses, err := getLookupTableAddresses(ctx, s.reader, tableAddress) if err != nil { return nil, nil, fmt.Errorf("error fetching static lookup table address: %w", err) } @@ -318,7 +318,7 @@ 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) + addresses, err := getLookupTableAddresses(ctx, reader, addressMeta.PublicKey) if err != nil { return nil, nil, fmt.Errorf("error fetching lookup table address: %w", err) } @@ -344,7 +344,7 @@ 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) (solana.PublicKeySlice, error) { +func getLookupTableAddresses(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", diff --git a/pkg/solana/relay.go b/pkg/solana/relay.go index 7b5c3b39f..5dfe8c836 100644 --- a/pkg/solana/relay.go +++ b/pkg/solana/relay.go @@ -33,7 +33,7 @@ type TxManager interface { // - txCfgs can be used to set custom tx configurations. // - If a txID is provided, it will be used to identify the tx. Otherwise, a random UUID will be generated. // - The caller needs to set the tx.Message.RecentBlockhash and provide the corresponding lastValidBlockHeight. These values are obtained from the GetLatestBlockhash RPC call. - Enqueue(ctx context.Context, accountID string, tx *solana.Transaction, txID *string, lastValidBlockHeight uint64, txCfgs ...txm.SetTxConfig) error + Enqueue(ctx context.Context, accountID string, tx *solana.Transaction, txID *string, lastValidBlockHeight uint64, txCfgs ...txmutils.SetTxConfig) error } var _ relaytypes.Relayer = &Relayer{} //nolint:staticcheck diff --git a/pkg/solana/transmitter_test.go b/pkg/solana/transmitter_test.go index 71d9d7bc3..ba1ec9cc3 100644 --- a/pkg/solana/transmitter_test.go +++ b/pkg/solana/transmitter_test.go @@ -27,7 +27,7 @@ type verifyTxSize struct { s *solana.PrivateKey } -func (txm verifyTxSize) Enqueue(_ context.Context, _ string, tx *solana.Transaction, txID *string, _ uint64, _ ...txm.SetTxConfig) error { +func (txm verifyTxSize) Enqueue(_ context.Context, _ string, tx *solana.Transaction, txID *string, _ uint64, _ ...txmutils.SetTxConfig) error { // additional components that transaction manager adds to the transaction require.NoError(txm.t, fees.SetComputeUnitPrice(tx, 0)) require.NoError(txm.t, fees.SetComputeUnitLimit(tx, 0)) diff --git a/pkg/solana/txm/mocks/tx_manager.go b/pkg/solana/txm/mocks/tx_manager.go index 50806a4da..7694703a3 100644 --- a/pkg/solana/txm/mocks/tx_manager.go +++ b/pkg/solana/txm/mocks/tx_manager.go @@ -71,14 +71,14 @@ func (_c *TxManager_Close_Call) RunAndReturn(run func() error) *TxManager_Close_ return _c } -// Enqueue provides a mock function with given fields: ctx, accountID, tx, txID, txCfgs -func (_m *TxManager) Enqueue(ctx context.Context, accountID string, tx *solana.Transaction, txID *string, txCfgs ...utils.SetTxConfig) error { +// Enqueue provides a mock function with given fields: ctx, accountID, tx, txID, txLastValidBlockHeight, txCfgs +func (_m *TxManager) Enqueue(ctx context.Context, accountID string, tx *solana.Transaction, txID *string, txLastValidBlockHeight uint64, txCfgs ...utils.SetTxConfig) error { _va := make([]interface{}, len(txCfgs)) for _i := range txCfgs { _va[_i] = txCfgs[_i] } var _ca []interface{} - _ca = append(_ca, ctx, accountID, tx, txID) + _ca = append(_ca, ctx, accountID, tx, txID, txLastValidBlockHeight) _ca = append(_ca, _va...) ret := _m.Called(_ca...) @@ -87,8 +87,8 @@ func (_m *TxManager) Enqueue(ctx context.Context, accountID string, tx *solana.T } var r0 error - if rf, ok := ret.Get(0).(func(context.Context, string, *solana.Transaction, *string, ...utils.SetTxConfig) error); ok { - r0 = rf(ctx, accountID, tx, txID, txCfgs...) + if rf, ok := ret.Get(0).(func(context.Context, string, *solana.Transaction, *string, uint64, ...utils.SetTxConfig) error); ok { + r0 = rf(ctx, accountID, tx, txID, txLastValidBlockHeight, txCfgs...) } else { r0 = ret.Error(0) } @@ -106,21 +106,22 @@ type TxManager_Enqueue_Call struct { // - accountID string // - tx *solana.Transaction // - txID *string +// - txLastValidBlockHeight uint64 // - txCfgs ...utils.SetTxConfig -func (_e *TxManager_Expecter) Enqueue(ctx interface{}, accountID interface{}, tx interface{}, txID interface{}, txCfgs ...interface{}) *TxManager_Enqueue_Call { +func (_e *TxManager_Expecter) Enqueue(ctx interface{}, accountID interface{}, tx interface{}, txID interface{}, txLastValidBlockHeight interface{}, txCfgs ...interface{}) *TxManager_Enqueue_Call { return &TxManager_Enqueue_Call{Call: _e.mock.On("Enqueue", - append([]interface{}{ctx, accountID, tx, txID}, txCfgs...)...)} + append([]interface{}{ctx, accountID, tx, txID, txLastValidBlockHeight}, txCfgs...)...)} } -func (_c *TxManager_Enqueue_Call) Run(run func(ctx context.Context, accountID string, tx *solana.Transaction, txID *string, txCfgs ...utils.SetTxConfig)) *TxManager_Enqueue_Call { +func (_c *TxManager_Enqueue_Call) Run(run func(ctx context.Context, accountID string, tx *solana.Transaction, txID *string, txLastValidBlockHeight uint64, txCfgs ...utils.SetTxConfig)) *TxManager_Enqueue_Call { _c.Call.Run(func(args mock.Arguments) { - variadicArgs := make([]utils.SetTxConfig, len(args)-4) - for i, a := range args[4:] { + variadicArgs := make([]utils.SetTxConfig, len(args)-5) + for i, a := range args[5:] { if a != nil { variadicArgs[i] = a.(utils.SetTxConfig) } } - run(args[0].(context.Context), args[1].(string), args[2].(*solana.Transaction), args[3].(*string), variadicArgs...) + run(args[0].(context.Context), args[1].(string), args[2].(*solana.Transaction), args[3].(*string), args[4].(uint64), variadicArgs...) }) return _c } @@ -130,7 +131,7 @@ func (_c *TxManager_Enqueue_Call) Return(_a0 error) *TxManager_Enqueue_Call { return _c } -func (_c *TxManager_Enqueue_Call) RunAndReturn(run func(context.Context, string, *solana.Transaction, *string, ...utils.SetTxConfig) error) *TxManager_Enqueue_Call { +func (_c *TxManager_Enqueue_Call) RunAndReturn(run func(context.Context, string, *solana.Transaction, *string, uint64, ...utils.SetTxConfig) error) *TxManager_Enqueue_Call { _c.Call.Return(run) return _c } diff --git a/pkg/solana/txm/pendingtx.go b/pkg/solana/txm/pendingtx.go index 3b60f0248..7784c47cd 100644 --- a/pkg/solana/txm/pendingtx.go +++ b/pkg/solana/txm/pendingtx.go @@ -11,6 +11,7 @@ import ( "golang.org/x/exp/maps" "github.com/smartcontractkit/chainlink-solana/pkg/solana/txm/utils" + txmutils "github.com/smartcontractkit/chainlink-solana/pkg/solana/txm/utils" ) var ( @@ -54,11 +55,11 @@ type PendingTxContext interface { // finishedTx is used to store info required to track transactions to finality or error type pendingTx struct { tx solana.Transaction - cfg TxConfig + cfg txmutils.TxConfig signatures []solana.Signature id string createTs time.Time - state TxState + state txmutils.TxState lastValidBlockHeight uint64 // to track expiration, equivalent to last valid block number. } @@ -234,7 +235,7 @@ func (c *pendingTxContext) ListAllExpiredBroadcastedTxs(currBlockNumber uint64) defer c.lock.RUnlock() expiredBroadcastedTxs := make([]pendingTx, 0, len(c.broadcastedProcessedTxs)) // worst case, all of them for _, tx := range c.broadcastedProcessedTxs { - if tx.state == Broadcasted && tx.lastValidBlockHeight < currBlockNumber { + if tx.state == txmutils.Broadcasted && tx.lastValidBlockHeight < currBlockNumber { expiredBroadcastedTxs = append(expiredBroadcastedTxs, tx) } } diff --git a/pkg/solana/txm/pendingtx_test.go b/pkg/solana/txm/pendingtx_test.go index ea8e65162..b082b2162 100644 --- a/pkg/solana/txm/pendingtx_test.go +++ b/pkg/solana/txm/pendingtx_test.go @@ -94,7 +94,7 @@ func TestPendingTxContext_new(t *testing.T) { require.Equal(t, sig, tx.signatures[0], "signature should match") // Check status is Broadcasted - require.Equal(t, Broadcasted, tx.state, "transaction state should be Broadcasted") + require.Equal(t, utils.Broadcasted, tx.state, "transaction state should be Broadcasted") // Check it does not exist in confirmed nor finalized maps _, exists = txs.confirmedTxs[msg.id] @@ -1195,12 +1195,12 @@ func TestPendingTxContext_ListAllExpiredBroadcastedTxs(t *testing.T) { setup: func(t *testing.T, ctx *pendingTxContext) { tx1 := pendingTx{ id: "tx1", - state: Broadcasted, + state: utils.Broadcasted, lastValidBlockHeight: 1500, } tx2 := pendingTx{ id: "tx2", - state: Broadcasted, + state: utils.Broadcasted, lastValidBlockHeight: 1600, } ctx.broadcastedProcessedTxs["tx1"] = tx1 @@ -1214,17 +1214,17 @@ func TestPendingTxContext_ListAllExpiredBroadcastedTxs(t *testing.T) { setup: func(t *testing.T, ctx *pendingTxContext) { tx1 := pendingTx{ id: "tx1", - state: Broadcasted, + state: utils.Broadcasted, lastValidBlockHeight: 1000, } tx2 := pendingTx{ id: "tx2", - state: Broadcasted, + state: utils.Broadcasted, lastValidBlockHeight: 1500, } tx3 := pendingTx{ id: "tx3", - state: Broadcasted, + state: utils.Broadcasted, lastValidBlockHeight: 900, } ctx.broadcastedProcessedTxs["tx1"] = tx1 @@ -1239,12 +1239,12 @@ func TestPendingTxContext_ListAllExpiredBroadcastedTxs(t *testing.T) { setup: func(t *testing.T, ctx *pendingTxContext) { tx1 := pendingTx{ id: "tx1", - state: Broadcasted, + state: utils.Broadcasted, lastValidBlockHeight: 1000, } tx2 := pendingTx{ id: "tx2", - state: Broadcasted, + state: utils.Broadcasted, lastValidBlockHeight: 1500, } ctx.broadcastedProcessedTxs["tx1"] = tx1 @@ -1258,17 +1258,17 @@ func TestPendingTxContext_ListAllExpiredBroadcastedTxs(t *testing.T) { setup: func(t *testing.T, ctx *pendingTxContext) { tx1 := pendingTx{ id: "tx1", - state: Broadcasted, + state: utils.Broadcasted, lastValidBlockHeight: 800, } tx2 := pendingTx{ id: "tx2", - state: Processed, // Not Broadcasted + state: utils.Processed, // Not Broadcasted lastValidBlockHeight: 700, } tx3 := pendingTx{ id: "tx3", - state: Processed, // Not Broadcasted + state: utils.Processed, // Not Broadcasted lastValidBlockHeight: 600, } ctx.broadcastedProcessedTxs["tx1"] = tx1 @@ -1283,17 +1283,17 @@ func TestPendingTxContext_ListAllExpiredBroadcastedTxs(t *testing.T) { setup: func(t *testing.T, ctx *pendingTxContext) { tx1 := pendingTx{ id: "tx1", - state: Broadcasted, + state: utils.Broadcasted, lastValidBlockHeight: 1000, } tx2 := pendingTx{ id: "tx2", - state: Broadcasted, + state: utils.Broadcasted, lastValidBlockHeight: 999, } tx3 := pendingTx{ id: "tx3", - state: Broadcasted, + state: utils.Broadcasted, lastValidBlockHeight: 1, } ctx.broadcastedProcessedTxs["tx1"] = tx1 diff --git a/pkg/solana/txm/txm.go b/pkg/solana/txm/txm.go index 8c69e1c30..c87089060 100644 --- a/pkg/solana/txm/txm.go +++ b/pkg/solana/txm/txm.go @@ -46,7 +46,7 @@ var _ loop.Keystore = (SimpleKeystore)(nil) type TxManager interface { services.Service - Enqueue(ctx context.Context, accountID string, tx *solanaGo.Transaction, txID *string, txCfgs ...txmutils.SetTxConfig) error + Enqueue(ctx context.Context, accountID string, tx *solanaGo.Transaction, txID *string, txLastValidBlockHeight uint64, txCfgs ...txmutils.SetTxConfig) error GetTransactionStatus(ctx context.Context, transactionID string) (commontypes.TransactionStatus, error) } @@ -189,7 +189,7 @@ func (txm *Txm) sendWithRetry(ctx context.Context, msg pendingTx) (solanaGo.Tran if initSendErr != nil { // Do not retry and exit early if fails cancel() - stateTransitionErr := txm.txs.OnPrebroadcastError(msg.id, txm.cfg.TxRetentionTimeout(), Errored, TxFailReject) + stateTransitionErr := txm.txs.OnPrebroadcastError(msg.id, txm.cfg.TxRetentionTimeout(), txmutils.Errored, TxFailReject) return solanaGo.Transaction{}, "", solanaGo.Signature{}, fmt.Errorf("tx failed initial transmit: %w", errors.Join(initSendErr, stateTransitionErr)) } @@ -202,7 +202,7 @@ func (txm *Txm) sendWithRetry(ctx context.Context, msg pendingTx) (solanaGo.Tran txm.lggr.Debugw("tx initial broadcast", "id", msg.id, "fee", msg.cfg.BaseComputeUnitPrice, "signature", sig, "lastValidBlockHeight", msg.lastValidBlockHeight) // Initialize signature list with initialTx signature. This list will be used to add new signatures and track retry attempts. - sigs := &signatureList{} + sigs := &txmutils.SignatureList{} sigs.Allocate() if initSetErr := sigs.Set(0, sig); initSetErr != nil { return solanaGo.Transaction{}, "", solanaGo.Signature{}, fmt.Errorf("failed to save initial signature in signature list: %w", initSetErr) @@ -263,7 +263,7 @@ func (txm *Txm) buildTx(ctx context.Context, msg pendingTx, retryCount int) (sol // retryTx contains the logic for retrying the transaction, including exponential backoff and fee bumping. // Retries until context cancelled by timeout or called externally. // It uses handleRetry helper function to handle each retry attempt. -func (txm *Txm) retryTx(ctx context.Context, msg pendingTx, currentTx solanaGo.Transaction, sigs *signatureList) { +func (txm *Txm) retryTx(ctx context.Context, msg pendingTx, currentTx solanaGo.Transaction, sigs *txmutils.SignatureList) { deltaT := 1 // initial delay in ms tick := time.After(0) bumpCount := 0 @@ -300,7 +300,7 @@ func (txm *Txm) retryTx(ctx context.Context, msg pendingTx, currentTx solanaGo.T } // Start a goroutine to handle the retry attempt - // takes currentTx and rebroadcast. If needs bumping it will new signature to already allocated space in signatureList. + // takes currentTx and rebroadcast. If needs bumping it will new signature to already allocated space in txmutils.SignatureList. wg.Add(1) go func(bump bool, count int, retryTx solanaGo.Transaction) { defer wg.Done() @@ -318,7 +318,7 @@ func (txm *Txm) retryTx(ctx context.Context, msg pendingTx, currentTx solanaGo.T } // handleRetry handles the logic for each retry attempt, including sending the transaction, updating signatures, and logging. -func (txm *Txm) handleRetry(ctx context.Context, msg pendingTx, bump bool, count int, retryTx solanaGo.Transaction, sigs *signatureList) { +func (txm *Txm) handleRetry(ctx context.Context, msg pendingTx, bump bool, count int, retryTx solanaGo.Transaction, sigs *txmutils.SignatureList) { // send retry transaction retrySig, err := txm.sendTx(ctx, &retryTx) if err != nil { @@ -420,7 +420,7 @@ func (txm *Txm) processConfirmations(ctx context.Context, client client.ReaderWr defer wg.Done() // to process successful first - sortedSigs, sortedRes, err := SortSignaturesAndResults(sigsBatch[i], statuses) + sortedSigs, sortedRes, err := txmutils.SortSignaturesAndResults(sigsBatch[i], statuses) if err != nil { txm.lggr.Errorw("sorting error", "error", err) return @@ -468,7 +468,7 @@ func (txm *Txm) processConfirmations(ctx context.Context, client client.ReaderWr func (txm *Txm) handleNotFoundSignatureStatus(sig solanaGo.Signature) { txm.lggr.Debugw("tx state: not found", "signature", sig) if txm.cfg.TxConfirmTimeout() != 0*time.Second && txm.txs.Expired(sig, txm.cfg.TxConfirmTimeout()) { - id, err := txm.txs.OnError(sig, txm.cfg.TxRetentionTimeout(), Errored, TxFailDrop) + id, err := txm.txs.OnError(sig, txm.cfg.TxRetentionTimeout(), txmutils.Errored, TxFailDrop) if err != nil { txm.lggr.Infow("failed to mark transaction as errored", "id", id, "signature", sig, "timeoutSeconds", txm.cfg.TxConfirmTimeout(), "error", err) } else { @@ -512,7 +512,7 @@ func (txm *Txm) handleProcessedSignatureStatus(sig solanaGo.Signature) { } // check confirm timeout exceeded if TxConfirmTimeout set if txm.cfg.TxConfirmTimeout() != 0*time.Second && txm.txs.Expired(sig, txm.cfg.TxConfirmTimeout()) { - id, err := txm.txs.OnError(sig, txm.cfg.TxRetentionTimeout(), Errored, TxFailDrop) + id, err := txm.txs.OnError(sig, txm.cfg.TxRetentionTimeout(), txmutils.Errored, TxFailDrop) if err != nil { txm.lggr.Infow("failed to mark transaction as errored", "id", id, "signature", sig, "timeoutSeconds", txm.cfg.TxConfirmTimeout(), "error", err) } else { @@ -591,7 +591,7 @@ func (txm *Txm) rebroadcastExpiredTxs(ctx context.Context, client client.ReaderW // call sendWithRetry directly to avoid enqueuing _, _, _, sendErr := txm.sendWithRetry(ctx, rebroadcastTx) if sendErr != nil { - stateTransitionErr := txm.txs.OnPrebroadcastError(tx.id, txm.cfg.TxRetentionTimeout(), Errored, TxFailReject) + stateTransitionErr := txm.txs.OnPrebroadcastError(tx.id, txm.cfg.TxRetentionTimeout(), txmutils.Errored, TxFailReject) txm.lggr.Errorw("failed to rebroadcast transaction", "id", tx.id, "error", errors.Join(sendErr, stateTransitionErr)) continue } @@ -667,7 +667,7 @@ func (txm *Txm) reap() { } // Enqueue enqueues a msg destined for the solana chain. -func (txm *Txm) Enqueue(ctx context.Context, accountID string, tx *solanaGo.Transaction, txID *string, txLastValidBlockHeight uint64, txCfgs ...SetTxConfig) error { +func (txm *Txm) Enqueue(ctx context.Context, accountID string, tx *solanaGo.Transaction, txID *string, txLastValidBlockHeight uint64, txCfgs ...txmutils.SetTxConfig) error { if err := txm.Ready(); err != nil { return fmt.Errorf("error in soltxm.Enqueue: %w", err) } @@ -834,7 +834,7 @@ func (txm *Txm) simulateTx(ctx context.Context, tx *solanaGo.Transaction) (res * } // ProcessError parses and handles relevant errors found in simulation results -func (txm *Txm) ProcessError(sig solanaGo.Signature, resErr interface{}, simulation bool) (txState TxState, errType TxErrType) { +func (txm *Txm) ProcessError(sig solanaGo.Signature, resErr interface{}, simulation bool) (txState txmutils.TxState, errType TxErrType) { if resErr != nil { // handle various errors // https://github.com/solana-labs/solana/blob/master/sdk/src/transaction/error.rs diff --git a/pkg/solana/txm/txm_internal_test.go b/pkg/solana/txm/txm_internal_test.go index 740c2b500..15e4631a3 100644 --- a/pkg/solana/txm/txm_internal_test.go +++ b/pkg/solana/txm/txm_internal_test.go @@ -688,7 +688,7 @@ func TestTxm(t *testing.T) { // send tx - with disabled fee bumping testTxID := uuid.New().String() lastValidBlockHeight := uint64(100) - assert.NoError(t, txm.Enqueue(ctx, t.Name(), tx, &testTxID, lastValidBlockHeight, SetFeeBumpPeriod(0))) + assert.NoError(t, txm.Enqueue(ctx, t.Name(), tx, &testTxID, lastValidBlockHeight, txmutils.SetFeeBumpPeriod(0))) wg.Wait() // no transactions stored inflight txs list @@ -741,7 +741,7 @@ func TestTxm(t *testing.T) { // send tx - with disabled fee bumping and disabled compute unit limit testTxID := uuid.New().String() lastValidBlockHeight := uint64(100) - assert.NoError(t, txm.Enqueue(ctx, t.Name(), tx, &testTxID, lastValidBlockHeight, SetFeeBumpPeriod(0), SetComputeUnitLimit(0))) + assert.NoError(t, txm.Enqueue(ctx, t.Name(), tx, &testTxID, lastValidBlockHeight, txmutils.SetFeeBumpPeriod(0), txmutils.SetComputeUnitLimit(0))) wg.Wait() // no transactions stored inflight txs list From f6302aca7441b9dae732b25ca19e6f14c82705d7 Mon Sep 17 00:00:00 2001 From: Silas Lenihan Date: Fri, 20 Dec 2024 11:42:21 -0500 Subject: [PATCH 28/29] updated comments and slight tweaks --- .../relayinterface/lookups_test.go | 6 +-- pkg/solana/chainwriter/ccip_example_config.go | 12 ++--- pkg/solana/chainwriter/chain_writer.go | 44 +++++++++++++++---- pkg/solana/chainwriter/chain_writer_test.go | 22 +++++----- pkg/solana/chainwriter/helpers.go | 3 ++ pkg/solana/chainwriter/lookups.go | 32 +++++--------- 6 files changed, 72 insertions(+), 47 deletions(-) diff --git a/integration-tests/relayinterface/lookups_test.go b/integration-tests/relayinterface/lookups_test.go index 1b91dc8df..7333b2e8d 100644 --- a/integration-tests/relayinterface/lookups_test.go +++ b/integration-tests/relayinterface/lookups_test.go @@ -126,7 +126,7 @@ func TestAccountLookups(t *testing.T) { } func TestPDALookups(t *testing.T) { - programID := solana.SystemProgramID + programID := chainwriter.GetRandomPubKey(t) t.Run("PDALookup resolves valid PDA with constant address seeds", func(t *testing.T) { seed := chainwriter.GetRandomPubKey(t) @@ -281,7 +281,7 @@ func TestLookupTables(t *testing.T) { table := chainwriter.CreateTestLookupTable(ctx, t, rpcClient, sender, pubKeys) lookupConfig := chainwriter.LookupTables{ DerivedLookupTables: nil, - StaticLookupTables: []string{table.String()}, + StaticLookupTables: []solana.PublicKey{table}, } _, staticTableMap, resolveErr := cw.ResolveLookupTables(ctx, nil, lookupConfig) require.NoError(t, resolveErr) @@ -342,7 +342,7 @@ func TestLookupTables(t *testing.T) { lookupConfig := chainwriter.LookupTables{ DerivedLookupTables: nil, - StaticLookupTables: []string{invalidTable.String()}, + StaticLookupTables: []solana.PublicKey{invalidTable}, } _, _, err = cw.ResolveLookupTables(ctx, nil, lookupConfig) diff --git a/pkg/solana/chainwriter/ccip_example_config.go b/pkg/solana/chainwriter/ccip_example_config.go index acdaf3d35..f277935d9 100644 --- a/pkg/solana/chainwriter/ccip_example_config.go +++ b/pkg/solana/chainwriter/ccip_example_config.go @@ -1,7 +1,7 @@ package chainwriter import ( - "fmt" + "github.com/gagliardetto/solana-go" ) func TestConfig() { @@ -13,8 +13,8 @@ func TestConfig() { systemProgramAddress := "4Nn9dsYBcSTzRbK9hg9kzCUdrCSkMZq1UR6Vw1Tkaf6E" computeBudgetProgramAddress := "4Nn9dsYBcSTzRbK9hg9kzCUdrCSkMZq1UR6Vw1Tkaf6F" sysvarProgramAddress := "4Nn9dsYBcSTzRbK9hg9kzCUdrCSkMZq1UR6Vw1Tkaf6G" - commonAddressesLookupTable := "4Nn9dsYBcSTzRbK9hg9kzCUdrCSkMZq1UR6Vw1Tkaf6H" - routerLookupTable := "4Nn9dsYBcSTzRbK9hg9kzCUdrCSkMZq1UR6Vw1Tkaf6I" + commonAddressesLookupTable := solana.MustPublicKeyFromBase58("4Nn9dsYBcSTzRbK9hg9kzCUdrCSkMZq1UR6Vw1Tkaf6H") + routerLookupTable := solana.MustPublicKeyFromBase58("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"}]}}` @@ -58,7 +58,7 @@ func TestConfig() { // 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{ + StaticLookupTables: []solana.PublicKey{ commonAddressesLookupTable, routerLookupTable, }, @@ -255,7 +255,7 @@ func TestConfig() { InputModifications: nil, ChainSpecificName: "commit", LookupTables: LookupTables{ - StaticLookupTables: []string{ + StaticLookupTables: []solana.PublicKey{ commonAddressesLookupTable, routerLookupTable, }, @@ -329,5 +329,5 @@ func TestConfig() { }, }, } - fmt.Println(chainWriterConfig) + _ = chainWriterConfig } diff --git a/pkg/solana/chainwriter/chain_writer.go b/pkg/solana/chainwriter/chain_writer.go index 8616b9f62..e16a55e60 100644 --- a/pkg/solana/chainwriter/chain_writer.go +++ b/pkg/solana/chainwriter/chain_writer.go @@ -135,8 +135,6 @@ for Solana transactions. It handles constant addresses, dynamic lookups, program ### 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 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 { @@ -149,6 +147,10 @@ func GetAddresses(ctx context.Context, args any, accounts []Lookup, derivedTable return addresses, nil } +// FilterLookupTableAddresses takes a list of accounts and two lookup table maps +// (one for derived tables, one for static tables) and filters out any addresses that are +// not used by the accounts. It returns a map of only those lookup table +// addresses that match entries in `accounts`. func (s *SolanaChainWriterService) FilterLookupTableAddresses( accounts []*solana.AccountMeta, derivedTableMap map[string]map[string][]*solana.AccountMeta, @@ -203,7 +205,30 @@ func (s *SolanaChainWriterService) FilterLookupTableAddresses( 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 { +// SubmitTransaction builds, encodes, and enqueues a transaction using the provided program +// configuration and method details. It relies on the configured IDL, account lookups, and +// lookup tables to gather the necessary accounts and data. The function retrieves the latest +// blockhash and assigns it to the transaction, so callers do not need to provide one. +// +// Submissions and retries are handled by the underlying transaction manager. If a “debug ID” +// location is configured, SubmitTransaction extracts it from the provided `args` and attaches +// it to errors for easier troubleshooting. Only the first debug ID it encounters will be used. +// +// Parameters: +// - ctx: The context for cancellation and timeouts. +// - contractName: Identifies which Solana program config to use from `s.config.Programs`. +// - method: Specifies which method config to invoke within the chosen program config. +// - args: Arbitrary arguments that are encoded into the transaction payload and/or used for dynamic address lookups. +// - transactionID: A unique identifier for the transaction, used for tracking within the transaction manager. +// - toAddress: The on-chain address (program ID) to which the transaction is directed. +// - meta: Currently unused; included for interface compatibility. +// - value: Currently unused; included for interface compatibility. +// +// Returns: +// +// An error if any stage of the transaction preparation or enqueueing fails. A nil return +// indicates that the transaction was successfully submitted to the transaction manager. +func (s *SolanaChainWriterService) SubmitTransaction(ctx context.Context, contractName, method string, args any, transactionID string, toAddress string, _ *types.TxMeta, _ *big.Int) error { programConfig, exists := s.config.Programs[contractName] if !exists { return fmt.Errorf("failed to find program config for contract name: %s", contractName) @@ -241,6 +266,14 @@ func (s *SolanaChainWriterService) SubmitTransaction(ctx context.Context, contra return errorWithDebugID(fmt.Errorf("error resolving account addresses: %w", err), debugID) } + feePayer, err := solana.PublicKeyFromBase58(methodConfig.FromAddress) + if err != nil { + return errorWithDebugID(fmt.Errorf("error parsing fee payer address: %w", err), debugID) + } + + accounts = append([]*solana.AccountMeta{solana.Meta(feePayer).SIGNER().WRITE()}, accounts...) + accounts = append(accounts, solana.Meta(solana.SystemProgramID)) + // Filter the lookup table addresses based on which accounts are actually used filteredLookupTableMap := s.FilterLookupTableAddresses(accounts, derivedTableMap, staticTableMap) @@ -256,11 +289,6 @@ func (s *SolanaChainWriterService) SubmitTransaction(ctx context.Context, contra return errorWithDebugID(fmt.Errorf("error parsing program ID: %w", err), debugID) } - feePayer, err := solana.PublicKeyFromBase58(methodConfig.FromAddress) - if err != nil { - return errorWithDebugID(fmt.Errorf("error parsing fee payer address: %w", err), debugID) - } - tx, err := solana.NewTransaction( []solana.Instruction{ solana.NewInstruction(programID, accounts, encodedPayload), diff --git a/pkg/solana/chainwriter/chain_writer_test.go b/pkg/solana/chainwriter/chain_writer_test.go index ef7b399af..9798bdb4c 100644 --- a/pkg/solana/chainwriter/chain_writer_test.go +++ b/pkg/solana/chainwriter/chain_writer_test.go @@ -273,7 +273,7 @@ func TestChainWriter_FilterLookupTableAddresses(t *testing.T) { Name: "DataAccountPDA", PublicKey: chainwriter.AccountConstant{Name: "WriteTest", Address: programID.String()}, Seeds: []chainwriter.Lookup{ - // extract seed2 for PDA lookup + // extract seed1 for PDA lookup chainwriter.AccountLookup{Name: "seed1", Location: "seed1"}, }, IsSigner: true, @@ -302,7 +302,7 @@ func TestChainWriter_FilterLookupTableAddresses(t *testing.T) { }, }, }, - StaticLookupTables: []string{staticLookupTablePubkey1.String(), staticLookupTablePubkey2.String()}, + StaticLookupTables: []solana.PublicKey{staticLookupTablePubkey1, staticLookupTablePubkey2}, } args := map[string]interface{}{ @@ -441,7 +441,7 @@ func TestChainWriter_SubmitTransaction(t *testing.T) { }, }, }, - StaticLookupTables: []string{staticLookupTablePubkey.String()}, + StaticLookupTables: []solana.PublicKey{staticLookupTablePubkey}, }, Accounts: []chainwriter.Lookup{ chainwriter.AccountConstant{ @@ -528,16 +528,18 @@ func TestChainWriter_SubmitTransaction(t *testing.T) { rw.On("LatestBlockhash", mock.Anything).Return(&rpc.GetLatestBlockhashResult{Value: &rpc.LatestBlockhashResult{Blockhash: recentBlockHash, LastValidBlockHeight: uint64(100)}}, nil).Once() txID := uuid.NewString() - txm.On("Enqueue", mock.Anything, account1.String(), mock.MatchedBy(func(tx *solana.Transaction) bool { + txm.On("Enqueue", mock.Anything, admin.String(), mock.MatchedBy(func(tx *solana.Transaction) bool { // match transaction fields to ensure it was built as expected require.Equal(t, recentBlockHash, tx.Message.RecentBlockhash) require.Len(t, tx.Message.Instructions, 1) - require.Len(t, tx.Message.AccountKeys, 5) // fee payer + derived accounts - require.Equal(t, admin, tx.Message.AccountKeys[0]) // fee payer - require.Equal(t, account1, tx.Message.AccountKeys[1]) // account constant - require.Equal(t, account2, tx.Message.AccountKeys[2]) // account lookup - require.Equal(t, account3, tx.Message.AccountKeys[3]) // pda lookup - require.Equal(t, programID, tx.Message.AccountKeys[4]) // instruction program ID + require.Len(t, tx.Message.AccountKeys, 6) // fee payer + derived accounts + require.Equal(t, admin, tx.Message.AccountKeys[0]) // fee payer + require.Equal(t, account1, tx.Message.AccountKeys[1]) // account constant + require.Equal(t, account2, tx.Message.AccountKeys[2]) // account lookup + require.Equal(t, account3, tx.Message.AccountKeys[3]) // pda lookup + require.Equal(t, solana.SystemProgramID, tx.Message.AccountKeys[4]) // system program ID + require.Equal(t, programID, tx.Message.AccountKeys[5]) // instruction program ID + // instruction program ID require.Len(t, tx.Message.AddressTableLookups, 1) // address table look contains entry require.Equal(t, derivedLookupTablePubkey, tx.Message.AddressTableLookups[0].AccountKey) // address table return true diff --git a/pkg/solana/chainwriter/helpers.go b/pkg/solana/chainwriter/helpers.go index 8b7276276..6f78c7a63 100644 --- a/pkg/solana/chainwriter/helpers.go +++ b/pkg/solana/chainwriter/helpers.go @@ -61,6 +61,9 @@ func GetDebugIDAtLocation(args any, location string) (string, error) { return "", err } + if len(debugIDList) == 0 { + return "", errors.New("no debug ID found at location: " + location) + } // there should only be one debug ID, others will be ignored. debugID := string(debugIDList[0]) diff --git a/pkg/solana/chainwriter/lookups.go b/pkg/solana/chainwriter/lookups.go index 9f1071c46..b9d3ca7cd 100644 --- a/pkg/solana/chainwriter/lookups.go +++ b/pkg/solana/chainwriter/lookups.go @@ -52,14 +52,10 @@ type InternalField struct { Location string } -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 + StaticLookupTables []solana.PublicKey } // DerivedLookupTable represents a lookup table that is used to derive addresses for a program. @@ -212,19 +208,18 @@ func decodeBorshIntoType(data []byte, typ reflect.Type) (interface{}, 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, reader client.Reader) ([][]byte, error) { var seedBytes [][]byte - maxSeedLength := 32 for _, seed := range lookup.Seeds { if lookupSeed, ok := seed.(AccountLookup); ok { - // Get value from a location (This doens't have to be an address, it can be any value) + // Get value from a location (This doesn't have to be an address, it can be any value) bytes, err := GetValuesAtLocation(args, lookupSeed.Location) if err != nil { return nil, fmt.Errorf("error getting address seed: %w", err) } // validate seed length for _, b := range bytes { - if len(b) > maxSeedLength { - return nil, fmt.Errorf("seed byte array exceeds maximum length of %d: got %d bytes", maxSeedLength, len(b)) + if len(b) > solana.MaxSeedLength { + return nil, fmt.Errorf("seed byte array exceeds maximum length of %d: got %d bytes", solana.MaxSeedLength, len(b)) } seedBytes = append(seedBytes, b) } @@ -247,7 +242,7 @@ func getSeedBytes(ctx context.Context, lookup PDALookups, args any, derivedTable // generatePDAs generates program-derived addresses (PDAs) from public keys and seeds. func generatePDAs(publicKeys []*solana.AccountMeta, seeds [][]byte, lookup PDALookups) ([]*solana.AccountMeta, error) { - if len(seeds) > 16 { + if len(seeds) > solana.MaxSeeds { return nil, fmt.Errorf("seed maximum exceeded: %d", len(seeds)) } var addresses []*solana.AccountMeta @@ -271,6 +266,8 @@ func (s *SolanaChainWriterService) ResolveLookupTables(ctx context.Context, args // Read derived lookup tables for _, derivedLookup := range lookupTables.DerivedLookupTables { + // Load the lookup table - note: This could be multiple tables if the lookup is a PDALookups that resovles to more + // than one address lookupTableMap, _, err := s.LoadTable(ctx, args, derivedLookup, s.reader, derivedTableMap) if err != nil { return nil, nil, fmt.Errorf("error loading derived lookup table: %w", err) @@ -289,17 +286,11 @@ func (s *SolanaChainWriterService) ResolveLookupTables(ctx context.Context, args // 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, fmt.Errorf("invalid static lookup table address: %s, error: %w", staticTable, err) - } - - addressses, err := getLookupTableAddresses(ctx, s.reader, tableAddress) + addressses, err := getLookupTableAddresses(ctx, s.reader, staticTable) if err != nil { return nil, nil, fmt.Errorf("error fetching static lookup table address: %w", err) } - staticTableMap[tableAddress] = addressses + staticTableMap[staticTable] = addressses } return derivedTableMap, staticTableMap, nil @@ -312,15 +303,16 @@ func (s *SolanaChainWriterService) LoadTable(ctx context.Context, args any, rlt return nil, nil, fmt.Errorf("error resolving addresses for lookup table: %w", err) } + // Nested map in case the lookup table resolves to multiple addresses 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 + // Read the full list of addresses from the lookup table addresses, err := getLookupTableAddresses(ctx, reader, addressMeta.PublicKey) if err != nil { - return nil, nil, fmt.Errorf("error fetching lookup table address: %w", err) + return nil, nil, fmt.Errorf("error fetching lookup table address: %s, error: %w", addressMeta.PublicKey, err) } // Create the inner map for this lookup table From 77e4ee69448d1b4d70e8cb5257a05504d8851915 Mon Sep 17 00:00:00 2001 From: Silas Lenihan Date: Fri, 27 Dec 2024 14:38:06 -0500 Subject: [PATCH 29/29] Updated PDALookups Seeds field and fixed default accounts --- .../relayinterface/lookups_test.go | 24 +-- pkg/solana/chainwriter/ccip_example_config.go | 44 +++--- pkg/solana/chainwriter/chain_writer.go | 105 +++++++++++- pkg/solana/chainwriter/chain_writer_test.go | 38 +++-- pkg/solana/chainwriter/helpers.go | 6 +- pkg/solana/chainwriter/lookups.go | 149 ++++-------------- pkg/solana/chainwriter/testContractIDL.json | 1 + 7 files changed, 197 insertions(+), 170 deletions(-) create mode 100644 pkg/solana/chainwriter/testContractIDL.json diff --git a/integration-tests/relayinterface/lookups_test.go b/integration-tests/relayinterface/lookups_test.go index 7333b2e8d..fd148abff 100644 --- a/integration-tests/relayinterface/lookups_test.go +++ b/integration-tests/relayinterface/lookups_test.go @@ -145,8 +145,8 @@ func TestPDALookups(t *testing.T) { pdaLookup := chainwriter.PDALookups{ Name: "TestPDA", PublicKey: chainwriter.AccountConstant{Name: "ProgramID", Address: programID.String()}, - Seeds: []chainwriter.Lookup{ - chainwriter.AccountConstant{Name: "seed", Address: seed.String()}, + Seeds: []chainwriter.Seed{ + {Dynamic: chainwriter.AccountConstant{Name: "seed", Address: seed.String()}}, }, IsSigner: false, IsWritable: true, @@ -175,9 +175,9 @@ func TestPDALookups(t *testing.T) { 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"}, + Seeds: []chainwriter.Seed{ + {Dynamic: chainwriter.AccountLookup{Name: "seed1", Location: "test_seed"}}, + {Dynamic: chainwriter.AccountLookup{Name: "seed2", Location: "another_seed"}}, }, IsSigner: false, IsWritable: true, @@ -198,8 +198,8 @@ func TestPDALookups(t *testing.T) { pdaLookup := chainwriter.PDALookups{ Name: "TestPDA", PublicKey: chainwriter.AccountConstant{Name: "ProgramID", Address: programID.String()}, - Seeds: []chainwriter.Lookup{ - chainwriter.AccountLookup{Name: "seed1", Location: "MissingSeed"}, + Seeds: []chainwriter.Seed{ + {Dynamic: chainwriter.AccountLookup{Name: "seed1", Location: "MissingSeed"}}, }, IsSigner: false, IsWritable: true, @@ -233,9 +233,9 @@ func TestPDALookups(t *testing.T) { 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"}, + Seeds: []chainwriter.Seed{ + {Dynamic: chainwriter.AccountLookup{Name: "seed1", Location: "test_seed"}}, + {Dynamic: chainwriter.AccountLookup{Name: "seed2", Location: "another_seed"}}, }, IsSigner: false, IsWritable: true, @@ -403,8 +403,8 @@ func TestLookupTables(t *testing.T) { Accounts: chainwriter.PDALookups{ Name: "DataAccountPDA", PublicKey: chainwriter.AccountConstant{Name: "WriteTest", Address: programID.String()}, - Seeds: []chainwriter.Lookup{ - chainwriter.AccountLookup{Name: "seed1", Location: "seed1"}, + Seeds: []chainwriter.Seed{ + {Dynamic: chainwriter.AccountLookup{Name: "seed1", Location: "seed1"}}, }, IsSigner: false, IsWritable: false, diff --git a/pkg/solana/chainwriter/ccip_example_config.go b/pkg/solana/chainwriter/ccip_example_config.go index f277935d9..adbd4d324 100644 --- a/pkg/solana/chainwriter/ccip_example_config.go +++ b/pkg/solana/chainwriter/ccip_example_config.go @@ -47,8 +47,8 @@ func TestConfig() { 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: []Lookup{ - AccountLookup{Location: "Message.TokenAmounts.DestTokenAddress"}, + Seeds: []Seed{ + {Dynamic: AccountLookup{Location: "Message.TokenAmounts.DestTokenAddress"}}, }, IsSigner: false, IsWritable: false, @@ -86,9 +86,9 @@ func TestConfig() { IsWritable: false, }, // Similar to the RegistryTokenState above, the user is looking up PDA accounts based on the dest tokens. - Seeds: []Lookup{ - AccountLookup{Location: "Message.TokenAmounts.DestTokenAddress"}, - AccountLookup{Location: "Message.Header.DestChainSelector"}, + Seeds: []Seed{ + {Dynamic: AccountLookup{Location: "Message.TokenAmounts.DestTokenAddress"}}, + {Dynamic: AccountLookup{Location: "Message.Header.DestChainSelector"}}, }, IsSigner: false, IsWritable: false, @@ -120,13 +120,13 @@ func TestConfig() { IsWritable: false, }, // The seed is the receiver address. - Seeds: []Lookup{ - AccountLookup{ + Seeds: []Seed{ + {Dynamic: AccountLookup{ Name: "Receiver", Location: "Message.Receiver", IsSigner: false, IsWritable: false, - }, + }}, }, }, // Account constant @@ -146,8 +146,8 @@ func TestConfig() { IsWritable: false, }, // The seed, once again, is the destination token address. - Seeds: []Lookup{ - AccountLookup{Location: "Message.TokenAmounts.DestTokenAddress"}, + Seeds: []Seed{ + {Dynamic: AccountLookup{Location: "Message.TokenAmounts.DestTokenAddress"}}, }, IsSigner: false, IsWritable: false, @@ -175,9 +175,9 @@ func TestConfig() { IsSigner: false, IsWritable: false, }, - Seeds: []Lookup{ - AccountLookup{Location: "Message.Header.DestChainSelector"}, - AccountLookup{Location: "Message.Header.SourceChainSelector"}, + Seeds: []Seed{ + {Dynamic: AccountLookup{Location: "Message.Header.DestChainSelector"}}, + {Dynamic: AccountLookup{Location: "Message.Header.SourceChainSelector"}}, }, IsSigner: false, IsWritable: false, @@ -191,11 +191,11 @@ func TestConfig() { IsSigner: false, IsWritable: false, }, - Seeds: []Lookup{ - AccountLookup{ + Seeds: []Seed{ + {Dynamic: AccountLookup{ // The seed is the merkle root of the report, as passed into the input params. Location: "args.MerkleRoot", - }, + }}, }, IsSigner: false, IsWritable: false, @@ -211,9 +211,9 @@ func TestConfig() { }, // 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: []Lookup{ - AccountLookup{Location: "Message.Receiver"}, - AccountLookup{Location: "Message.Header.DestChainSelector"}, + Seeds: []Seed{ + {Dynamic: AccountLookup{Location: "Message.Receiver"}}, + {Dynamic: AccountLookup{Location: "Message.Header.DestChainSelector"}}, }, }, // Account constant @@ -284,11 +284,11 @@ func TestConfig() { IsSigner: false, IsWritable: false, }, - Seeds: []Lookup{ - AccountLookup{ + Seeds: []Seed{ + {Dynamic: AccountLookup{ // The seed is the merkle root of the report, as passed into the input params. Location: "args.MerkleRoots", - }, + }}, }, IsSigner: false, IsWritable: false, diff --git a/pkg/solana/chainwriter/chain_writer.go b/pkg/solana/chainwriter/chain_writer.go index e16a55e60..4ed9c8a60 100644 --- a/pkg/solana/chainwriter/chain_writer.go +++ b/pkg/solana/chainwriter/chain_writer.go @@ -7,6 +7,8 @@ import ( "math/big" "github.com/gagliardetto/solana-go" + addresslookuptable "github.com/gagliardetto/solana-go/programs/address-lookup-table" + "github.com/gagliardetto/solana-go/rpc" commoncodec "github.com/smartcontractkit/chainlink-common/pkg/codec" "github.com/smartcontractkit/chainlink-common/pkg/codec/encodings/binary" @@ -80,22 +82,22 @@ func parseIDLCodecs(config ChainWriterConfig) (map[string]types.Codec, error) { 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) + return nil, fmt.Errorf("failed to unmarshal IDL for program: %s, error: %w", program, err) } idlCodec, err := codec.NewIDLInstructionsCodec(idl, binary.LittleEndian()) if err != nil { - return nil, fmt.Errorf("failed to create codec from IDL: %w", err) + return nil, fmt.Errorf("failed to create codec from IDL for program: %s, error: %w", program, 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) + return nil, fmt.Errorf("failed to create input modifications for method %s.%s, error: %w", program, method, err) } // add mods to codec idlCodec, err = codec.NewNamedModifierCodec(idlCodec, method, modConfig) if err != nil { - return nil, fmt.Errorf("failed to create named codec: %w", err) + return nil, fmt.Errorf("failed to create named codec for method %s.%s, error: %w", program, method, err) } } } @@ -250,6 +252,10 @@ func (s *SolanaChainWriterService) SubmitTransaction(ctx context.Context, contra codec := s.codecs[contractName] encodedPayload, err := codec.Encode(ctx, args, method) + + discriminator := GetDiscriminator(methodConfig.ChainSpecificName) + encodedPayload = append(discriminator[:], encodedPayload...) + if err != nil { return errorWithDebugID(fmt.Errorf("error encoding transaction payload: %w", err), debugID) } @@ -302,7 +308,7 @@ func (s *SolanaChainWriterService) SubmitTransaction(ctx context.Context, contra } // Enqueue transaction - if err = s.txm.Enqueue(ctx, accounts[0].PublicKey.String(), tx, &transactionID, blockhash.Value.LastValidBlockHeight); err != nil { + if err = s.txm.Enqueue(ctx, methodConfig.FromAddress, tx, &transactionID, blockhash.Value.LastValidBlockHeight); err != nil { return errorWithDebugID(fmt.Errorf("error enqueuing transaction: %w", err), debugID) } @@ -327,6 +333,95 @@ func (s *SolanaChainWriterService) GetFeeComponents(ctx context.Context) (*types }, nil } +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 { + // Load the lookup table - note: This could be multiple tables if the lookup is a PDALookups that resolves to more + // than one address + lookupTableMap, err := s.loadTable(ctx, args, derivedLookup) + if err != nil { + return nil, nil, fmt.Errorf("error loading derived lookup table: %w", err) + } + + // 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 { + addressses, err := getLookupTableAddresses(ctx, s.reader, staticTable) + if err != nil { + return nil, nil, fmt.Errorf("error fetching static lookup table address: %w", err) + } + staticTableMap[staticTable] = addressses + } + + return derivedTableMap, staticTableMap, nil +} + +func (s *SolanaChainWriterService) loadTable(ctx context.Context, args any, rlt DerivedLookupTable) (map[string]map[string][]*solana.AccountMeta, error) { + // Resolve all addresses specified by the identifier + lookupTableAddresses, err := GetAddresses(ctx, args, []Lookup{rlt.Accounts}, nil, s.reader) + if err != nil { + return nil, fmt.Errorf("error resolving addresses for lookup table: %w", err) + } + + // Nested map in case the lookup table resolves to multiple addresses + resultMap := make(map[string]map[string][]*solana.AccountMeta) + + // Iterate over each address of the lookup table + for _, addressMeta := range lookupTableAddresses { + // Read the full list of addresses from the lookup table + addresses, err := getLookupTableAddresses(ctx, s.reader, addressMeta.PublicKey) + if err != nil { + return nil, fmt.Errorf("error fetching lookup table address: %s, error: %w", addressMeta.PublicKey, err) + } + + // 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][addressMeta.PublicKey.String()] = append(resultMap[rlt.Name][addressMeta.PublicKey.String()], &solana.AccountMeta{ + PublicKey: addr, + IsSigner: addressMeta.IsSigner, + IsWritable: addressMeta.IsWritable, + }) + } + } + + return resultMap, nil +} + +func getLookupTableAddresses(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.CommitmentFinalized, + }) + + if err != nil || accountInfo == nil || accountInfo.Value == nil { + 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, fmt.Errorf("error decoding address lookup table state: %w", err) + } + return alt.Addresses, nil +} + func (s *SolanaChainWriterService) Start(_ context.Context) error { return s.StartOnce(ServiceName, func() error { return nil diff --git a/pkg/solana/chainwriter/chain_writer_test.go b/pkg/solana/chainwriter/chain_writer_test.go index 9798bdb4c..03428d080 100644 --- a/pkg/solana/chainwriter/chain_writer_test.go +++ b/pkg/solana/chainwriter/chain_writer_test.go @@ -3,7 +3,9 @@ package chainwriter_test import ( "bytes" "errors" + "io/ioutil" "math/big" + "os" "reflect" "testing" @@ -25,8 +27,6 @@ import ( txmMocks "github.com/smartcontractkit/chainlink-solana/pkg/solana/txm/mocks" ) -var testContractIDLJson = `{"version":"0.1.0","name":"contractReaderInterface","instructions":[{"name":"initialize","accounts":[{"name":"data","isMut":true,"isSigner":false},{"name":"signer","isMut":true,"isSigner":true},{"name":"systemProgram","isMut":false,"isSigner":false}],"args":[{"name":"testIdx","type":"u64"},{"name":"value","type":"u64"}]},{"name":"initializeLookupTable","accounts":[{"name":"writeDataAccount","isMut":true,"isSigner":false,"docs":["PDA for LookupTableDataAccount, derived from seeds and created by the System Program"]},{"name":"admin","isMut":true,"isSigner":true,"docs":["Admin account that pays for PDA creation and signs the transaction"]},{"name":"systemProgram","isMut":false,"isSigner":false,"docs":["System Program required for PDA creation"]}],"args":[{"name":"lookupTable","type":"publicKey"}]}],"accounts":[{"name":"LookupTableDataAccount","type":{"kind":"struct","fields":[{"name":"version","type":"u8"},{"name":"administrator","type":"publicKey"},{"name":"pendingAdministrator","type":"publicKey"},{"name":"lookupTable","type":"publicKey"}]}},{"name":"DataAccount","type":{"kind":"struct","fields":[{"name":"idx","type":"u64"},{"name":"bump","type":"u8"},{"name":"u64Value","type":"u64"},{"name":"u64Slice","type":{"vec":"u64"}}]}}]}` - func TestChainWriter_GetAddresses(t *testing.T) { ctx := tests.Context(t) @@ -85,9 +85,9 @@ func TestChainWriter_GetAddresses(t *testing.T) { Accounts: chainwriter.PDALookups{ Name: "DataAccountPDA", PublicKey: chainwriter.AccountConstant{Name: "WriteTest", Address: programID.String()}, - Seeds: []chainwriter.Lookup{ + Seeds: []chainwriter.Seed{ // extract seed2 for PDA lookup - chainwriter.AccountLookup{Name: "seed2", Location: "seed2"}, + {Dynamic: chainwriter.AccountLookup{Name: "seed2", Location: "seed2"}}, }, IsSigner: derivedTablePdaLookupMeta.IsSigner, IsWritable: derivedTablePdaLookupMeta.IsWritable, @@ -129,9 +129,9 @@ func TestChainWriter_GetAddresses(t *testing.T) { chainwriter.PDALookups{ Name: "DataAccountPDA", PublicKey: chainwriter.AccountConstant{Name: "WriteTest", Address: solana.SystemProgramID.String()}, - Seeds: []chainwriter.Lookup{ + Seeds: []chainwriter.Seed{ // extract seed1 for PDA lookup - chainwriter.AccountLookup{Name: "seed1", Location: "seed1"}, + {Dynamic: chainwriter.AccountLookup{Name: "seed1", Location: "seed1"}}, }, IsSigner: pdaLookupMeta.IsSigner, IsWritable: pdaLookupMeta.IsWritable, @@ -272,9 +272,9 @@ func TestChainWriter_FilterLookupTableAddresses(t *testing.T) { Accounts: chainwriter.PDALookups{ Name: "DataAccountPDA", PublicKey: chainwriter.AccountConstant{Name: "WriteTest", Address: programID.String()}, - Seeds: []chainwriter.Lookup{ + Seeds: []chainwriter.Seed{ // extract seed1 for PDA lookup - chainwriter.AccountLookup{Name: "seed1", Location: "seed1"}, + {Dynamic: chainwriter.AccountLookup{Name: "seed1", Location: "seed1"}}, }, IsSigner: true, IsWritable: true, @@ -289,9 +289,9 @@ func TestChainWriter_FilterLookupTableAddresses(t *testing.T) { Accounts: chainwriter.PDALookups{ Name: "MiscPDA", PublicKey: chainwriter.AccountConstant{Name: "UnusedAccount", Address: unusedProgramID.String()}, - Seeds: []chainwriter.Lookup{ + Seeds: []chainwriter.Seed{ // extract seed2 for PDA lookup - chainwriter.AccountLookup{Name: "seed2", Location: "seed2"}, + {Dynamic: chainwriter.AccountLookup{Name: "seed2", Location: "seed2"}}, }, IsSigner: true, IsWritable: true, @@ -414,6 +414,16 @@ func TestChainWriter_SubmitTransaction(t *testing.T) { staticLookupKeys := chainwriter.CreateTestPubKeys(t, 2) mockFetchLookupTableAddresses(t, rw, staticLookupTablePubkey, staticLookupKeys) + jsonFile, err := os.Open("testContractIDL.json") + require.NoError(t, err) + + defer jsonFile.Close() + + data, err := ioutil.ReadAll(jsonFile) + require.NoError(t, err) + + testContractIDLJson := string(data) + cwConfig := chainwriter.ChainWriterConfig{ Programs: map[string]chainwriter.ProgramConfig{ "contractReaderInterface": { @@ -428,9 +438,9 @@ func TestChainWriter_SubmitTransaction(t *testing.T) { Accounts: chainwriter.PDALookups{ Name: "DataAccountPDA", PublicKey: chainwriter.AccountConstant{Name: "WriteTest", Address: programID.String()}, - Seeds: []chainwriter.Lookup{ + Seeds: []chainwriter.Seed{ // extract seed2 for PDA lookup - chainwriter.AccountLookup{Name: "seed2", Location: "seed2"}, + {Dynamic: chainwriter.AccountLookup{Name: "seed2", Location: "seed2"}}, }, IsSigner: false, IsWritable: false, @@ -459,9 +469,9 @@ func TestChainWriter_SubmitTransaction(t *testing.T) { chainwriter.PDALookups{ Name: "DataAccountPDA", PublicKey: chainwriter.AccountConstant{Name: "WriteTest", Address: solana.SystemProgramID.String()}, - Seeds: []chainwriter.Lookup{ + Seeds: []chainwriter.Seed{ // extract seed1 for PDA lookup - chainwriter.AccountLookup{Name: "seed1", Location: "seed1"}, + {Dynamic: chainwriter.AccountLookup{Name: "seed1", Location: "seed1"}}, }, IsSigner: false, IsWritable: false, diff --git a/pkg/solana/chainwriter/helpers.go b/pkg/solana/chainwriter/helpers.go index 6f78c7a63..a4b18e4d5 100644 --- a/pkg/solana/chainwriter/helpers.go +++ b/pkg/solana/chainwriter/helpers.go @@ -3,6 +3,7 @@ package chainwriter import ( "context" "crypto/sha256" + "encoding/binary" "errors" "fmt" "reflect" @@ -41,12 +42,15 @@ func GetValuesAtLocation(args any, location string) ([][]byte, error) { if err != nil { return nil, err } - for _, value := range addressList { if byteArray, ok := value.([]byte); ok { vals = append(vals, byteArray) } else if address, ok := value.(solana.PublicKey); ok { vals = append(vals, address.Bytes()) + } else if num, ok := value.(uint64); ok { + buf := make([]byte, 8) + binary.LittleEndian.PutUint64(buf, num) + vals = append(vals, buf) } else { return nil, fmt.Errorf("invalid value format at path: %s", location) } diff --git a/pkg/solana/chainwriter/lookups.go b/pkg/solana/chainwriter/lookups.go index b9d3ca7cd..fb8c9cd98 100644 --- a/pkg/solana/chainwriter/lookups.go +++ b/pkg/solana/chainwriter/lookups.go @@ -7,7 +7,6 @@ import ( 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" "github.com/smartcontractkit/chainlink-solana/pkg/solana/client" @@ -33,6 +32,11 @@ type AccountLookup struct { IsWritable bool } +type Seed struct { + Static []byte // Static seed value + Dynamic Lookup // Dynamic lookup for seed +} + // PDALookups generates Program Derived Addresses (PDA) by combining a derived public key with one or more seeds. type PDALookups struct { Name string @@ -40,7 +44,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 []Seed IsSigner bool IsWritable bool // OPTIONAL: On-chain location and type of desired data from PDA (e.g. a sub-account of the data account) @@ -210,29 +214,35 @@ func getSeedBytes(ctx context.Context, lookup PDALookups, args any, derivedTable var seedBytes [][]byte for _, seed := range lookup.Seeds { - if lookupSeed, ok := seed.(AccountLookup); ok { - // Get value from a location (This doesn't have to be an address, it can be any value) - bytes, err := GetValuesAtLocation(args, lookupSeed.Location) - if err != nil { - return nil, fmt.Errorf("error getting address seed: %w", err) - } - // validate seed length - for _, b := range bytes { - if len(b) > solana.MaxSeedLength { - return nil, fmt.Errorf("seed byte array exceeds maximum length of %d: got %d bytes", solana.MaxSeedLength, len(b)) + if seed.Static != nil { + seedBytes = append(seedBytes, seed.Static) + } + if seed.Dynamic != nil { + dynamicSeed := seed.Dynamic + if lookupSeed, ok := dynamicSeed.(AccountLookup); ok { + // 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, fmt.Errorf("error getting address seed: %w", err) + } + // validate seed length + for _, b := range bytes { + if len(b) > solana.MaxSeedLength { + return nil, fmt.Errorf("seed byte array exceeds maximum length of %d: got %d bytes", solana.MaxSeedLength, len(b)) + } + seedBytes = append(seedBytes, b) + } + } else { + // Get address seeds from the lookup + seedAddresses, err := GetAddresses(ctx, args, []Lookup{dynamicSeed}, derivedTableMap, reader) + if err != nil { + return nil, fmt.Errorf("error getting address seed: %w", err) } - seedBytes = append(seedBytes, b) - } - } else { - // Get address seeds from the lookup - seedAddresses, err := GetAddresses(ctx, args, []Lookup{seed}, derivedTableMap, reader) - if err != nil { - return nil, fmt.Errorf("error getting address seed: %w", err) - } - // 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()) + } } } } @@ -259,96 +269,3 @@ func generatePDAs(publicKeys []*solana.AccountMeta, seeds [][]byte, lookup PDALo } return addresses, nil } - -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 { - // Load the lookup table - note: This could be multiple tables if the lookup is a PDALookups that resovles to more - // than one address - lookupTableMap, _, err := s.LoadTable(ctx, args, derivedLookup, s.reader, derivedTableMap) - if err != nil { - return nil, nil, fmt.Errorf("error loading derived lookup table: %w", err) - } - - // 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 { - addressses, err := getLookupTableAddresses(ctx, s.reader, staticTable) - if err != nil { - return nil, nil, fmt.Errorf("error fetching static lookup table address: %w", err) - } - staticTableMap[staticTable] = 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) (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, reader) - if err != nil { - return nil, nil, fmt.Errorf("error resolving addresses for lookup table: %w", err) - } - - // Nested map in case the lookup table resolves to multiple addresses - resultMap := make(map[string]map[string][]*solana.AccountMeta) - var lookupTableMetas []*solana.AccountMeta - - // Iterate over each address of the lookup table - for _, addressMeta := range lookupTableAddresses { - // Read the full list of addresses from the lookup table - addresses, err := getLookupTableAddresses(ctx, reader, addressMeta.PublicKey) - if err != nil { - return nil, nil, fmt.Errorf("error fetching lookup table address: %s, error: %w", addressMeta.PublicKey, err) - } - - // 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][addressMeta.PublicKey.String()] = append(resultMap[rlt.Name][addressMeta.PublicKey.String()], &solana.AccountMeta{ - PublicKey: addr, - IsSigner: addressMeta.IsSigner, - IsWritable: addressMeta.IsWritable, - }) - } - - // Add the current lookup table address to the list of metas - lookupTableMetas = append(lookupTableMetas, addressMeta) - } - - return resultMap, lookupTableMetas, nil -} - -func getLookupTableAddresses(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.CommitmentFinalized, - }) - - if err != nil || accountInfo == nil || accountInfo.Value == nil { - 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, fmt.Errorf("error decoding address lookup table state: %w", err) - } - return alt.Addresses, nil -} diff --git a/pkg/solana/chainwriter/testContractIDL.json b/pkg/solana/chainwriter/testContractIDL.json new file mode 100644 index 000000000..9631e4acf --- /dev/null +++ b/pkg/solana/chainwriter/testContractIDL.json @@ -0,0 +1 @@ +{"version":"0.1.0","name":"contractReaderInterface","instructions":[{"name":"initialize","accounts":[{"name":"data","isMut":true,"isSigner":false},{"name":"signer","isMut":true,"isSigner":true},{"name":"systemProgram","isMut":false,"isSigner":false}],"args":[{"name":"testIdx","type":"u64"},{"name":"value","type":"u64"}]},{"name":"initializeLookupTable","accounts":[{"name":"writeDataAccount","isMut":true,"isSigner":false,"docs":["PDA for LookupTableDataAccount, derived from seeds and created by the System Program"]},{"name":"admin","isMut":true,"isSigner":true,"docs":["Admin account that pays for PDA creation and signs the transaction"]},{"name":"systemProgram","isMut":false,"isSigner":false,"docs":["System Program required for PDA creation"]}],"args":[{"name":"lookupTable","type":"publicKey"}]}],"accounts":[{"name":"LookupTableDataAccount","type":{"kind":"struct","fields":[{"name":"version","type":"u8"},{"name":"administrator","type":"publicKey"},{"name":"pendingAdministrator","type":"publicKey"},{"name":"lookupTable","type":"publicKey"}]}},{"name":"DataAccount","type":{"kind":"struct","fields":[{"name":"idx","type":"u64"},{"name":"bump","type":"u8"},{"name":"u64Value","type":"u64"},{"name":"u64Slice","type":{"vec":"u64"}}]}}]} \ No newline at end of file