From 95b3038d87637cff5a00f09de6d4dc3666b63841 Mon Sep 17 00:00:00 2001 From: Silas Lenihan Date: Fri, 25 Oct 2024 15:09:13 -0400 Subject: [PATCH 01/25] Added Address searcher for decoded data --- .../actions/projectserum_version/action.yml | 1 + pkg/solana/chainwriter/chain_writer.go | 1 + pkg/solana/chainwriter/helpers_test.go | 69 +++++++++++++++++++ 3 files changed, 71 insertions(+) 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 e02148d89..0f088ca57 100644 --- a/pkg/solana/chainwriter/chain_writer.go +++ b/pkg/solana/chainwriter/chain_writer.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" "math/big" + "reflect" "github.com/gagliardetto/solana-go" addresslookuptable "github.com/gagliardetto/solana-go/programs/address-lookup-table" 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 320b3d6595656356f5a6646e2f154fc082fe505f Mon Sep 17 00:00:00 2001 From: Silas Lenihan Date: Mon, 28 Oct 2024 14:11:07 -0400 Subject: [PATCH 02/25] Introduced new Solana config --- pkg/solana/chainwriter/helpers_test.go | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) 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 957bc884dff8c7bf83b882858a6dd7927d5f1ed6 Mon Sep 17 00:00:00 2001 From: Silas Lenihan Date: Tue, 29 Oct 2024 15:09:05 -0400 Subject: [PATCH 03/25] Completed iteration of ChainWriter config --- pkg/solana/chainwriter/helpers.go | 39 ++++++++++++++ pkg/solana/chainwriter/helpers_test.go | 74 -------------------------- 2 files changed, 39 insertions(+), 74 deletions(-) delete mode 100644 pkg/solana/chainwriter/helpers_test.go diff --git a/pkg/solana/chainwriter/helpers.go b/pkg/solana/chainwriter/helpers.go index a4b18e4d5..77d793d9a 100644 --- a/pkg/solana/chainwriter/helpers.go +++ b/pkg/solana/chainwriter/helpers.go @@ -81,6 +81,45 @@ func errorWithDebugID(err error, debugID string) error { return fmt.Errorf("Debug ID: %s: Error: %s", debugID, err) } +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 fe9946f82b32882ccb7204483a57f7bd95c852f5 Mon Sep 17 00:00:00 2001 From: Silas Lenihan Date: Wed, 30 Oct 2024 11:12:57 -0400 Subject: [PATCH 04/25] Created sample configuration for execute method --- pkg/solana/chainwriter/helpers_test.go | 109 +++++++++++++++++++++++++ 1 file changed, 109 insertions(+) create mode 100644 pkg/solana/chainwriter/helpers_test.go 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 b4a11d290b1842fb06509b1d973bcc7807c43c08 Mon Sep 17 00:00:00 2001 From: Silas Lenihan Date: Wed, 30 Oct 2024 14:31:44 -0400 Subject: [PATCH 05/25] Cleaned up exec config and added comments --- pkg/solana/chainwriter/chain_writer_test.go | 1 + 1 file changed, 1 insertion(+) diff --git a/pkg/solana/chainwriter/chain_writer_test.go b/pkg/solana/chainwriter/chain_writer_test.go index 947674a2f..6195d298e 100644 --- a/pkg/solana/chainwriter/chain_writer_test.go +++ b/pkg/solana/chainwriter/chain_writer_test.go @@ -305,6 +305,7 @@ func TestChainWriter_FilterLookupTableAddresses(t *testing.T) { Location: "LookupTable", }, }, + IDL: "ccip-router", }, }, StaticLookupTables: []solana.PublicKey{staticLookupTablePubkey1, staticLookupTablePubkey2}, From deaba614db6ba8981af00a0528d9e107674eac0f Mon Sep 17 00:00:00 2001 From: Silas Lenihan Date: Mon, 4 Nov 2024 13:03:37 -0500 Subject: [PATCH 06/25] Added commit report config example --- pkg/solana/chainwriter/chain_writer.go | 1 + 1 file changed, 1 insertion(+) diff --git a/pkg/solana/chainwriter/chain_writer.go b/pkg/solana/chainwriter/chain_writer.go index 0f088ca57..c95fc81b2 100644 --- a/pkg/solana/chainwriter/chain_writer.go +++ b/pkg/solana/chainwriter/chain_writer.go @@ -309,6 +309,7 @@ func (s *SolanaChainWriterService) SubmitTransaction(ctx context.Context, contra solana.TransactionPayer(feePayer), solana.TransactionAddressTables(filteredLookupTableMap), ) + if err != nil { return errorWithDebugID(fmt.Errorf("error constructing transaction: %w", err), debugID) } From fb906a2e0450576dabaa3772a551868b1b9c3890 Mon Sep 17 00:00:00 2001 From: Silas Lenihan Date: Fri, 15 Nov 2024 14:08:56 -0500 Subject: [PATCH 07/25] Updated ChainWriter implementation to reflect new design changes --- pkg/solana/chainwriter/chain_writer.go | 3 --- pkg/solana/chainwriter/helpers.go | 8 ++++---- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/pkg/solana/chainwriter/chain_writer.go b/pkg/solana/chainwriter/chain_writer.go index c95fc81b2..df58464f9 100644 --- a/pkg/solana/chainwriter/chain_writer.go +++ b/pkg/solana/chainwriter/chain_writer.go @@ -2,10 +2,8 @@ package chainwriter import ( "context" - "encoding/json" "fmt" "math/big" - "reflect" "github.com/gagliardetto/solana-go" addresslookuptable "github.com/gagliardetto/solana-go/programs/address-lookup-table" @@ -309,7 +307,6 @@ func (s *SolanaChainWriterService) SubmitTransaction(ctx context.Context, contra solana.TransactionPayer(feePayer), solana.TransactionAddressTables(filteredLookupTableMap), ) - if err != nil { return errorWithDebugID(fmt.Errorf("error constructing transaction: %w", err), debugID) } diff --git a/pkg/solana/chainwriter/helpers.go b/pkg/solana/chainwriter/helpers.go index 77d793d9a..fe4771c5a 100644 --- a/pkg/solana/chainwriter/helpers.go +++ b/pkg/solana/chainwriter/helpers.go @@ -81,8 +81,8 @@ func errorWithDebugID(err error, debugID string) error { return fmt.Errorf("Debug ID: %s: Error: %s", debugID, err) } -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 } @@ -93,10 +93,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 1b873b84e9a38315294419deb6e15a3ebadc26c2 Mon Sep 17 00:00:00 2001 From: Silas Lenihan Date: Mon, 18 Nov 2024 11:16:04 -0500 Subject: [PATCH 08/25] Added codec implementation --- pkg/solana/chainwriter/chain_writer.go | 1 + 1 file changed, 1 insertion(+) diff --git a/pkg/solana/chainwriter/chain_writer.go b/pkg/solana/chainwriter/chain_writer.go index df58464f9..e02148d89 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" From b9ab05cd81933d4c6f91ebd4a51414f18a58300a Mon Sep 17 00:00:00 2001 From: Silas Lenihan Date: Tue, 19 Nov 2024 14:57:20 -0500 Subject: [PATCH 09/25] updated CCIP example --- .github/actions/projectserum_version/action.yml | 1 - 1 file changed, 1 deletion(-) 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 From fe4d467f2365a4c7dd08cb7cc0800a45d3d4deaa Mon Sep 17 00:00:00 2001 From: Silas Lenihan Date: Thu, 21 Nov 2024 09:48:39 -0500 Subject: [PATCH 10/25] unit tests for lookups --- go.mod | 1 + gotest.log | 72 +++++ pkg/solana/chainwriter/lookups_test.go | 374 +++++++++++++++++++++++++ 3 files changed, 447 insertions(+) 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/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 4db5108b0ad7b703c6f213a040db35ae85266492 Mon Sep 17 00:00:00 2001 From: Silas Lenihan Date: Thu, 21 Nov 2024 16:45:54 -0500 Subject: [PATCH 11/25] Added utils to their own package --- gotest.log | 72 ---------- pkg/solana/chainwriter/lookups_test.go | 185 ++++++------------------- pkg/solana/utils/utils_test.go | 1 + 3 files changed, 44 insertions(+), 214 deletions(-) delete mode 100644 gotest.log 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/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/utils_test.go b/pkg/solana/utils/utils_test.go index 0f41f80c9..da6166ff0 100644 --- a/pkg/solana/utils/utils_test.go +++ b/pkg/solana/utils/utils_test.go @@ -3,6 +3,7 @@ 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" From b45d3961e805b22e127df911862f3f3fa28b270f Mon Sep 17 00:00:00 2001 From: Silas Lenihan Date: Mon, 25 Nov 2024 11:49:10 -0500 Subject: [PATCH 12/25] Updated lookup tests and helpers --- pkg/solana/chainwriter/helpers.go | 1 + pkg/solana/chainwriter/lookups_test.go | 128 +++++++++++++++++++++---- 2 files changed, 112 insertions(+), 17 deletions(-) diff --git a/pkg/solana/chainwriter/helpers.go b/pkg/solana/chainwriter/helpers.go index fe4771c5a..d3a506efd 100644 --- a/pkg/solana/chainwriter/helpers.go +++ b/pkg/solana/chainwriter/helpers.go @@ -133,6 +133,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: 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 f5a31748f888eb8da6969a733aa5f6532fb8aede Mon Sep 17 00:00:00 2001 From: Silas Lenihan Date: Mon, 25 Nov 2024 11:51:15 -0500 Subject: [PATCH 13/25] 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 c53ce3a438042601a35cbb43b30c9acf0ddcd098 Mon Sep 17 00:00:00 2001 From: Silas Lenihan Date: Mon, 25 Nov 2024 12:06:25 -0500 Subject: [PATCH 14/25] refactored ccip example --- pkg/solana/chainwriter/chain_writer_test.go | 705 -------------------- 1 file changed, 705 deletions(-) delete mode 100644 pkg/solana/chainwriter/chain_writer_test.go diff --git a/pkg/solana/chainwriter/chain_writer_test.go b/pkg/solana/chainwriter/chain_writer_test.go deleted file mode 100644 index 6195d298e..000000000 --- a/pkg/solana/chainwriter/chain_writer_test.go +++ /dev/null @@ -1,705 +0,0 @@ -package chainwriter_test - -import ( - "bytes" - "errors" - "math/big" - "os" - "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/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" -) - -type Arguments struct { - LookupTable solana.PublicKey - Seed1 []byte - Seed2 []byte -} - -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(testutils.NewNullLogger(), 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.Seed{ - // extract seed2 for PDA lookup - {Dynamic: chainwriter.AccountLookup{Name: "Seed2", Location: "Seed2"}}, - }, - IsSigner: derivedTablePdaLookupMeta.IsSigner, - IsWritable: derivedTablePdaLookupMeta.IsWritable, - InternalField: chainwriter.InternalField{ - Type: reflect.TypeOf(chainwriter.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 := Arguments{ - LookupTable: accountLookupMeta.PublicKey, - 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: "LookupTable", - IsSigner: accountLookupMeta.IsSigner, - IsWritable: accountLookupMeta.IsWritable, - }, - chainwriter.PDALookups{ - Name: "DataAccountPDA", - PublicKey: chainwriter.AccountConstant{Name: "WriteTest", Address: solana.SystemProgramID.String()}, - Seeds: []chainwriter.Seed{ - // extract seed1 for PDA lookup - {Dynamic: 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 := Arguments{ - 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 := Arguments{ - 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(testutils.NewNullLogger(), 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.Seed{ - // extract seed1 for PDA lookup - {Dynamic: chainwriter.AccountLookup{Name: "Seed1", Location: "Seed1"}}, - }, - IsSigner: true, - IsWritable: true, - InternalField: chainwriter.InternalField{ - Type: reflect.TypeOf(chainwriter.DataAccount{}), - Location: "LookupTable", - }, - }, - }, - { - Name: "MiscDerivedTable", - Accounts: chainwriter.PDALookups{ - Name: "MiscPDA", - PublicKey: chainwriter.AccountConstant{Name: "UnusedAccount", Address: unusedProgramID.String()}, - Seeds: []chainwriter.Seed{ - // extract seed2 for PDA lookup - {Dynamic: chainwriter.AccountLookup{Name: "Seed2", Location: "Seed2"}}, - }, - IsSigner: true, - IsWritable: true, - InternalField: chainwriter.InternalField{ - Type: reflect.TypeOf(chainwriter.DataAccount{}), - Location: "LookupTable", - }, - }, - IDL: "ccip-router", - }, - }, - StaticLookupTables: []solana.PublicKey{staticLookupTablePubkey1, staticLookupTablePubkey2}, - } - - args := Arguments{ - 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 := solana.MustPublicKeyFromBase58("6AfuXF6HapDUhQfE4nQG9C1SGtA1YjP3icaJyRfU4RyE") - 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) - - data, err := os.ReadFile("testContractIDL.json") - require.NoError(t, err) - - testContractIDLJson := string(data) - - cwConfig := chainwriter.ChainWriterConfig{ - Programs: map[string]chainwriter.ProgramConfig{ - "contract_reader_interface": { - Methods: map[string]chainwriter.MethodConfig{ - "initializeLookupTable": { - FromAddress: admin.String(), - ChainSpecificName: "initializeLookupTable", - LookupTables: chainwriter.LookupTables{ - DerivedLookupTables: []chainwriter.DerivedLookupTable{ - { - Name: "DerivedTable", - Accounts: chainwriter.PDALookups{ - Name: "DataAccountPDA", - PublicKey: chainwriter.AccountConstant{Name: "WriteTest", Address: programID.String()}, - Seeds: []chainwriter.Seed{ - // extract seed2 for PDA lookup - {Dynamic: chainwriter.AccountLookup{Name: "Seed2", Location: "Seed2"}}, - }, - IsSigner: false, - IsWritable: false, - InternalField: chainwriter.InternalField{ - Type: reflect.TypeOf(chainwriter.DataAccount{}), - Location: "LookupTable", - }, - }, - }, - }, - StaticLookupTables: []solana.PublicKey{staticLookupTablePubkey}, - }, - Accounts: []chainwriter.Lookup{ - chainwriter.AccountConstant{ - Name: "Constant", - Address: account1.String(), - IsSigner: false, - IsWritable: false, - }, - chainwriter.AccountLookup{ - Name: "LookupTable", - Location: "LookupTable", - IsSigner: false, - IsWritable: false, - }, - chainwriter.PDALookups{ - Name: "DataAccountPDA", - PublicKey: chainwriter.AccountConstant{Name: "WriteTest", Address: solana.SystemProgramID.String()}, - Seeds: []chainwriter.Seed{ - // extract seed1 for PDA lookup - {Dynamic: 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: testContractIDLJson, - }, - }, - } - - // initialize chain writer - 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{ - "invalid_program": { - Methods: map[string]chainwriter.MethodConfig{ - "invalid": { - ChainSpecificName: "invalid", - }, - }, - IDL: "", - }, - }, - } - - _, 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() - type InvalidArgs struct{} - args := InvalidArgs{} - submitErr := cw.SubmitTransaction(ctx, "contract_reader_interface", "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 := Arguments{} - 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 := Arguments{} - submitErr := cw.SubmitTransaction(ctx, "contract_reader_interface", "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() - - 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, 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 - }), &txID, mock.Anything).Return(nil).Once() - - args := Arguments{ - LookupTable: account2, - Seed1: seed1, - Seed2: seed2, - } - - submitErr := cw.SubmitTransaction(ctx, "contract_reader_interface", "initializeLookupTable", 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(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) { - 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(testutils.NewNullLogger(), 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(testutils.NewNullLogger(), 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 := chainwriter.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) -} From 5a76e2c50e5e15334eef81da17b38818693e4d1f Mon Sep 17 00:00:00 2001 From: Silas Lenihan Date: Tue, 26 Nov 2024 14:49:08 -0500 Subject: [PATCH 15/25] Completed chained lookup integration test --- contracts/Anchor.toml | 3 +- contracts/Cargo.lock | 7 + .../localnet/write_test-keypair.json | 1 + contracts/pnpm-lock.yaml | 5 +- 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/helpers.go | 1 - pkg/solana/chainwriter/lookups_test.go | 244 ++++++++++++++---- pkg/solana/utils/utils.go | 9 + 10 files changed, 293 insertions(+), 50 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 5a742745d..eaaccf726 100644 --- a/contracts/pnpm-lock.yaml +++ b/contracts/pnpm-lock.yaml @@ -653,6 +653,7 @@ packages: optional: true dependencies: ms: 2.1.2 + optionalDependencies: supports-color: 8.1.1 dev: false @@ -959,10 +960,10 @@ packages: 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 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/helpers.go b/pkg/solana/chainwriter/helpers.go index d3a506efd..fe4771c5a 100644 --- a/pkg/solana/chainwriter/helpers.go +++ b/pkg/solana/chainwriter/helpers.go @@ -133,7 +133,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_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/utils/utils.go b/pkg/solana/utils/utils.go index 764c236de..deb747158 100644 --- a/pkg/solana/utils/utils.go +++ b/pkg/solana/utils/utils.go @@ -31,6 +31,15 @@ var ( PathToAnchorConfig = filepath.Join(ProjectRoot, "contracts", "Anchor.toml") ) +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 From 5b17978184666f4a71cc81fd63b0405f65a2584c 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 16/25] 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 --- .../localnet/write_test-keypair.json | 7 +- contracts/programs/write_test/src/lib.rs | 3 +- go.mod | 1 - pkg/solana/chainwriter/chain_writer_test.go | 690 ++++++++++++++++++ pkg/solana/chainwriter/helpers.go | 6 +- pkg/solana/chainwriter/lookups.go | 1 + pkg/solana/chainwriter/lookups_test.go | 140 +--- pkg/solana/utils/utils_test.go | 1 - 8 files changed, 734 insertions(+), 115 deletions(-) create mode 100644 pkg/solana/chainwriter/chain_writer_test.go 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/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 fe4771c5a..2285cd699 100644 --- a/pkg/solana/chainwriter/helpers.go +++ b/pkg/solana/chainwriter/helpers.go @@ -103,11 +103,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 diff --git a/pkg/solana/chainwriter/lookups.go b/pkg/solana/chainwriter/lookups.go index f875ade3a..1d885adf3 100644 --- a/pkg/solana/chainwriter/lookups.go +++ b/pkg/solana/chainwriter/lookups.go @@ -163,6 +163,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) 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/utils/utils_test.go b/pkg/solana/utils/utils_test.go index da6166ff0..0f41f80c9 100644 --- a/pkg/solana/utils/utils_test.go +++ b/pkg/solana/utils/utils_test.go @@ -3,7 +3,6 @@ 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" From f6aa6d9c9ceaf5d26802788ecc5b97bfbf4d4c60 Mon Sep 17 00:00:00 2001 From: Silas Lenihan Date: Wed, 11 Dec 2024 16:58:25 -0500 Subject: [PATCH 17/25] merged with develop --- gotest.log | 92 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 92 insertions(+) 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: + From 0723ba699633e769206d71aea32fc0211737bc0a Mon Sep 17 00:00:00 2001 From: Silas Lenihan Date: Thu, 12 Dec 2024 16:24:27 -0500 Subject: [PATCH 18/25] Removed write_test and moved logic to contract_reader_interface test program --- contracts/Anchor.toml | 3 +- contracts/Cargo.lock | 7 - contracts/pnpm-lock.yaml | 5 +- 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 ---- pkg/solana/chainwriter/chain_writer_test.go | 47 +- pkg/solana/chainwriter/lookups.go | 1 - pkg/solana/chainwriter/lookups_test.go | 447 -------------------- 10 files changed, 27 insertions(+), 647 deletions(-) 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 delete mode 100644 pkg/solana/chainwriter/lookups_test.go 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/pnpm-lock.yaml b/contracts/pnpm-lock.yaml index eaaccf726..5a742745d 100644 --- a/contracts/pnpm-lock.yaml +++ b/contracts/pnpm-lock.yaml @@ -653,7 +653,6 @@ packages: optional: true dependencies: ms: 2.1.2 - optionalDependencies: supports-color: 8.1.1 dev: false @@ -960,10 +959,10 @@ packages: 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 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/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/lookups.go b/pkg/solana/chainwriter/lookups.go index 1d885adf3..f875ade3a 100644 --- a/pkg/solana/chainwriter/lookups.go +++ b/pkg/solana/chainwriter/lookups.go @@ -163,7 +163,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/chainwriter/lookups_test.go b/pkg/solana/chainwriter/lookups_test.go deleted file mode 100644 index 53972feac..000000000 --- a/pkg/solana/chainwriter/lookups_test.go +++ /dev/null @@ -1,447 +0,0 @@ -package chainwriter_test - -import ( - "context" - "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" -) - -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) - expectedMeta := []*solana.AccountMeta{ - { - PublicKey: expectedAddr, - IsSigner: true, - IsWritable: true, - }, - } - constantConfig := chainwriter.AccountConstant{ - Name: "TestAccount", - Address: expectedAddr.String(), - IsSigner: true, - IsWritable: true, - } - 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 := chainwriter.GetRandomPubKey(t) - testArgs := TestArgs{ - Inner: []InnerArgs{ - {Address: expectedAddr.Bytes()}, - }, - } - expectedMeta := []*solana.AccountMeta{ - { - PublicKey: expectedAddr, - IsSigner: true, - IsWritable: true, - }, - } - - lookupConfig := chainwriter.AccountLookup{ - Name: "TestAccount", - Location: "Inner.Address", - IsSigner: true, - IsWritable: true, - } - 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 := chainwriter.GetRandomPubKey(t) - expectedAddr2 := chainwriter.GetRandomPubKey(t) - - testArgs := TestArgs{ - Inner: []InnerArgs{ - {Address: expectedAddr1.Bytes()}, - {Address: expectedAddr2.Bytes()}, - }, - } - expectedMeta := []*solana.AccountMeta{ - { - PublicKey: expectedAddr1, - IsSigner: true, - IsWritable: true, - }, - { - PublicKey: expectedAddr2, - IsSigner: true, - IsWritable: true, - }, - } - - lookupConfig := chainwriter.AccountLookup{ - Name: "TestAccount", - Location: "Inner.Address", - IsSigner: true, - IsWritable: true, - } - result, err := lookupConfig.Resolve(ctx, testArgs, nil, 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 := chainwriter.GetRandomPubKey(t) - - testArgs := TestArgs{ - Inner: []InnerArgs{ - {Address: expectedAddr.Bytes()}, - }, - } - lookupConfig := chainwriter.AccountLookup{ - Name: "InvalidAccount", - Location: "Invalid.Directory", - IsSigner: true, - IsWritable: true, - } - _, err := lookupConfig.Resolve(ctx, testArgs, nil, nil) - require.Error(t, err) - }) -} - -func TestPDALookups(t *testing.T) { - programID := solana.SystemProgramID - - t.Run("PDALookup resolves valid PDA with constant address seeds", func(t *testing.T) { - seed := chainwriter.GetRandomPubKey(t) - - 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, 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, nil) - require.NoError(t, err) - require.Equal(t, expectedMeta, result) - }) - - t.Run("PDALookup fails with missing seeds", func(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"}, - }, - 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 := chainwriter.GetRandomPubKey(t) - seed2 := chainwriter.GetRandomPubKey(t) - - 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, nil) - require.NoError(t, err) - require.Equal(t, expectedMeta, result) - }) -} - -func TestLookupTables(t *testing.T) { - ctx := tests.Context(t) - - sender, err := solana.NewRandomPrivateKey() - require.NoError(t, err) - - 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) - lggr := logger.Test(t) - - txm := txm.NewTxm("localnet", loader, nil, cfg, mkey, lggr) - - cw, err := chainwriter.NewSolanaChainWriterService(solanaClient, txm, nil, chainwriter.ChainWriterConfig{}) - - t.Run("StaticLookup table resolves properly", func(t *testing.T) { - pubKeys := chainwriter.CreateTestPubKeys(t, 8) - table := chainwriter.CreateTestLookupTable(ctx, t, rpcClient, sender, pubKeys) - lookupConfig := chainwriter.LookupTables{ - DerivedLookupTables: nil, - StaticLookupTables: []string{table.String()}, - } - _, 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 := chainwriter.CreateTestPubKeys(t, 8) - table := chainwriter.CreateTestLookupTable(ctx, t, rpcClient, 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, _, resolveErr := cw.ResolveLookupTables(ctx, nil, lookupConfig) - require.NoError(t, resolveErr) - - 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 fails with invalid address", func(t *testing.T) { - invalidTable := chainwriter.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 := chainwriter.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 := chainwriter.CreateTestPubKeys(t, 8) - table := chainwriter.CreateTestLookupTable(ctx, t, rpcClient, 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 := cw.ResolveLookupTables(ctx, testArgs, lookupConfig) - 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 PDALookup address", func(t *testing.T) { - // Deployed write_test contract - programID := solana.MustPublicKeyFromBase58("39vbQVpEMtZtg3e6ZSE7nBSzmNZptmW45WnLkbqEe4TU") - - lookupKeys := chainwriter.CreateTestPubKeys(t, 5) - lookupTable := chainwriter.CreateTestLookupTable(ctx, t, rpcClient, sender, lookupKeys) - - chainwriter.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) - } - }) -} From f9a3e7e4838b1810cbb6f975620e9620486ec9a5 Mon Sep 17 00:00:00 2001 From: Silas Lenihan Date: Fri, 13 Dec 2024 14:59:09 -0500 Subject: [PATCH 19/25] addressed feedback comments --- .../localnet/write_test-keypair.json | 6 ------ pkg/solana/chainwriter/chain_writer_test.go | 17 ++++++++-------- pkg/solana/chainwriter/helpers.go | 20 ------------------- pkg/solana/chainwriter/lookups.go | 1 + 4 files changed, 9 insertions(+), 35 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/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 2285cd699..b2c008011 100644 --- a/pkg/solana/chainwriter/helpers.go +++ b/pkg/solana/chainwriter/helpers.go @@ -93,26 +93,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 f875ade3a..58e78f116 100644 --- a/pkg/solana/chainwriter/lookups.go +++ b/pkg/solana/chainwriter/lookups.go @@ -213,6 +213,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 seed.Static != nil { From fea14871820281d8d27c9fa591baa8cc98f6d86f Mon Sep 17 00:00:00 2001 From: Silas Lenihan Date: Thu, 19 Dec 2024 13:15:15 -0500 Subject: [PATCH 20/25] solved conflicts between TXM changes and unit test changes --- pkg/solana/chainwriter/chain_writer_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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(), From 8ddccaf7e4ec2beb10a914e4acc30466fcac81f5 Mon Sep 17 00:00:00 2001 From: Silas Lenihan Date: Fri, 20 Dec 2024 11:42:21 -0500 Subject: [PATCH 21/25] updated comments and slight tweaks --- pkg/solana/chainwriter/chain_writer_test.go | 22 +++++++++++---------- pkg/solana/chainwriter/lookups.go | 1 - 2 files changed, 12 insertions(+), 11 deletions(-) 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/lookups.go b/pkg/solana/chainwriter/lookups.go index 58e78f116..f875ade3a 100644 --- a/pkg/solana/chainwriter/lookups.go +++ b/pkg/solana/chainwriter/lookups.go @@ -213,7 +213,6 @@ 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 seed.Static != nil { From cf148a9fe4cef31e8cffabb3d205fe581518dd84 Mon Sep 17 00:00:00 2001 From: Silas Lenihan Date: Fri, 27 Dec 2024 14:38:06 -0500 Subject: [PATCH 22/25] Updated PDALookups Seeds field and fixed default accounts --- pkg/solana/chainwriter/chain_writer_test.go | 38 +++++++++++++-------- 1 file changed, 24 insertions(+), 14 deletions(-) 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, From bfa1f8d9d2f81673c0a5245bed91a66316fe4744 Mon Sep 17 00:00:00 2001 From: Silas Lenihan Date: Mon, 30 Dec 2024 12:13:20 -0500 Subject: [PATCH 23/25] Updated codec usage --- pkg/solana/chainwriter/chain_writer_test.go | 80 +++++++++++---------- 1 file changed, 42 insertions(+), 38 deletions(-) diff --git a/pkg/solana/chainwriter/chain_writer_test.go b/pkg/solana/chainwriter/chain_writer_test.go index 03428d080..cd215971e 100644 --- a/pkg/solana/chainwriter/chain_writer_test.go +++ b/pkg/solana/chainwriter/chain_writer_test.go @@ -3,7 +3,7 @@ package chainwriter_test import ( "bytes" "errors" - "io/ioutil" + "fmt" "math/big" "os" "reflect" @@ -27,6 +27,12 @@ import ( txmMocks "github.com/smartcontractkit/chainlink-solana/pkg/solana/txm/mocks" ) +type Arguments struct { + LookupTable solana.PublicKey + Seed1 []byte + Seed2 []byte +} + func TestChainWriter_GetAddresses(t *testing.T) { ctx := tests.Context(t) @@ -87,7 +93,7 @@ func TestChainWriter_GetAddresses(t *testing.T) { PublicKey: chainwriter.AccountConstant{Name: "WriteTest", Address: programID.String()}, Seeds: []chainwriter.Seed{ // extract seed2 for PDA lookup - {Dynamic: chainwriter.AccountLookup{Name: "seed2", Location: "seed2"}}, + {Dynamic: chainwriter.AccountLookup{Name: "Seed2", Location: "Seed2"}}, }, IsSigner: derivedTablePdaLookupMeta.IsSigner, IsWritable: derivedTablePdaLookupMeta.IsWritable, @@ -107,10 +113,10 @@ func TestChainWriter_GetAddresses(t *testing.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, + args := Arguments{ + LookupTable: accountLookupMeta.PublicKey, + Seed1: seed1, + Seed2: seed2, } accountLookupConfig := []chainwriter.Lookup{ @@ -122,7 +128,7 @@ func TestChainWriter_GetAddresses(t *testing.T) { }, chainwriter.AccountLookup{ Name: "LookupTable", - Location: "lookup_table", + Location: "LookupTable", IsSigner: accountLookupMeta.IsSigner, IsWritable: accountLookupMeta.IsWritable, }, @@ -131,7 +137,7 @@ func TestChainWriter_GetAddresses(t *testing.T) { PublicKey: chainwriter.AccountConstant{Name: "WriteTest", Address: solana.SystemProgramID.String()}, Seeds: []chainwriter.Seed{ // extract seed1 for PDA lookup - {Dynamic: chainwriter.AccountLookup{Name: "seed1", Location: "seed1"}}, + {Dynamic: chainwriter.AccountLookup{Name: "Seed1", Location: "Seed1"}}, }, IsSigner: pdaLookupMeta.IsSigner, IsWritable: pdaLookupMeta.IsWritable, @@ -177,8 +183,8 @@ func TestChainWriter_GetAddresses(t *testing.T) { }) t.Run("resolve addresses for multiple indices from derived lookup table", func(t *testing.T) { - args := map[string]interface{}{ - "seed2": seed2, + args := Arguments{ + Seed2: seed2, } accountLookupConfig := []chainwriter.Lookup{ @@ -202,8 +208,8 @@ func TestChainWriter_GetAddresses(t *testing.T) { }) t.Run("resolve all addresses from derived lookup table if indices not specified", func(t *testing.T) { - args := map[string]interface{}{ - "seed2": seed2, + args := Arguments{ + Seed2: seed2, } accountLookupConfig := []chainwriter.Lookup{ @@ -274,7 +280,7 @@ func TestChainWriter_FilterLookupTableAddresses(t *testing.T) { PublicKey: chainwriter.AccountConstant{Name: "WriteTest", Address: programID.String()}, Seeds: []chainwriter.Seed{ // extract seed1 for PDA lookup - {Dynamic: chainwriter.AccountLookup{Name: "seed1", Location: "seed1"}}, + {Dynamic: chainwriter.AccountLookup{Name: "Seed1", Location: "Seed1"}}, }, IsSigner: true, IsWritable: true, @@ -291,7 +297,7 @@ func TestChainWriter_FilterLookupTableAddresses(t *testing.T) { PublicKey: chainwriter.AccountConstant{Name: "UnusedAccount", Address: unusedProgramID.String()}, Seeds: []chainwriter.Seed{ // extract seed2 for PDA lookup - {Dynamic: chainwriter.AccountLookup{Name: "seed2", Location: "seed2"}}, + {Dynamic: chainwriter.AccountLookup{Name: "Seed2", Location: "Seed2"}}, }, IsSigner: true, IsWritable: true, @@ -305,9 +311,9 @@ func TestChainWriter_FilterLookupTableAddresses(t *testing.T) { StaticLookupTables: []solana.PublicKey{staticLookupTablePubkey1, staticLookupTablePubkey2}, } - args := map[string]interface{}{ - "seed1": seed1, - "seed2": seed2, + args := Arguments{ + Seed1: seed1, + Seed2: seed2, } t.Run("returns filtered map with only relevant addresses required by account lookup config", func(t *testing.T) { @@ -403,6 +409,7 @@ func TestChainWriter_SubmitTransaction(t *testing.T) { seed2 := []byte("seed2") programID := solana.MustPublicKeyFromBase58("6AfuXF6HapDUhQfE4nQG9C1SGtA1YjP3icaJyRfU4RyE") derivedTablePda := mustFindPdaProgramAddress(t, [][]byte{seed2}, programID) + fmt.Println("pda:", derivedTablePda) // mock data account response from program derivedLookupTablePubkey := mockDataAccountLookupTable(t, rw, derivedTablePda) // mock fetch lookup table addresses call @@ -414,19 +421,14 @@ 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) + data, err := os.ReadFile("testContractIDL.json") require.NoError(t, err) testContractIDLJson := string(data) cwConfig := chainwriter.ChainWriterConfig{ Programs: map[string]chainwriter.ProgramConfig{ - "contractReaderInterface": { + "contract_reader_interface": { Methods: map[string]chainwriter.MethodConfig{ "initializeLookupTable": { FromAddress: admin.String(), @@ -440,7 +442,7 @@ func TestChainWriter_SubmitTransaction(t *testing.T) { PublicKey: chainwriter.AccountConstant{Name: "WriteTest", Address: programID.String()}, Seeds: []chainwriter.Seed{ // extract seed2 for PDA lookup - {Dynamic: chainwriter.AccountLookup{Name: "seed2", Location: "seed2"}}, + {Dynamic: chainwriter.AccountLookup{Name: "Seed2", Location: "Seed2"}}, }, IsSigner: false, IsWritable: false, @@ -462,7 +464,7 @@ func TestChainWriter_SubmitTransaction(t *testing.T) { }, chainwriter.AccountLookup{ Name: "LookupTable", - Location: "lookup_table", + Location: "LookupTable", IsSigner: false, IsWritable: false, }, @@ -471,7 +473,7 @@ func TestChainWriter_SubmitTransaction(t *testing.T) { PublicKey: chainwriter.AccountConstant{Name: "WriteTest", Address: solana.SystemProgramID.String()}, Seeds: []chainwriter.Seed{ // extract seed1 for PDA lookup - {Dynamic: chainwriter.AccountLookup{Name: "seed1", Location: "seed1"}}, + {Dynamic: chainwriter.AccountLookup{Name: "Seed1", Location: "Seed1"}}, }, IsSigner: false, IsWritable: false, @@ -514,22 +516,24 @@ 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, "contractReaderInterface", "initializeLookupTable", args, txID, programID.String(), nil, nil) + type InvalidArgs struct{} + args := InvalidArgs{} + submitErr := cw.SubmitTransaction(ctx, "contract_reader_interface", "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{}{} + args := Arguments{} 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, "contractReaderInterface", "badMethod", args, txID, programID.String(), nil, nil) + + args := Arguments{} + submitErr := cw.SubmitTransaction(ctx, "contract_reader_interface", "badMethod", args, txID, programID.String(), nil, nil) require.Error(t, submitErr) }) @@ -555,13 +559,13 @@ func TestChainWriter_SubmitTransaction(t *testing.T) { return true }), &txID, mock.Anything).Return(nil).Once() - args := map[string]interface{}{ - "lookupTable": chainwriter.GetRandomPubKey(t).Bytes(), - "lookup_table": account2.Bytes(), - "seed1": seed1, - "seed2": seed2, + args := Arguments{ + LookupTable: account2, + Seed1: seed1, + Seed2: seed2, } - submitErr := cw.SubmitTransaction(ctx, "contractReaderInterface", "initializeLookupTable", args, txID, programID.String(), nil, nil) + + submitErr := cw.SubmitTransaction(ctx, "contract_reader_interface", "initializeLookupTable", args, txID, programID.String(), nil, nil) require.NoError(t, submitErr) }) } From 52c4ef4bcc08500d9036fe482fffa99d83522af3 Mon Sep 17 00:00:00 2001 From: Silas Lenihan Date: Fri, 20 Dec 2024 11:42:21 -0500 Subject: [PATCH 24/25] updated comments and slight tweaks --- .../relayinterface/chain_components_test.go | 131 ++++++++++++++++-- pkg/solana/txm/txm.go | 14 ++ 2 files changed, 137 insertions(+), 8 deletions(-) diff --git a/integration-tests/relayinterface/chain_components_test.go b/integration-tests/relayinterface/chain_components_test.go index c32507e49..39d2e5b98 100644 --- a/integration-tests/relayinterface/chain_components_test.go +++ b/integration-tests/relayinterface/chain_components_test.go @@ -7,7 +7,9 @@ import ( "context" "encoding/binary" "encoding/json" + "fmt" "io" + "log" "os" "path/filepath" "sync" @@ -19,13 +21,16 @@ import ( "github.com/gagliardetto/solana-go/rpc/ws" "github.com/gagliardetto/solana-go/text" "github.com/stretchr/testify/require" + "github.com/test-go/testify/mock" commoncodec "github.com/smartcontractkit/chainlink-common/pkg/codec" "github.com/smartcontractkit/chainlink-common/pkg/logger" commontestutils "github.com/smartcontractkit/chainlink-common/pkg/loop/testutils" + "github.com/smartcontractkit/chainlink-common/pkg/services/servicetest" "github.com/smartcontractkit/chainlink-common/pkg/types" . "github.com/smartcontractkit/chainlink-common/pkg/types/interfacetests" //nolint common practice to import test mods with . "github.com/smartcontractkit/chainlink-common/pkg/types/query/primitives" + commonutils "github.com/smartcontractkit/chainlink-common/pkg/utils" "github.com/smartcontractkit/chainlink-common/pkg/utils/tests" "github.com/smartcontractkit/chainlink-solana/pkg/solana/codec" @@ -34,7 +39,11 @@ 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/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" solanautils "github.com/smartcontractkit/chainlink-solana/pkg/solana/utils" ) @@ -127,7 +136,9 @@ type SolanaChainComponentsInterfaceTesterHelper[T TestingT[T]] interface { Context(t T) context.Context Logger(t T) logger.Logger GetJSONEncodedIDL(t T) []byte - CreateAccount(t T, value uint64) solana.PublicKey + CreateAccount(t T, it SolanaChainComponentsInterfaceTester[T], value uint64) solana.PublicKey + TXM() *txm.TxManager + SolanaClient() *client.Client } type SolanaChainComponentsInterfaceTester[T TestingT[T]] struct { @@ -135,6 +146,7 @@ type SolanaChainComponentsInterfaceTester[T TestingT[T]] struct { Helper SolanaChainComponentsInterfaceTesterHelper[T] cr *chainreader.SolanaChainReaderService contractReaderConfig config.ContractReader + chainWriterConfig chainwriter.ChainWriterConfig } func (it *SolanaChainComponentsInterfaceTester[T]) Setup(t T) { @@ -173,6 +185,41 @@ func (it *SolanaChainComponentsInterfaceTester[T]) Setup(t T) { }, }, } + + it.chainWriterConfig = chainwriter.ChainWriterConfig{ + Programs: map[string]chainwriter.ProgramConfig{ + AnyContractName: { + IDL: string(it.Helper.GetJSONEncodedIDL(t)), + Methods: map[string]chainwriter.MethodConfig{ + "initialize": { + FromAddress: solana.MustPrivateKeyFromBase58(solclient.DefaultPrivateKeysSolValidator[1]).PublicKey().String(), + InputModifications: nil, + ChainSpecificName: "initialize", + LookupTables: chainwriter.LookupTables{}, + Accounts: []chainwriter.Lookup{ + chainwriter.PDALookups{ + Name: "Account", + PublicKey: chainwriter.AccountConstant{ + Name: "ProgramID", + Address: programPubKey, + }, + Seeds: []chainwriter.Seed{ + {Static: []byte("data")}, + {Dynamic: chainwriter.AccountLookup{ + Name: "TestIDX", + Location: "testIdx", + }}, + }, + IsWritable: true, + IsSigner: false, + }, + }, + DebugIDLocation: "", + }, + }, + }, + }, + } } func (it *SolanaChainComponentsInterfaceTester[T]) Name() string { @@ -204,14 +251,18 @@ func (it *SolanaChainComponentsInterfaceTester[T]) GetContractReader(t T) types. } func (it *SolanaChainComponentsInterfaceTester[T]) GetContractWriter(t T) types.ContractWriter { - return nil + cw, err := chainwriter.NewSolanaChainWriterService(it.Helper.Logger(t), it.Helper.SolanaClient(), *it.Helper.TXM(), nil, it.chainWriterConfig) + require.NoError(t, err) + + servicetest.Run(t, cw) + return cw } func (it *SolanaChainComponentsInterfaceTester[T]) GetBindings(t T) []types.BoundContract { // Create a new account with fresh state for each test return []types.BoundContract{ - {Name: AnyContractName, Address: it.Helper.CreateAccount(t, AnyValueToReadWithoutAnArgument).String()}, - {Name: AnySecondContractName, Address: it.Helper.CreateAccount(t, AnyDifferentValueToReadWithoutAnArgument).String()}, + {Name: AnyContractName, Address: it.Helper.CreateAccount(t, *it, AnyValueToReadWithoutAnArgument).String()}, + {Name: AnySecondContractName, Address: it.Helper.CreateAccount(t, *it, AnyDifferentValueToReadWithoutAnArgument).String()}, } } @@ -234,6 +285,8 @@ type helper struct { idlBts []byte nonce uint64 nonceMu sync.Mutex + txm txm.TxManager + sc *client.Client } func (h *helper) Init(t *testing.T) { @@ -250,6 +303,26 @@ func (h *helper) Init(t *testing.T) { solanautils.FundAccounts(t, []solana.PrivateKey{privateKey}, h.rpcClient) + cfg := config.NewDefault() + solanaClient, err := client.NewClient(h.rpcURL, cfg, 5*time.Second, nil) + require.NoError(t, err) + + h.sc = solanaClient + + loader := commonutils.NewLazyLoad(func() (client.ReaderWriter, error) { return solanaClient, nil }) + mkey := keyMocks.NewSimpleKeystore(t) + mkey.On("Sign", mock.Anything, privateKey.PublicKey().String(), mock.Anything).Return(func(_ context.Context, _ string, data []byte) []byte { + sig, _ := privateKey.Sign(data) + verifySignature(privateKey.PublicKey(), sig[:], data) + fmt.Printf("Signed for %s: %x\n", privateKey.PublicKey().String(), sig) + return sig[:] + }, nil) + lggr := logger.Test(t) + + txm := txm.NewTxm("localnet", loader, nil, cfg, mkey, lggr) + txm.Start(tests.Context(t)) + h.txm = txm + pubkey, err := solana.PublicKeyFromBase58(programPubKey) require.NoError(t, err) @@ -257,10 +330,28 @@ func (h *helper) Init(t *testing.T) { h.programID = pubkey } +func verifySignature(publicKey solana.PublicKey, signature []byte, message []byte) bool { + valid := publicKey.Verify(message, solana.SignatureFromBytes(signature)) + if valid { + log.Printf("Signature is valid for public key: %s\n", publicKey.String()) + } else { + log.Printf("Signature is invalid for public key: %s\n", publicKey.String()) + } + return valid +} + func (h *helper) RPCClient() *chainreader.RPCClientWrapper { return &chainreader.RPCClientWrapper{Client: h.rpcClient} } +func (h *helper) TXM() *txm.TxManager { + return &h.txm +} + +func (h *helper) SolanaClient() *client.Client { + return h.sc +} + func (h *helper) Context(t *testing.T) context.Context { return tests.Context(t) } @@ -292,7 +383,7 @@ func (h *helper) GetJSONEncodedIDL(t *testing.T) []byte { return h.idlBts } -func (h *helper) CreateAccount(t *testing.T, value uint64) solana.PublicKey { +func (h *helper) CreateAccount(t *testing.T, it SolanaChainComponentsInterfaceTester[*testing.T], value uint64) solana.PublicKey { t.Helper() // avoid collisions in parallel tests @@ -311,7 +402,7 @@ func (h *helper) CreateAccount(t *testing.T, value uint64) solana.PublicKey { privateKey, err := solana.PrivateKeyFromBase58(solclient.DefaultPrivateKeysSolValidator[1]) require.NoError(t, err) - h.runInitialize(t, nonce, value, pubKey, func(key solana.PublicKey) *solana.PrivateKey { + h.runInitialize(t, it, nonce, value, pubKey, func(key solana.PublicKey) *solana.PrivateKey { return &privateKey }, privateKey.PublicKey()) @@ -320,6 +411,7 @@ func (h *helper) CreateAccount(t *testing.T, value uint64) solana.PublicKey { func (h *helper) runInitialize( t *testing.T, + it SolanaChainComponentsInterfaceTester[*testing.T], nonce uint64, value uint64, data solana.PublicKey, @@ -328,10 +420,33 @@ func (h *helper) runInitialize( ) { t.Helper() - inst, err := contract.NewInitializeInstruction(nonce*value, value, data, payer, solana.SystemProgramID).ValidateAndBuild() + cw := it.GetContractWriter(t) + + args := map[string]interface{}{ + "testIdx": nonce * value, + "value": value, + } + + buf := make([]byte, 8) + binary.LittleEndian.PutUint64(buf, nonce*value) + + data, _, err := solana.FindProgramAddress( + [][]byte{ + []byte("data"), // Seed 1 + buf, // Seed 2 (test_idx) + }, + solana.MustPublicKeyFromBase58(programPubKey), // The program ID + ) require.NoError(t, err) - h.sendInstruction(t, inst, signerFunc, payer) + fmt.Printf("Derived PDA in test: %s\n", data.String()) + + SubmitTransactionToCW(t, &it, cw, "initialize", args, types.BoundContract{Name: AnyContractName, Address: h.programID.String()}, types.Finalized) + + // inst, err := contract.NewInitializeInstruction(nonce*value, value, data, payer, solana.SystemProgramID).ValidateAndBuild() + // require.NoError(t, err) + + // h.sendInstruction(t, inst, signerFunc, payer) } func (h *helper) sendInstruction( diff --git a/pkg/solana/txm/txm.go b/pkg/solana/txm/txm.go index c87089060..06729ca63 100644 --- a/pkg/solana/txm/txm.go +++ b/pkg/solana/txm/txm.go @@ -253,10 +253,24 @@ func (txm *Txm) buildTx(ctx context.Context, msg pendingTx, retryCount int) (sol if err != nil { return solanaGo.Transaction{}, fmt.Errorf("error in Sign: %w", err) } + fmt.Printf("Transaction Message (hex): %x\n", txMsg) + var finalSig [64]byte copy(finalSig[:], sigBytes) newTx.Signatures = append(newTx.Signatures, finalSig) + for i, sig := range newTx.Signatures { + fmt.Printf("Signature[%d]: %x\n", i, sig) + } + + for i, account := range newTx.Message.AccountKeys { + writable, err := newTx.Message.IsWritable(account) + if err != nil { + return solanaGo.Transaction{}, fmt.Errorf("error in IsWritable: %w", err) + } + fmt.Printf("Account[%d]: %s (Signer: %v, Writable: %v)\n", i, account, newTx.Message.IsSigner(account), writable) + } + return newTx, nil } From d7ed88e4fd7737d907ed5fd70a4efa6d12dcb3da Mon Sep 17 00:00:00 2001 From: Silas Lenihan Date: Fri, 27 Dec 2024 14:14:06 -0500 Subject: [PATCH 25/25] Got ChainComponentsTests working with ChainWriter --- .../contract_reader_interface/Initialize.go | 36 ++--- .../contract-reader-interface/src/lib.rs | 6 +- .../relayinterface/chain_components_test.go | 123 +++--------------- pkg/solana/chainwriter/chain_writer_test.go | 2 - pkg/solana/chainwriter/helpers.go | 19 --- pkg/solana/txm/txm.go | 14 -- pkg/solana/utils/utils.go | 9 -- solana.sb | 3 + 8 files changed, 40 insertions(+), 172 deletions(-) create mode 100644 solana.sb diff --git a/contracts/generated/contract_reader_interface/Initialize.go b/contracts/generated/contract_reader_interface/Initialize.go index ee6fa51f8..03e13f579 100644 --- a/contracts/generated/contract_reader_interface/Initialize.go +++ b/contracts/generated/contract_reader_interface/Initialize.go @@ -15,9 +15,9 @@ type Initialize struct { TestIdx *uint64 Value *uint64 - // [0] = [WRITE] data + // [0] = [WRITE, SIGNER] signer // - // [1] = [WRITE, SIGNER] signer + // [1] = [WRITE] data // // [2] = [] systemProgram ag_solanago.AccountMetaSlice `bin:"-" borsh_skip:"true"` @@ -43,25 +43,25 @@ func (inst *Initialize) SetValue(value uint64) *Initialize { return inst } -// SetDataAccount sets the "data" account. -func (inst *Initialize) SetDataAccount(data ag_solanago.PublicKey) *Initialize { - inst.AccountMetaSlice[0] = ag_solanago.Meta(data).WRITE() +// SetSignerAccount sets the "signer" account. +func (inst *Initialize) SetSignerAccount(signer ag_solanago.PublicKey) *Initialize { + inst.AccountMetaSlice[0] = ag_solanago.Meta(signer).WRITE().SIGNER() return inst } -// GetDataAccount gets the "data" account. -func (inst *Initialize) GetDataAccount() *ag_solanago.AccountMeta { +// GetSignerAccount gets the "signer" account. +func (inst *Initialize) GetSignerAccount() *ag_solanago.AccountMeta { return inst.AccountMetaSlice[0] } -// SetSignerAccount sets the "signer" account. -func (inst *Initialize) SetSignerAccount(signer ag_solanago.PublicKey) *Initialize { - inst.AccountMetaSlice[1] = ag_solanago.Meta(signer).WRITE().SIGNER() +// SetDataAccount sets the "data" account. +func (inst *Initialize) SetDataAccount(data ag_solanago.PublicKey) *Initialize { + inst.AccountMetaSlice[1] = ag_solanago.Meta(data).WRITE() return inst } -// GetSignerAccount gets the "signer" account. -func (inst *Initialize) GetSignerAccount() *ag_solanago.AccountMeta { +// GetDataAccount gets the "data" account. +func (inst *Initialize) GetDataAccount() *ag_solanago.AccountMeta { return inst.AccountMetaSlice[1] } @@ -107,10 +107,10 @@ func (inst *Initialize) Validate() error { // Check whether all (required) accounts are set: { if inst.AccountMetaSlice[0] == nil { - return errors.New("accounts.Data is not set") + return errors.New("accounts.Signer is not set") } if inst.AccountMetaSlice[1] == nil { - return errors.New("accounts.Signer is not set") + return errors.New("accounts.Data is not set") } if inst.AccountMetaSlice[2] == nil { return errors.New("accounts.SystemProgram is not set") @@ -135,8 +135,8 @@ func (inst *Initialize) EncodeToTree(parent ag_treeout.Branches) { // Accounts of the instruction: instructionBranch.Child("Accounts[len=3]").ParentFunc(func(accountsBranch ag_treeout.Branches) { - accountsBranch.Child(ag_format.Meta(" data", inst.AccountMetaSlice[0])) - accountsBranch.Child(ag_format.Meta(" signer", inst.AccountMetaSlice[1])) + accountsBranch.Child(ag_format.Meta(" signer", inst.AccountMetaSlice[0])) + accountsBranch.Child(ag_format.Meta(" data", inst.AccountMetaSlice[1])) accountsBranch.Child(ag_format.Meta("systemProgram", inst.AccountMetaSlice[2])) }) }) @@ -176,13 +176,13 @@ func NewInitializeInstruction( testIdx uint64, value uint64, // Accounts: - data ag_solanago.PublicKey, signer ag_solanago.PublicKey, + data ag_solanago.PublicKey, systemProgram ag_solanago.PublicKey) *Initialize { return NewInitializeInstructionBuilder(). SetTestIdx(testIdx). SetValue(value). - SetDataAccount(data). SetSignerAccount(signer). + SetDataAccount(data). SetSystemProgramAccount(systemProgram) } diff --git a/contracts/programs/contract-reader-interface/src/lib.rs b/contracts/programs/contract-reader-interface/src/lib.rs index b02b68888..51c06a33e 100644 --- a/contracts/programs/contract-reader-interface/src/lib.rs +++ b/contracts/programs/contract-reader-interface/src/lib.rs @@ -35,6 +35,9 @@ pub mod contract_reader_interface { #[derive(Accounts)] #[instruction(test_idx: u64)] pub struct Initialize<'info> { + #[account(mut)] + pub signer: Signer<'info>, + // derived test PDA #[account( init, @@ -44,9 +47,6 @@ pub struct Initialize<'info> { bump)] pub data: Account<'info, DataAccount>, - #[account(mut)] - pub signer: Signer<'info>, - pub system_program: Program<'info, System>, } diff --git a/integration-tests/relayinterface/chain_components_test.go b/integration-tests/relayinterface/chain_components_test.go index 39d2e5b98..3b05020ad 100644 --- a/integration-tests/relayinterface/chain_components_test.go +++ b/integration-tests/relayinterface/chain_components_test.go @@ -7,9 +7,6 @@ import ( "context" "encoding/binary" "encoding/json" - "fmt" - "io" - "log" "os" "path/filepath" "sync" @@ -19,11 +16,11 @@ import ( "github.com/gagliardetto/solana-go" "github.com/gagliardetto/solana-go/rpc" "github.com/gagliardetto/solana-go/rpc/ws" - "github.com/gagliardetto/solana-go/text" + "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" - "github.com/test-go/testify/mock" commoncodec "github.com/smartcontractkit/chainlink-common/pkg/codec" + commonconfig "github.com/smartcontractkit/chainlink-common/pkg/config" "github.com/smartcontractkit/chainlink-common/pkg/logger" commontestutils "github.com/smartcontractkit/chainlink-common/pkg/loop/testutils" "github.com/smartcontractkit/chainlink-common/pkg/services/servicetest" @@ -207,7 +204,7 @@ func (it *SolanaChainComponentsInterfaceTester[T]) Setup(t T) { {Static: []byte("data")}, {Dynamic: chainwriter.AccountLookup{ Name: "TestIDX", - Location: "testIdx", + Location: "TestIdx", }}, }, IsWritable: true, @@ -304,6 +301,7 @@ func (h *helper) Init(t *testing.T) { solanautils.FundAccounts(t, []solana.PrivateKey{privateKey}, h.rpcClient) cfg := config.NewDefault() + cfg.Chain.TxRetentionTimeout = commonconfig.MustNewDuration(10 * time.Minute) solanaClient, err := client.NewClient(h.rpcURL, cfg, 5*time.Second, nil) require.NoError(t, err) @@ -313,14 +311,14 @@ func (h *helper) Init(t *testing.T) { mkey := keyMocks.NewSimpleKeystore(t) mkey.On("Sign", mock.Anything, privateKey.PublicKey().String(), mock.Anything).Return(func(_ context.Context, _ string, data []byte) []byte { sig, _ := privateKey.Sign(data) - verifySignature(privateKey.PublicKey(), sig[:], data) - fmt.Printf("Signed for %s: %x\n", privateKey.PublicKey().String(), sig) return sig[:] }, nil) lggr := logger.Test(t) txm := txm.NewTxm("localnet", loader, nil, cfg, mkey, lggr) - txm.Start(tests.Context(t)) + err = txm.Start(tests.Context(t)) + require.NoError(t, err) + h.txm = txm pubkey, err := solana.PublicKeyFromBase58(programPubKey) @@ -330,16 +328,6 @@ func (h *helper) Init(t *testing.T) { h.programID = pubkey } -func verifySignature(publicKey solana.PublicKey, signature []byte, message []byte) bool { - valid := publicKey.Verify(message, solana.SignatureFromBytes(signature)) - if valid { - log.Printf("Signature is valid for public key: %s\n", publicKey.String()) - } else { - log.Printf("Signature is invalid for public key: %s\n", publicKey.String()) - } - return valid -} - func (h *helper) RPCClient() *chainreader.RPCClientWrapper { return &chainreader.RPCClientWrapper{Client: h.rpcClient} } @@ -398,114 +386,35 @@ func (h *helper) CreateAccount(t *testing.T, it SolanaChainComponentsInterfaceTe pubKey, _, err := solana.FindProgramAddress([][]byte{[]byte("data"), bts}, h.programID) require.NoError(t, err) - // Getting the default localnet private key - privateKey, err := solana.PrivateKeyFromBase58(solclient.DefaultPrivateKeysSolValidator[1]) - require.NoError(t, err) - - h.runInitialize(t, it, nonce, value, pubKey, func(key solana.PublicKey) *solana.PrivateKey { - return &privateKey - }, privateKey.PublicKey()) + h.runInitialize(t, it, nonce, value) return pubKey } +type InitializeArgs struct { + TestIdx uint64 + Value uint64 +} + func (h *helper) runInitialize( t *testing.T, it SolanaChainComponentsInterfaceTester[*testing.T], nonce uint64, value uint64, - data solana.PublicKey, - signerFunc func(key solana.PublicKey) *solana.PrivateKey, - payer solana.PublicKey, ) { t.Helper() cw := it.GetContractWriter(t) - args := map[string]interface{}{ - "testIdx": nonce * value, - "value": value, + args := InitializeArgs{ + TestIdx: nonce * value, + Value: value, } buf := make([]byte, 8) binary.LittleEndian.PutUint64(buf, nonce*value) - data, _, err := solana.FindProgramAddress( - [][]byte{ - []byte("data"), // Seed 1 - buf, // Seed 2 (test_idx) - }, - solana.MustPublicKeyFromBase58(programPubKey), // The program ID - ) - require.NoError(t, err) - - fmt.Printf("Derived PDA in test: %s\n", data.String()) - SubmitTransactionToCW(t, &it, cw, "initialize", args, types.BoundContract{Name: AnyContractName, Address: h.programID.String()}, types.Finalized) - - // inst, err := contract.NewInitializeInstruction(nonce*value, value, data, payer, solana.SystemProgramID).ValidateAndBuild() - // require.NoError(t, err) - - // h.sendInstruction(t, inst, signerFunc, payer) -} - -func (h *helper) sendInstruction( - t *testing.T, - inst *contract.Instruction, - signerFunc func(key solana.PublicKey) *solana.PrivateKey, - payer solana.PublicKey, -) { - t.Helper() - - ctx := tests.Context(t) - - recent, err := h.rpcClient.GetLatestBlockhash(ctx, rpc.CommitmentFinalized) - require.NoError(t, err) - - tx, err := solana.NewTransaction( - []solana.Instruction{ - inst, - }, - recent.Value.Blockhash, - solana.TransactionPayer(payer), - ) - require.NoError(t, err) - - _, err = tx.EncodeTree(text.NewTreeEncoder(io.Discard, "Initialize")) - require.NoError(t, err) - - _, err = tx.Sign(signerFunc) - require.NoError(t, err) - - sig, err := h.rpcClient.SendTransactionWithOpts( - ctx, tx, - rpc.TransactionOpts{ - PreflightCommitment: rpc.CommitmentConfirmed, - }, - ) - require.NoError(t, err) - - h.waitForTX(t, sig, rpc.CommitmentFinalized) -} - -func (h *helper) waitForTX(t *testing.T, sig solana.Signature, commitment rpc.CommitmentType) { - t.Helper() - - sub, err := h.wsClient.SignatureSubscribe( - sig, - commitment, - ) - require.NoError(t, err) - - defer sub.Unsubscribe() - - res, err := sub.Recv() - require.NoError(t, err) - - if res.Value.Err != nil { - t.Logf("transaction confirmation failed: %v", res.Value.Err) - t.FailNow() - } } func mustUnmarshalIDL[T TestingT[T]](t T, rawIDL string) codec.IDL { diff --git a/pkg/solana/chainwriter/chain_writer_test.go b/pkg/solana/chainwriter/chain_writer_test.go index cd215971e..947674a2f 100644 --- a/pkg/solana/chainwriter/chain_writer_test.go +++ b/pkg/solana/chainwriter/chain_writer_test.go @@ -3,7 +3,6 @@ package chainwriter_test import ( "bytes" "errors" - "fmt" "math/big" "os" "reflect" @@ -409,7 +408,6 @@ func TestChainWriter_SubmitTransaction(t *testing.T) { seed2 := []byte("seed2") programID := solana.MustPublicKeyFromBase58("6AfuXF6HapDUhQfE4nQG9C1SGtA1YjP3icaJyRfU4RyE") derivedTablePda := mustFindPdaProgramAddress(t, [][]byte{seed2}, programID) - fmt.Println("pda:", derivedTablePda) // mock data account response from program derivedLookupTablePubkey := mockDataAccountLookupTable(t, rw, derivedTablePda) // mock fetch lookup table addresses call diff --git a/pkg/solana/chainwriter/helpers.go b/pkg/solana/chainwriter/helpers.go index b2c008011..a4b18e4d5 100644 --- a/pkg/solana/chainwriter/helpers.go +++ b/pkg/solana/chainwriter/helpers.go @@ -81,25 +81,6 @@ func errorWithDebugID(err error, debugID string) error { return fmt.Errorf("Debug ID: %s: Error: %s", debugID, err) } -func GetDebugIDAtLocation(args any, location string) (string, error) { - debugIDList, err := GetValueAtLocation(args, 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 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/txm/txm.go b/pkg/solana/txm/txm.go index 06729ca63..c87089060 100644 --- a/pkg/solana/txm/txm.go +++ b/pkg/solana/txm/txm.go @@ -253,24 +253,10 @@ func (txm *Txm) buildTx(ctx context.Context, msg pendingTx, retryCount int) (sol if err != nil { return solanaGo.Transaction{}, fmt.Errorf("error in Sign: %w", err) } - fmt.Printf("Transaction Message (hex): %x\n", txMsg) - var finalSig [64]byte copy(finalSig[:], sigBytes) newTx.Signatures = append(newTx.Signatures, finalSig) - for i, sig := range newTx.Signatures { - fmt.Printf("Signature[%d]: %x\n", i, sig) - } - - for i, account := range newTx.Message.AccountKeys { - writable, err := newTx.Message.IsWritable(account) - if err != nil { - return solanaGo.Transaction{}, fmt.Errorf("error in IsWritable: %w", err) - } - fmt.Printf("Account[%d]: %s (Signer: %v, Writable: %v)\n", i, account, newTx.Message.IsSigner(account), writable) - } - return newTx, nil } diff --git a/pkg/solana/utils/utils.go b/pkg/solana/utils/utils.go index deb747158..764c236de 100644 --- a/pkg/solana/utils/utils.go +++ b/pkg/solana/utils/utils.go @@ -31,15 +31,6 @@ var ( PathToAnchorConfig = filepath.Join(ProjectRoot, "contracts", "Anchor.toml") ) -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 diff --git a/solana.sb b/solana.sb new file mode 100644 index 000000000..fab47f4ba --- /dev/null +++ b/solana.sb @@ -0,0 +1,3 @@ +(version 1) +(deny default) +(allow network*)