From 95e21f4d6c48af684e0a56152b9a83de544c9837 Mon Sep 17 00:00:00 2001 From: Jordan Krage Date: Tue, 4 Feb 2025 16:12:32 -0600 Subject: [PATCH] pkg/solana: bump framework; pass context to select rpc --- go.mod | 2 +- go.sum | 4 +- integration-tests/go.mod | 14 +- integration-tests/go.sum | 24 +-- .../relayinterface/chain_components_test.go | 7 +- .../relayinterface/lookups_test.go | 15 +- integration-tests/utils/utils.go | 167 ++++++++++++++++++ pkg/solana/chain.go | 27 +-- pkg/solana/chain_test.go | 25 +-- pkg/solana/chainwriter/helpers.go | 23 --- pkg/solana/client/multi_client.go | 42 ++--- pkg/solana/fees/block_history.go | 9 +- pkg/solana/fees/block_history_test.go | 51 ++---- pkg/solana/internal/loader.go | 24 --- pkg/solana/monitor/balance.go | 19 +- pkg/solana/monitor/balance_test.go | 3 +- pkg/solana/txm/txm.go | 32 ++-- pkg/solana/txm/txm_integration_test.go | 9 +- pkg/solana/txm/txm_internal_test.go | 28 +-- pkg/solana/txm/txm_load_test.go | 14 +- pkg/solana/txm/txm_race_test.go | 13 +- pkg/solana/txm/txm_unit_test.go | 6 +- pkg/solana/utils/loader.go | 78 ++++++++ pkg/solana/{internal => utils}/loader_test.go | 16 +- pkg/solana/utils/utils.go | 130 +------------- 25 files changed, 421 insertions(+), 361 deletions(-) create mode 100644 integration-tests/utils/utils.go delete mode 100644 pkg/solana/internal/loader.go create mode 100644 pkg/solana/utils/loader.go rename pkg/solana/{internal => utils}/loader_test.go (56%) diff --git a/go.mod b/go.mod index 602265192..b95b6628f 100644 --- a/go.mod +++ b/go.mod @@ -18,7 +18,7 @@ require ( github.com/smartcontractkit/chainlink-ccip v0.0.0-20250128162345-af4c8fd4481a github.com/smartcontractkit/chainlink-ccip/chains/solana v0.0.0-20250128162345-af4c8fd4481a github.com/smartcontractkit/chainlink-common v0.4.2-0.20250127125541-a8fa42cc0f36 - github.com/smartcontractkit/chainlink-framework/multinode v0.0.0-20250203183025-939526523893 + github.com/smartcontractkit/chainlink-framework/multinode v0.0.0-20250205165125-271e20f6de0a github.com/smartcontractkit/libocr v0.0.0-20241223215956-e5b78d8e3919 github.com/stretchr/testify v1.10.0 go.uber.org/zap v1.27.0 diff --git a/go.sum b/go.sum index 038a893d8..5e83fc94d 100644 --- a/go.sum +++ b/go.sum @@ -581,8 +581,8 @@ github.com/smartcontractkit/chainlink-ccip/chains/solana v0.0.0-20250128162345-a github.com/smartcontractkit/chainlink-ccip/chains/solana v0.0.0-20250128162345-af4c8fd4481a/go.mod h1:Bmwq4lNb5tE47sydN0TKetcLEGbgl+VxHEWp4S0LI60= github.com/smartcontractkit/chainlink-common v0.4.2-0.20250127125541-a8fa42cc0f36 h1:dytZPggag6auyzmbhpIDmkHu7KrflIBEhLLec4/xFIk= github.com/smartcontractkit/chainlink-common v0.4.2-0.20250127125541-a8fa42cc0f36/go.mod h1:Z2e1ynSJ4pg83b4Qldbmryc5lmnrI3ojOdg1FUloa68= -github.com/smartcontractkit/chainlink-framework/multinode v0.0.0-20250203183025-939526523893 h1:hQEEpKrWRqZ//SkA/m1G5puVHK1mYhZzturgX7VsPhk= -github.com/smartcontractkit/chainlink-framework/multinode v0.0.0-20250203183025-939526523893/go.mod h1:4JqpgFy01LaqG1yM2iFTzwX3ZgcAvW9WdstBZQgPHzU= +github.com/smartcontractkit/chainlink-framework/multinode v0.0.0-20250205165125-271e20f6de0a h1:ZG8v7aQxyp9cOYXpW6oodL+OWgwDku544qyzXPPgs7M= +github.com/smartcontractkit/chainlink-framework/multinode v0.0.0-20250205165125-271e20f6de0a/go.mod h1:4JqpgFy01LaqG1yM2iFTzwX3ZgcAvW9WdstBZQgPHzU= github.com/smartcontractkit/grpc-proxy v0.0.0-20240830132753-a7e17fec5ab7 h1:12ijqMM9tvYVEm+nR826WsrNi6zCKpwBhuApq127wHs= github.com/smartcontractkit/grpc-proxy v0.0.0-20240830132753-a7e17fec5ab7/go.mod h1:FX7/bVdoep147QQhsOPkYsPEXhGZjeYx6lBSaSXtZOA= github.com/smartcontractkit/libocr v0.0.0-20241223215956-e5b78d8e3919 h1:IpGoPTXpvllN38kT2z2j13sifJMz4nbHglidvop7mfg= diff --git a/integration-tests/go.mod b/integration-tests/go.mod index b1eb47507..1e81a5600 100644 --- a/integration-tests/go.mod +++ b/integration-tests/go.mod @@ -15,12 +15,12 @@ require ( github.com/pelletier/go-toml/v2 v2.2.3 github.com/rs/zerolog v1.33.0 github.com/smartcontractkit/chainlink-common v0.4.2-0.20250130202959-6f1f48342e36 - github.com/smartcontractkit/chainlink-solana v1.1.2-0.20250203214419-38982a7fc48b + github.com/smartcontractkit/chainlink-solana v1.1.2-0.20250204221232-93cfb3ea152b github.com/smartcontractkit/chainlink-testing-framework/lib v1.50.21 github.com/smartcontractkit/chainlink-testing-framework/seth v1.50.10 - github.com/smartcontractkit/chainlink/deployment v0.0.0-20250203214543-d9da97d53b9b - github.com/smartcontractkit/chainlink/integration-tests v0.0.0-20250203214543-d9da97d53b9b - github.com/smartcontractkit/chainlink/v2 v2.19.0-ccip1.5.16-alpha.0.0.20250203214543-d9da97d53b9b + github.com/smartcontractkit/chainlink/deployment v0.0.0-20250204221413-c69897a24c5d + github.com/smartcontractkit/chainlink/integration-tests v0.0.0-20250204221413-c69897a24c5d + github.com/smartcontractkit/chainlink/v2 v2.19.0-ccip1.5.16-alpha.0.0.20250204221413-c69897a24c5d github.com/smartcontractkit/libocr v0.0.0-20241223215956-e5b78d8e3919 github.com/stretchr/testify v1.10.0 github.com/testcontainers/testcontainers-go v0.35.0 @@ -342,13 +342,13 @@ require ( github.com/slack-go/slack v0.15.0 // indirect github.com/smartcontractkit/chain-selectors v1.0.37 // indirect github.com/smartcontractkit/chainlink-automation v0.8.1 // indirect - github.com/smartcontractkit/chainlink-ccip v0.0.0-20250203130001-13e2609047e9 // indirect + github.com/smartcontractkit/chainlink-ccip v0.0.0-20250203132120-f0d42463e405 // indirect github.com/smartcontractkit/chainlink-ccip/chains/solana v0.0.0-20250128162345-af4c8fd4481a // indirect github.com/smartcontractkit/chainlink-cosmos v0.5.2-0.20250130125138-3df261e09ddc // indirect github.com/smartcontractkit/chainlink-data-streams v0.1.1-0.20250128203428-08031923fbe5 // indirect github.com/smartcontractkit/chainlink-feeds v0.1.1 // indirect - github.com/smartcontractkit/chainlink-framework/chains v0.0.0-20250203160922-fbdf168bb92a // indirect - github.com/smartcontractkit/chainlink-framework/multinode v0.0.0-20250203183025-939526523893 // indirect + github.com/smartcontractkit/chainlink-framework/chains v0.0.0-20250204211601-c6bfa53cfb1c // indirect + github.com/smartcontractkit/chainlink-framework/multinode v0.0.0-20250205165125-271e20f6de0a // indirect github.com/smartcontractkit/chainlink-protos/orchestrator v0.4.0 // indirect github.com/smartcontractkit/chainlink-protos/svr v0.0.0-20250123084029-58cce9b32112 // indirect github.com/smartcontractkit/chainlink-starknet/relayer v0.1.1-0.20250117224137-afdcdd75070d // indirect diff --git a/integration-tests/go.sum b/integration-tests/go.sum index 5fd9e4c64..b74d9536c 100644 --- a/integration-tests/go.sum +++ b/integration-tests/go.sum @@ -1227,8 +1227,8 @@ github.com/smartcontractkit/chain-selectors v1.0.37 h1:EKVl8wayhOVfnlqfVmEyZ8rXO github.com/smartcontractkit/chain-selectors v1.0.37/go.mod h1:xsKM0aN3YGcQKTPRPDDtPx2l4mlTN1Djmg0VVXV40b8= github.com/smartcontractkit/chainlink-automation v0.8.1 h1:sTc9LKpBvcKPc1JDYAmgBc2xpDKBco/Q4h4ydl6+UUU= github.com/smartcontractkit/chainlink-automation v0.8.1/go.mod h1:Iij36PvWZ6blrdC5A/nrQUBuf3MH3JvsBB9sSyc9W08= -github.com/smartcontractkit/chainlink-ccip v0.0.0-20250203130001-13e2609047e9 h1:+/KEPuWctPObgOoEEBCnli1/H3XnjMdCY3Tn+J32XRM= -github.com/smartcontractkit/chainlink-ccip v0.0.0-20250203130001-13e2609047e9/go.mod h1:UEnHaxkUsfreeA7rR45LMmua1Uen95tOFUR8/AI9BAo= +github.com/smartcontractkit/chainlink-ccip v0.0.0-20250203132120-f0d42463e405 h1:5QyaPGLmt+rlnvQL7drAE23Wq9rX5hO35kTZirAb97A= +github.com/smartcontractkit/chainlink-ccip v0.0.0-20250203132120-f0d42463e405/go.mod h1:UEnHaxkUsfreeA7rR45LMmua1Uen95tOFUR8/AI9BAo= github.com/smartcontractkit/chainlink-ccip/chains/solana v0.0.0-20250128162345-af4c8fd4481a h1:1MrD2OiP/CRfyBSwTQE66R1+gLWBgWcU/SYl/+DmZ/Y= github.com/smartcontractkit/chainlink-ccip/chains/solana v0.0.0-20250128162345-af4c8fd4481a/go.mod h1:Bmwq4lNb5tE47sydN0TKetcLEGbgl+VxHEWp4S0LI60= github.com/smartcontractkit/chainlink-common v0.4.2-0.20250130202959-6f1f48342e36 h1:bS51NFGHVjkCy7yu9L2Ss4sBsCW6jpa5GuhRAdWWxzM= @@ -1239,10 +1239,10 @@ github.com/smartcontractkit/chainlink-data-streams v0.1.1-0.20250128203428-08031 github.com/smartcontractkit/chainlink-data-streams v0.1.1-0.20250128203428-08031923fbe5/go.mod h1:pDZagSGjs9U+l4YIFhveDznMHqxuuz+5vRxvVgpbdr8= github.com/smartcontractkit/chainlink-feeds v0.1.1 h1:JzvUOM/OgGQA1sOqTXXl52R6AnNt+Wg64sVG+XSA49c= github.com/smartcontractkit/chainlink-feeds v0.1.1/go.mod h1:55EZ94HlKCfAsUiKUTNI7QlE/3d3IwTlsU3YNa/nBb4= -github.com/smartcontractkit/chainlink-framework/chains v0.0.0-20250203160922-fbdf168bb92a h1:fVtn9CDfoGF40FeqGwLvp9belfIw7VT3lgQTctFGP5E= -github.com/smartcontractkit/chainlink-framework/chains v0.0.0-20250203160922-fbdf168bb92a/go.mod h1:tHem58EihQh63kR2LlAOKDAs9Vbghf1dJKZRGy6LG8g= -github.com/smartcontractkit/chainlink-framework/multinode v0.0.0-20250203183025-939526523893 h1:hQEEpKrWRqZ//SkA/m1G5puVHK1mYhZzturgX7VsPhk= -github.com/smartcontractkit/chainlink-framework/multinode v0.0.0-20250203183025-939526523893/go.mod h1:4JqpgFy01LaqG1yM2iFTzwX3ZgcAvW9WdstBZQgPHzU= +github.com/smartcontractkit/chainlink-framework/chains v0.0.0-20250204211601-c6bfa53cfb1c h1:/Bai8iDJQ8l+93i57cZGibTos4QJh6P4YKbMkRLHjBQ= +github.com/smartcontractkit/chainlink-framework/chains v0.0.0-20250204211601-c6bfa53cfb1c/go.mod h1:tHem58EihQh63kR2LlAOKDAs9Vbghf1dJKZRGy6LG8g= +github.com/smartcontractkit/chainlink-framework/multinode v0.0.0-20250205165125-271e20f6de0a h1:ZG8v7aQxyp9cOYXpW6oodL+OWgwDku544qyzXPPgs7M= +github.com/smartcontractkit/chainlink-framework/multinode v0.0.0-20250205165125-271e20f6de0a/go.mod h1:4JqpgFy01LaqG1yM2iFTzwX3ZgcAvW9WdstBZQgPHzU= github.com/smartcontractkit/chainlink-protos/orchestrator v0.4.0 h1:ZBat8EBvE2LpSQR9U1gEbRV6PfAkiFdINmQ8nVnXIAQ= github.com/smartcontractkit/chainlink-protos/orchestrator v0.4.0/go.mod h1:m/A3lqD7ms/RsQ9BT5P2uceYY0QX5mIt4KQxT2G6qEo= github.com/smartcontractkit/chainlink-protos/svr v0.0.0-20250123084029-58cce9b32112 h1:c77Gi/APraqwbBO8fbd/5JY2wW+MSIpYg8Uma9MEZFE= @@ -1253,12 +1253,12 @@ github.com/smartcontractkit/chainlink-testing-framework/lib v1.50.21 h1:1UYLu0QA github.com/smartcontractkit/chainlink-testing-framework/lib v1.50.21/go.mod h1:y6pVvAT/R+YGocAqoQIat+AEaZz2Jdmj/0uUBmwvLCU= github.com/smartcontractkit/chainlink-testing-framework/seth v1.50.10 h1:Yf+n3T/fnUWcYyfe7bsygV4sWAkNo0QhN58APJFIKIc= github.com/smartcontractkit/chainlink-testing-framework/seth v1.50.10/go.mod h1:05duR85P8YHuIfIkA7sn2bvrhKo/pDpFKV2rliYHNOo= -github.com/smartcontractkit/chainlink/deployment v0.0.0-20250203214543-d9da97d53b9b h1:CJbV0ra65AiR5K1GZpbXHyDcPtDP5j82U6RY1pDWpPg= -github.com/smartcontractkit/chainlink/deployment v0.0.0-20250203214543-d9da97d53b9b/go.mod h1:/9iouaqMDOAyPkHKFiYzzPL/5516U7mjp/t14XiN59I= -github.com/smartcontractkit/chainlink/integration-tests v0.0.0-20250203214543-d9da97d53b9b h1:x3mtyzJAGovCmjawrmTHW4XHvchFjOXJtrL15OujeM0= -github.com/smartcontractkit/chainlink/integration-tests v0.0.0-20250203214543-d9da97d53b9b/go.mod h1:e/GI2DNI54CKvpTY6Wqq8fcpZ3xYztWv+ihG5CF1XUc= -github.com/smartcontractkit/chainlink/v2 v2.19.0-ccip1.5.16-alpha.0.0.20250203214543-d9da97d53b9b h1:fWFIcI6tzCzF3j2FBcbOGv3E7rfX0URmxI2zO3tFuXA= -github.com/smartcontractkit/chainlink/v2 v2.19.0-ccip1.5.16-alpha.0.0.20250203214543-d9da97d53b9b/go.mod h1:huBdm7XEfj6DniyGxYLxV7g41McNvkyUhPRQIO/yXko= +github.com/smartcontractkit/chainlink/deployment v0.0.0-20250204221413-c69897a24c5d h1:zekv9FnVSvP6GR/sgjQplPM18mPck4yQGbILWmVfkwM= +github.com/smartcontractkit/chainlink/deployment v0.0.0-20250204221413-c69897a24c5d/go.mod h1:gmWQ4kQvNftID+6cPFwtWz+6OvEzK8IU+6FWWDuJ4NE= +github.com/smartcontractkit/chainlink/integration-tests v0.0.0-20250204221413-c69897a24c5d h1:Wccwi7MYscyorqG9fm5Dq7hdrEJAk6oOmD3VlW3WUeM= +github.com/smartcontractkit/chainlink/integration-tests v0.0.0-20250204221413-c69897a24c5d/go.mod h1:pmyE+ZZRMQITcQ0lf1ZfKNSuqVh8dMrf0xSiHVPHFlw= +github.com/smartcontractkit/chainlink/v2 v2.19.0-ccip1.5.16-alpha.0.0.20250204221413-c69897a24c5d h1:amubfj+/h+BTJBkyQA+pREL8CbfVKHtBvRivrjYVScQ= +github.com/smartcontractkit/chainlink/v2 v2.19.0-ccip1.5.16-alpha.0.0.20250204221413-c69897a24c5d/go.mod h1:Jpn6AiX4YGRO8u9RT4/T0u/6i0oVB5QuOnU5OQG4UYQ= github.com/smartcontractkit/grpc-proxy v0.0.0-20240830132753-a7e17fec5ab7 h1:12ijqMM9tvYVEm+nR826WsrNi6zCKpwBhuApq127wHs= github.com/smartcontractkit/grpc-proxy v0.0.0-20240830132753-a7e17fec5ab7/go.mod h1:FX7/bVdoep147QQhsOPkYsPEXhGZjeYx6lBSaSXtZOA= github.com/smartcontractkit/libocr v0.0.0-20241223215956-e5b78d8e3919 h1:IpGoPTXpvllN38kT2z2j13sifJMz4nbHglidvop7mfg= diff --git a/integration-tests/relayinterface/chain_components_test.go b/integration-tests/relayinterface/chain_components_test.go index 330b6e65d..a7614787d 100644 --- a/integration-tests/relayinterface/chain_components_test.go +++ b/integration-tests/relayinterface/chain_components_test.go @@ -28,14 +28,13 @@ import ( "github.com/smartcontractkit/chainlink-common/pkg/types/query/primitives" "github.com/smartcontractkit/chainlink-common/pkg/utils/tests" + contract "github.com/smartcontractkit/chainlink-solana/contracts/generated/contract_reader_interface" "github.com/smartcontractkit/chainlink-solana/pkg/solana/codec" - contract "github.com/smartcontractkit/chainlink-solana/contracts/generated/contract_reader_interface" "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/config" - solanautils "github.com/smartcontractkit/chainlink-solana/pkg/solana/utils" ) func TestChainComponents(t *testing.T) { @@ -242,13 +241,13 @@ func (h *helper) Init(t *testing.T) { privateKey, err := solana.PrivateKeyFromBase58(solclient.DefaultPrivateKeysSolValidator[1]) require.NoError(t, err) - h.rpcURL, h.wsURL = solanautils.SetupTestValidatorWithAnchorPrograms(t, privateKey.PublicKey().String(), []string{"contract-reader-interface"}) + h.rpcURL, h.wsURL = utils.SetupTestValidatorWithAnchorPrograms(t, privateKey.PublicKey().String(), []string{"contract-reader-interface"}) h.wsClient, err = ws.Connect(tests.Context(t), h.wsURL) h.rpcClient = rpc.New(h.rpcURL) require.NoError(t, err) - solanautils.FundAccounts(t, []solana.PrivateKey{privateKey}, h.rpcClient) + utils.FundAccounts(t, []solana.PrivateKey{privateKey}, h.rpcClient) pubkey, err := solana.PublicKeyFromBase58(programPubKey) require.NoError(t, err) diff --git a/integration-tests/relayinterface/lookups_test.go b/integration-tests/relayinterface/lookups_test.go index 221b08652..9290122f0 100644 --- a/integration-tests/relayinterface/lookups_test.go +++ b/integration-tests/relayinterface/lookups_test.go @@ -9,15 +9,16 @@ import ( "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/integration-tests/utils" + "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" + solanautils "github.com/smartcontractkit/chainlink-solana/pkg/solana/utils" ) type InnerAccountArgs struct { @@ -475,7 +476,7 @@ func TestLookupTables(t *testing.T) { solanaClient, err := client.NewClient(url, cfg, 5*time.Second, nil) require.NoError(t, err) - loader := commonutils.NewLazyLoad(func() (client.ReaderWriter, error) { return solanaClient, nil }) + loader := solanautils.NewStaticLoader[client.ReaderWriter](solanaClient) mkey := keyMocks.NewSimpleKeystore(t) lggr := logger.Test(t) @@ -485,7 +486,7 @@ func TestLookupTables(t *testing.T) { t.Run("StaticLookup table resolves properly", func(t *testing.T) { pubKeys := chainwriter.CreateTestPubKeys(t, 8) - table := chainwriter.CreateTestLookupTable(ctx, t, rpcClient, sender, pubKeys) + table := utils.CreateTestLookupTable(ctx, t, rpcClient, sender, pubKeys) lookupConfig := chainwriter.LookupTables{ DerivedLookupTables: nil, StaticLookupTables: []solana.PublicKey{table}, @@ -496,7 +497,7 @@ func TestLookupTables(t *testing.T) { }) 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) + table := utils.CreateTestLookupTable(ctx, t, rpcClient, sender, pubKeys) lookupConfig := chainwriter.LookupTables{ DerivedLookupTables: []chainwriter.DerivedLookupTable{ { @@ -559,7 +560,7 @@ func TestLookupTables(t *testing.T) { 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) + table := utils.CreateTestLookupTable(ctx, t, rpcClient, sender, pubKeys) lookupConfig := chainwriter.LookupTables{ DerivedLookupTables: []chainwriter.DerivedLookupTable{ { @@ -595,7 +596,7 @@ func TestLookupTables(t *testing.T) { programID := solana.MustPublicKeyFromBase58("6AfuXF6HapDUhQfE4nQG9C1SGtA1YjP3icaJyRfU4RyE") lookupKeys := chainwriter.CreateTestPubKeys(t, 5) - lookupTable := chainwriter.CreateTestLookupTable(ctx, t, rpcClient, sender, lookupKeys) + lookupTable := utils.CreateTestLookupTable(ctx, t, rpcClient, sender, lookupKeys) chainwriter.InitializeDataAccount(ctx, t, rpcClient, programID, sender, lookupTable) diff --git a/integration-tests/utils/utils.go b/integration-tests/utils/utils.go new file mode 100644 index 000000000..c59458536 --- /dev/null +++ b/integration-tests/utils/utils.go @@ -0,0 +1,167 @@ +package utils + +import ( + "context" + "encoding/binary" + "fmt" + "os" + "path/filepath" + "strings" + "testing" + "time" + + "github.com/gagliardetto/solana-go" + "github.com/gagliardetto/solana-go/rpc" + "github.com/pelletier/go-toml/v2" + "github.com/stretchr/testify/require" + + "github.com/smartcontractkit/chainlink-common/pkg/utils/tests" + + "github.com/smartcontractkit/chainlink-solana/pkg/solana/client" + "github.com/smartcontractkit/chainlink-solana/pkg/solana/utils" +) + +var PathToAnchorConfig = filepath.Join(ProjectRoot, "contracts", "Anchor.toml") + +var ( + AddressLookupTableProgram = solana.MustPublicKeyFromBase58("AddressLookupTab1e1111111111111111111111111") +) + +const ( + InstructionCreateLookupTable uint32 = iota + InstructionFreezeLookupTable + InstructionExtendLookupTable + InstructionDeactiveLookupTable + InstructionCloseLookupTable +) + +func NewCreateLookupTableInstruction( + authority, funder solana.PublicKey, + slot uint64, +) (solana.PublicKey, solana.Instruction, error) { + // https://github.com/solana-labs/solana-web3.js/blob/c1c98715b0c7900ce37c59bffd2056fa0037213d/src/programs/address-lookup-table/index.ts#L274 + slotLE := make([]byte, 8) + binary.LittleEndian.PutUint64(slotLE, slot) + account, bumpSeed, err := solana.FindProgramAddress([][]byte{authority.Bytes(), slotLE}, AddressLookupTableProgram) + if err != nil { + return solana.PublicKey{}, nil, err + } + + data := binary.LittleEndian.AppendUint32([]byte{}, InstructionCreateLookupTable) + data = binary.LittleEndian.AppendUint64(data, slot) + data = append(data, bumpSeed) + return account, solana.NewInstruction( + AddressLookupTableProgram, + solana.AccountMetaSlice{ + solana.Meta(account).WRITE(), + solana.Meta(authority).SIGNER(), + solana.Meta(funder).SIGNER().WRITE(), + solana.Meta(solana.SystemProgramID), + }, + data, + ), nil +} + +func NewExtendLookupTableInstruction( + table, authority, funder solana.PublicKey, + accounts []solana.PublicKey, +) solana.Instruction { + // https://github.com/solana-labs/solana-web3.js/blob/c1c98715b0c7900ce37c59bffd2056fa0037213d/src/programs/address-lookup-table/index.ts#L113 + + data := binary.LittleEndian.AppendUint32([]byte{}, InstructionExtendLookupTable) + data = binary.LittleEndian.AppendUint64(data, uint64(len(accounts))) // note: this is usually u32 + 8 byte buffer + for _, a := range accounts { + data = append(data, a.Bytes()...) + } + + return solana.NewInstruction( + AddressLookupTableProgram, + solana.AccountMetaSlice{ + solana.Meta(table).WRITE(), + solana.Meta(authority).SIGNER(), + solana.Meta(funder).SIGNER().WRITE(), + solana.Meta(solana.SystemProgramID), + }, + data, + ) +} + +func FundAccounts(t *testing.T, accounts []solana.PrivateKey, solanaGoClient *rpc.Client) { + ctx := tests.Context(t) + sigs := []solana.Signature{} + for _, v := range accounts { + sig, err := solanaGoClient.RequestAirdrop(ctx, v.PublicKey(), 1000*solana.LAMPORTS_PER_SOL, rpc.CommitmentFinalized) + require.NoError(t, err) + sigs = append(sigs, sig) + } + + // wait for confirmation so later transactions don't fail + remaining := len(sigs) + count := 0 + for remaining > 0 { + count++ + statusRes, sigErr := solanaGoClient.GetSignatureStatuses(ctx, true, sigs...) + require.NoError(t, sigErr) + require.NotNil(t, statusRes) + require.NotNil(t, statusRes.Value) + + unconfirmedTxCount := 0 + for _, res := range statusRes.Value { + if res == nil || res.ConfirmationStatus == rpc.ConfirmationStatusProcessed || res.ConfirmationStatus == rpc.ConfirmationStatusConfirmed { + unconfirmedTxCount++ + } + } + remaining = unconfirmedTxCount + + time.Sleep(500 * time.Millisecond) + if count > 60 { + require.NoError(t, fmt.Errorf("unable to find transaction within timeout")) + } + } +} + +func SetupTestValidatorWithAnchorPrograms(t *testing.T, upgradeAuthority string, programs []string) (string, string) { + anchorData := struct { + Programs struct { + Localnet map[string]string + } + }{} + + // upload programs to validator + anchorBytes, err := os.ReadFile(PathToAnchorConfig) + require.NoError(t, err) + require.NoError(t, toml.Unmarshal(anchorBytes, &anchorData)) + + flags := []string{"--warp-slot", "42"} + for i := range programs { + k := programs[i] + v := anchorData.Programs.Localnet[k] + k = strings.Replace(k, "-", "_", -1) + flags = append(flags, "--upgradeable-program", v, filepath.Join(ContractsDir, k+".so"), upgradeAuthority) + } + rpcURL, wsURL := client.SetupLocalSolNodeWithFlags(t, flags...) + return rpcURL, wsURL +} + +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 := 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{ + NewExtendLookupTableInstruction( + table, sender.PublicKey(), sender.PublicKey(), + addresses, + ), + }, sender, rpc.CommitmentConfirmed) + + return table +} diff --git a/pkg/solana/chain.go b/pkg/solana/chain.go index 0b2d8e336..9850c17f7 100644 --- a/pkg/solana/chain.go +++ b/pkg/solana/chain.go @@ -23,18 +23,16 @@ import ( "github.com/smartcontractkit/chainlink-common/pkg/sqlutil" "github.com/smartcontractkit/chainlink-common/pkg/types" "github.com/smartcontractkit/chainlink-common/pkg/types/core" - "github.com/smartcontractkit/chainlink-common/pkg/utils" mn "github.com/smartcontractkit/chainlink-framework/multinode" - "github.com/smartcontractkit/chainlink-solana/pkg/solana/fees" - "github.com/smartcontractkit/chainlink-solana/pkg/solana/client" "github.com/smartcontractkit/chainlink-solana/pkg/solana/config" - "github.com/smartcontractkit/chainlink-solana/pkg/solana/internal" + "github.com/smartcontractkit/chainlink-solana/pkg/solana/fees" "github.com/smartcontractkit/chainlink-solana/pkg/solana/logpoller" "github.com/smartcontractkit/chainlink-solana/pkg/solana/monitor" "github.com/smartcontractkit/chainlink-solana/pkg/solana/txm" txmutils "github.com/smartcontractkit/chainlink-solana/pkg/solana/txm/utils" + "github.com/smartcontractkit/chainlink-solana/pkg/solana/utils" ) type LogPoller interface { @@ -101,6 +99,7 @@ var _ Chain = (*chain)(nil) type chain struct { services.StateMachine + stopCh services.StopChan id string cfg *config.TOMLConfig lp LogPoller @@ -248,14 +247,15 @@ func newChain(id string, cfg *config.TOMLConfig, ks core.Keystore, lggr logger.L lggr = logger.Named(lggr, "Chain") lggr = logger.With(lggr, "chainID", id, "chain", "solana") var ch = chain{ + stopCh: make(services.StopChan), id: id, cfg: cfg, lggr: lggr, clientCache: map[string]*verifiedCachedClient{}, } - var tc internal.Loader[client.ReaderWriter] = utils.NewLazyLoad(func() (client.ReaderWriter, error) { return ch.getClient() }) - var bc internal.Loader[monitor.BalanceClient] = utils.NewLazyLoad(func() (monitor.BalanceClient, error) { return ch.getClient() }) + var tc utils.Loader[client.ReaderWriter] = utils.NewLoader(func(ctx context.Context) (client.ReaderWriter, error) { return ch.getClient(ctx) }) + var bc utils.Loader[monitor.BalanceClient] = utils.NewLoader(func(ctx context.Context) (monitor.BalanceClient, error) { return ch.getClient(ctx) }) // getClient returns random client or if MultiNodeEnabled RPC picked and controlled by MultiNode ch.multiClient = client.NewMultiClient(ch.getClient) @@ -323,8 +323,8 @@ func newChain(id string, cfg *config.TOMLConfig, ks core.Keystore, lggr logger.L return sig, err } - tc = internal.NewLoader[client.ReaderWriter](func() (client.ReaderWriter, error) { return ch.multiNode.SelectRPC() }) - bc = internal.NewLoader[monitor.BalanceClient](func() (monitor.BalanceClient, error) { return ch.multiNode.SelectRPC() }) + tc = utils.NewOnceLoader[client.ReaderWriter](func(ctx context.Context) (client.ReaderWriter, error) { return ch.multiNode.SelectRPC(ctx) }) + bc = utils.NewOnceLoader[monitor.BalanceClient](func(ctx context.Context) (monitor.BalanceClient, error) { return ch.multiNode.SelectRPC(ctx) }) } ch.lp = logpoller.New(logger.Sugared(logger.Named(lggr, "LogPoller")), logpoller.NewORM(ch.ID(), ds, lggr), ch.multiClient) @@ -334,7 +334,7 @@ func newChain(id string, cfg *config.TOMLConfig, ks core.Keystore, lggr logger.L } func (c *chain) LatestHead(ctx context.Context) (types.Head, error) { - sc, err := c.getClient() + sc, err := c.getClient(ctx) if err != nil { return types.Head{}, err } @@ -430,7 +430,9 @@ func (c *chain) FeeEstimator() fees.Estimator { } func (c *chain) Reader() (client.Reader, error) { - return c.getClient() + ctx, cancel := c.stopCh.NewCtx() + defer cancel() + return c.getClient(ctx) } func (c *chain) ChainID() string { @@ -439,9 +441,9 @@ func (c *chain) ChainID() string { // getClient returns a client, randomly selecting one from available and valid nodes // If multinode is enabled, it will return a client using the multinode selection instead. -func (c *chain) getClient() (client.ReaderWriter, error) { +func (c *chain) getClient(ctx context.Context) (client.ReaderWriter, error) { if c.cfg.MultiNode.Enabled() { - return c.multiNode.SelectRPC() + return c.multiNode.SelectRPC(ctx) } var node *config.Node @@ -534,6 +536,7 @@ func (c *chain) Start(ctx context.Context) error { func (c *chain) Close() error { return c.StopOnce("Chain", func() error { + close(c.stopCh) c.lggr.Debug("Stopping") c.lggr.Debug("Stopping txm") c.lggr.Debug("Stopping balance monitor") diff --git a/pkg/solana/chain_test.go b/pkg/solana/chain_test.go index 56853e0f4..ef7c889b8 100644 --- a/pkg/solana/chain_test.go +++ b/pkg/solana/chain_test.go @@ -38,6 +38,7 @@ import ( const TestSolanaGenesisHashTemplate = `{"jsonrpc":"2.0","result":"%s","id":1}` func TestSolanaChain_GetClient(t *testing.T) { + ctx := tests.Context(t) checkOnce := map[string]struct{}{} mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { out := fmt.Sprintf(TestSolanaGenesisHashTemplate, client.MainnetGenesisHash) // mainnet genesis hash @@ -82,7 +83,7 @@ func TestSolanaChain_GetClient(t *testing.T) { URL: config.MustParseURL(mockServer.URL + "/2"), }, } - _, err := testChain.getClient() + _, err := testChain.getClient(ctx) assert.NoError(t, err) // random nodes (happy path, 1 valid + multiple invalid) @@ -108,12 +109,12 @@ func TestSolanaChain_GetClient(t *testing.T) { URL: config.MustParseURL(mockServer.URL + "/mismatch/4"), }, } - _, err = testChain.getClient() + _, err = testChain.getClient(ctx) assert.NoError(t, err) // empty nodes response cfg.Nodes = nil - _, err = testChain.getClient() + _, err = testChain.getClient(ctx) assert.Error(t, err) // no valid nodes to select from @@ -127,7 +128,7 @@ func TestSolanaChain_GetClient(t *testing.T) { URL: config.MustParseURL(mockServer.URL + "/mismatch/2"), }, } - _, err = testChain.getClient() + _, err = testChain.getClient(ctx) assert.NoError(t, err) } @@ -360,10 +361,11 @@ func TestSolanaChain_MultiNode_GetClient(t *testing.T) { servicetest.Run(t, testChain) + ctx := tests.Context(t) var selectedClient client.ReaderWriter require.Eventually(t, func() bool { var cerr error - selectedClient, cerr = testChain.getClient() + selectedClient, cerr = testChain.getClient(ctx) return cerr == nil }, time.Minute, time.Second, "failed to get a client") @@ -401,12 +403,12 @@ func TestChain_MultiNode_TransactionSender(t *testing.T) { servicetest.Run(t, c) require.Eventually(t, func() bool { - _, err := c.getClient() + _, err := c.getClient(ctx) return err == nil }, time.Minute, time.Second, "failed to get a client") t.Run("successful transaction", func(t *testing.T) { - cl, err := c.getClient() + cl, err := c.getClient(ctx) require.NoError(t, err) hash, hashErr := cl.LatestBlockhash(tests.Context(t)) @@ -442,7 +444,7 @@ func TestChain_MultiNode_TransactionSender(t *testing.T) { t.Run("unsigned transaction error", func(t *testing.T) { // create + sign transaction - cl, err := c.getClient() + cl, err := c.getClient(ctx) require.NoError(t, err) hash, hashErr := cl.LatestBlockhash(tests.Context(t)) @@ -477,6 +479,7 @@ func TestChain_MultiNode_TransactionSender(t *testing.T) { } func TestSolanaChain_MultiNode_Txm(t *testing.T) { + ctx := tests.Context(t) cfg := solcfg.NewDefault() cfg.MultiNode.MultiNode.Enabled = ptr(true) cfg.Nodes = []*solcfg.Node{ @@ -518,14 +521,14 @@ func TestSolanaChain_MultiNode_Txm(t *testing.T) { client.FundTestAccounts(t, []solana.PublicKey{pubKey}, cfg.Nodes[0].URL.String()) // track initial balance - selectedClient, err := testChain.getClient() + selectedClient, err := testChain.getClient(ctx) require.NoError(t, err) receiverBal, err := selectedClient.Balance(tests.Context(t), pubKeyReceiver) assert.NoError(t, err) assert.Equal(t, uint64(0), receiverBal) createTx := func(signer solana.PublicKey, sender solana.PublicKey, receiver solana.PublicKey, amt uint64) (*solana.Transaction, uint64) { - selectedClient, err = testChain.getClient() + selectedClient, err = testChain.getClient(ctx) assert.NoError(t, err) hash, hashErr := selectedClient.LatestBlockhash(tests.Context(t)) assert.NoError(t, hashErr) @@ -591,7 +594,7 @@ loop: } // verify funds were transferred through transaction sender - selectedClient, err = testChain.getClient() + selectedClient, err = testChain.getClient(ctx) assert.NoError(t, err) receiverBal, err = selectedClient.Balance(tests.Context(t), pubKeyReceiver) assert.NoError(t, err) diff --git a/pkg/solana/chainwriter/helpers.go b/pkg/solana/chainwriter/helpers.go index 6e2a3e5be..4f73a2a33 100644 --- a/pkg/solana/chainwriter/helpers.go +++ b/pkg/solana/chainwriter/helpers.go @@ -213,26 +213,3 @@ func CreateTestPubKeys(t *testing.T, num int) solana.PublicKeySlice { } 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/client/multi_client.go b/pkg/solana/client/multi_client.go index d5c1eaf72..0dc9cd24e 100644 --- a/pkg/solana/client/multi_client.go +++ b/pkg/solana/client/multi_client.go @@ -14,17 +14,17 @@ var _ ReaderWriter = (*MultiClient)(nil) // MultiClient - wrapper over multiple RPCs, underlying provider can be MultiNode or LazyLoader. // Main purpose is to eliminate need for frequent error handling on selection of a client. type MultiClient struct { - getClient func() (ReaderWriter, error) + getClient func(context.Context) (ReaderWriter, error) } -func NewMultiClient(getClient func() (ReaderWriter, error)) *MultiClient { +func NewMultiClient(getClient func(context.Context) (ReaderWriter, error)) *MultiClient { return &MultiClient{ getClient: getClient, } } func (m *MultiClient) GetLatestBlockHeight(ctx context.Context) (uint64, error) { - r, err := m.getClient() + r, err := m.getClient(ctx) if err != nil { return 0, err } @@ -33,7 +33,7 @@ func (m *MultiClient) GetLatestBlockHeight(ctx context.Context) (uint64, error) } func (m *MultiClient) SendTx(ctx context.Context, tx *solana.Transaction) (solana.Signature, error) { - r, err := m.getClient() + r, err := m.getClient(ctx) if err != nil { return solana.Signature{}, err } @@ -42,7 +42,7 @@ func (m *MultiClient) SendTx(ctx context.Context, tx *solana.Transaction) (solan } func (m *MultiClient) SimulateTx(ctx context.Context, tx *solana.Transaction, opts *rpc.SimulateTransactionOpts) (*rpc.SimulateTransactionResult, error) { - r, err := m.getClient() + r, err := m.getClient(ctx) if err != nil { return nil, err } @@ -51,7 +51,7 @@ func (m *MultiClient) SimulateTx(ctx context.Context, tx *solana.Transaction, op } func (m *MultiClient) SignatureStatuses(ctx context.Context, sigs []solana.Signature) ([]*rpc.SignatureStatusesResult, error) { - r, err := m.getClient() + r, err := m.getClient(ctx) if err != nil { return nil, err } @@ -60,7 +60,7 @@ func (m *MultiClient) SignatureStatuses(ctx context.Context, sigs []solana.Signa } func (m *MultiClient) GetAccountInfoWithOpts(ctx context.Context, addr solana.PublicKey, opts *rpc.GetAccountInfoOpts) (*rpc.GetAccountInfoResult, error) { - r, err := m.getClient() + r, err := m.getClient(ctx) if err != nil { return nil, err } @@ -69,7 +69,7 @@ func (m *MultiClient) GetAccountInfoWithOpts(ctx context.Context, addr solana.Pu } func (m *MultiClient) GetMultipleAccountsWithOpts(ctx context.Context, accounts []solana.PublicKey, opts *rpc.GetMultipleAccountsOpts) (out *rpc.GetMultipleAccountsResult, err error) { - r, err := m.getClient() + r, err := m.getClient(ctx) if err != nil { return nil, err } @@ -78,7 +78,7 @@ func (m *MultiClient) GetMultipleAccountsWithOpts(ctx context.Context, accounts } func (m *MultiClient) Balance(ctx context.Context, addr solana.PublicKey) (uint64, error) { - r, err := m.getClient() + r, err := m.getClient(ctx) if err != nil { return 0, err } @@ -87,7 +87,7 @@ func (m *MultiClient) Balance(ctx context.Context, addr solana.PublicKey) (uint6 } func (m *MultiClient) SlotHeight(ctx context.Context) (uint64, error) { - r, err := m.getClient() + r, err := m.getClient(ctx) if err != nil { return 0, err } @@ -96,7 +96,7 @@ func (m *MultiClient) SlotHeight(ctx context.Context) (uint64, error) { } func (m *MultiClient) LatestBlockhash(ctx context.Context) (*rpc.GetLatestBlockhashResult, error) { - r, err := m.getClient() + r, err := m.getClient(ctx) if err != nil { return nil, err } @@ -105,7 +105,7 @@ func (m *MultiClient) LatestBlockhash(ctx context.Context) (*rpc.GetLatestBlockh } func (m *MultiClient) ChainID(ctx context.Context) (mn.StringID, error) { - r, err := m.getClient() + r, err := m.getClient(ctx) if err != nil { return "", err } @@ -114,7 +114,7 @@ func (m *MultiClient) ChainID(ctx context.Context) (mn.StringID, error) { } func (m *MultiClient) GetFeeForMessage(ctx context.Context, msg string) (uint64, error) { - r, err := m.getClient() + r, err := m.getClient(ctx) if err != nil { return 0, err } @@ -123,7 +123,7 @@ func (m *MultiClient) GetFeeForMessage(ctx context.Context, msg string) (uint64, } func (m *MultiClient) GetLatestBlock(ctx context.Context) (*rpc.GetBlockResult, error) { - r, err := m.getClient() + r, err := m.getClient(ctx) if err != nil { return nil, err } @@ -132,7 +132,7 @@ func (m *MultiClient) GetLatestBlock(ctx context.Context) (*rpc.GetBlockResult, } func (m *MultiClient) GetTransaction(ctx context.Context, txHash solana.Signature) (*rpc.GetTransactionResult, error) { - r, err := m.getClient() + r, err := m.getClient(ctx) if err != nil { return nil, err } @@ -141,7 +141,7 @@ func (m *MultiClient) GetTransaction(ctx context.Context, txHash solana.Signatur } func (m *MultiClient) GetBlocks(ctx context.Context, startSlot uint64, endSlot *uint64) (rpc.BlocksResult, error) { - r, err := m.getClient() + r, err := m.getClient(ctx) if err != nil { return nil, err } @@ -150,7 +150,7 @@ func (m *MultiClient) GetBlocks(ctx context.Context, startSlot uint64, endSlot * } func (m *MultiClient) GetBlocksWithLimit(ctx context.Context, startSlot uint64, limit uint64) (*rpc.BlocksResult, error) { - r, err := m.getClient() + r, err := m.getClient(ctx) if err != nil { return nil, err } @@ -159,7 +159,7 @@ func (m *MultiClient) GetBlocksWithLimit(ctx context.Context, startSlot uint64, } func (m *MultiClient) GetBlock(ctx context.Context, slot uint64) (*rpc.GetBlockResult, error) { - r, err := m.getClient() + r, err := m.getClient(ctx) if err != nil { return nil, err } @@ -168,7 +168,7 @@ func (m *MultiClient) GetBlock(ctx context.Context, slot uint64) (*rpc.GetBlockR } func (m *MultiClient) GetSignaturesForAddressWithOpts(ctx context.Context, addr solana.PublicKey, opts *rpc.GetSignaturesForAddressOpts) ([]*rpc.TransactionSignature, error) { - r, err := m.getClient() + r, err := m.getClient(ctx) if err != nil { return nil, err } @@ -177,7 +177,7 @@ func (m *MultiClient) GetSignaturesForAddressWithOpts(ctx context.Context, addr } func (m *MultiClient) GetBlockWithOpts(ctx context.Context, slot uint64, opts *rpc.GetBlockOpts) (*rpc.GetBlockResult, error) { - r, err := m.getClient() + r, err := m.getClient(ctx) if err != nil { return nil, err } @@ -186,7 +186,7 @@ func (m *MultiClient) GetBlockWithOpts(ctx context.Context, slot uint64, opts *r } func (m *MultiClient) SlotHeightWithCommitment(ctx context.Context, commitment rpc.CommitmentType) (uint64, error) { - r, err := m.getClient() + r, err := m.getClient(ctx) if err != nil { return 0, err } diff --git a/pkg/solana/fees/block_history.go b/pkg/solana/fees/block_history.go index c4eb55b2e..70e39f075 100644 --- a/pkg/solana/fees/block_history.go +++ b/pkg/solana/fees/block_history.go @@ -11,7 +11,6 @@ import ( "github.com/smartcontractkit/chainlink-solana/pkg/solana/client" "github.com/smartcontractkit/chainlink-solana/pkg/solana/config" - "github.com/smartcontractkit/chainlink-solana/pkg/solana/internal" ) var _ Estimator = &blockHistoryEstimator{} @@ -23,7 +22,7 @@ type blockHistoryEstimator struct { chStop services.StopChan done sync.WaitGroup - client internal.Loader[client.ReaderWriter] + client func(context.Context) (client.ReaderWriter, error) cfg config.Config lgr logger.Logger @@ -34,7 +33,7 @@ type blockHistoryEstimator struct { // NewBlockHistoryEstimator creates a new fee estimator that parses historical fees from a fetched block // Note: getRecentPrioritizationFees is not used because it provides the lowest prioritization fee for an included tx in the block // which is not effective enough for increasing the chances of block inclusion -func NewBlockHistoryEstimator(c internal.Loader[client.ReaderWriter], cfg config.Config, lgr logger.Logger) (*blockHistoryEstimator, error) { +func NewBlockHistoryEstimator(c func(context.Context) (client.ReaderWriter, error), cfg config.Config, lgr logger.Logger) (*blockHistoryEstimator, error) { if cfg.BlockHistorySize() < 1 { return nil, fmt.Errorf("invalid block history depth: %d", cfg.BlockHistorySize()) } @@ -115,7 +114,7 @@ func (bhe *blockHistoryEstimator) calculatePrice(ctx context.Context) error { func (bhe *blockHistoryEstimator) calculatePriceFromLatestBlock(ctx context.Context) error { // fetch client - c, err := bhe.client.Get() + c, err := bhe.client(ctx) if err != nil { return fmt.Errorf("failed to get client: %w", err) } @@ -158,7 +157,7 @@ func (bhe *blockHistoryEstimator) calculatePriceFromLatestBlock(ctx context.Cont func (bhe *blockHistoryEstimator) calculatePriceFromMultipleBlocks(ctx context.Context, desiredBlockCount uint64) error { // fetch client - c, err := bhe.client.Get() + c, err := bhe.client(ctx) if err != nil { return fmt.Errorf("failed to get client: %w", err) } diff --git a/pkg/solana/fees/block_history_test.go b/pkg/solana/fees/block_history_test.go index 2cb92cc72..f9c6ab8a2 100644 --- a/pkg/solana/fees/block_history_test.go +++ b/pkg/solana/fees/block_history_test.go @@ -14,7 +14,6 @@ import ( "github.com/stretchr/testify/require" "github.com/smartcontractkit/chainlink-common/pkg/logger" - "github.com/smartcontractkit/chainlink-common/pkg/utils" "github.com/smartcontractkit/chainlink-common/pkg/utils/mathutil" "github.com/smartcontractkit/chainlink-common/pkg/utils/tests" @@ -27,9 +26,7 @@ func TestBlockHistoryEstimator_InvalidBlockHistorySize(t *testing.T) { // Setup invalidDepth := uint64(0) // Invalid value to trigger error rw := clientmock.NewReaderWriter(t) - rwLoader := utils.NewLazyLoad(func() (client.ReaderWriter, error) { - return rw, nil - }) + rwLoader := func(ctx context.Context) (client.ReaderWriter, error) { return rw, nil } cfg := cfgmock.NewConfig(t) cfg.On("BlockHistorySize").Return(invalidDepth) @@ -55,9 +52,7 @@ func TestBlockHistoryEstimator_LatestBlock(t *testing.T) { lastBlockMedianPrice, _ := mathutil.Median(lastBlockFeeData.Prices...) rw := clientmock.NewReaderWriter(t) - rwLoader := utils.NewLazyLoad(func() (client.ReaderWriter, error) { - return rw, nil - }) + rwLoader := func(ctx context.Context) (client.ReaderWriter, error) { return rw, nil } rw.On("GetLatestBlock", mock.Anything).Return(lastBlock, nil) t.Run("Successful Estimation", func(t *testing.T) { @@ -105,9 +100,7 @@ func TestBlockHistoryEstimator_LatestBlock(t *testing.T) { t.Run("Failed to Get Latest Block", func(t *testing.T) { // Setup rw := clientmock.NewReaderWriter(t) - rwLoader := utils.NewLazyLoad(func() (client.ReaderWriter, error) { - return rw, nil - }) + rwLoader := func(ctx context.Context) (client.ReaderWriter, error) { return rw, nil } cfg := cfgmock.NewConfig(t) setupConfigMock(cfg, defaultPrice, minPrice, pollPeriod, depth) rw.On("GetLatestBlock", mock.Anything).Return(nil, fmt.Errorf("fail rpc call")) // Mock GetLatestBlock returning error @@ -122,9 +115,7 @@ func TestBlockHistoryEstimator_LatestBlock(t *testing.T) { t.Run("Failed to Parse Block", func(t *testing.T) { // Setup rw := clientmock.NewReaderWriter(t) - rwLoader := utils.NewLazyLoad(func() (client.ReaderWriter, error) { - return rw, nil - }) + rwLoader := func(ctx context.Context) (client.ReaderWriter, error) { return rw, nil } cfg := cfgmock.NewConfig(t) setupConfigMock(cfg, defaultPrice, minPrice, pollPeriod, depth) rw.On("GetLatestBlock", mock.Anything).Return(nil, nil) // Mock GetLatestBlock returning nil @@ -139,9 +130,7 @@ func TestBlockHistoryEstimator_LatestBlock(t *testing.T) { t.Run("no compute unit prices collected", func(t *testing.T) { // Setup rw := clientmock.NewReaderWriter(t) - rwLoader := utils.NewLazyLoad(func() (client.ReaderWriter, error) { - return rw, nil - }) + rwLoader := func(ctx context.Context) (client.ReaderWriter, error) { return rw, nil } cfg := cfgmock.NewConfig(t) setupConfigMock(cfg, defaultPrice, minPrice, pollPeriod, depth) rw.On("GetLatestBlock", mock.Anything).Return(&rpc.GetBlockResult{}, nil) // Mock GetLatestBlock returning empty block @@ -155,10 +144,10 @@ func TestBlockHistoryEstimator_LatestBlock(t *testing.T) { t.Run("Failed to Get Client", func(t *testing.T) { // Setup - rwFailLoader := utils.NewLazyLoad(func() (client.ReaderWriter, error) { + rwFailLoader := func(ctx context.Context) (client.ReaderWriter, error) { // Return error to simulate failure to get client return nil, fmt.Errorf("fail client load") - }) + } cfg := cfgmock.NewConfig(t) setupConfigMock(cfg, defaultPrice, minPrice, pollPeriod, depth) estimator := initializeEstimator(ctx, t, rwFailLoader, cfg, logger.Test(t)) @@ -206,9 +195,7 @@ func TestBlockHistoryEstimator_MultipleBlocks(t *testing.T) { multipleBlocksAvg, _ := mathutil.Avg(testPrices...) rw := clientmock.NewReaderWriter(t) - rwLoader := utils.NewLazyLoad(func() (client.ReaderWriter, error) { - return rw, nil - }) + rwLoader := func(ctx context.Context) (client.ReaderWriter, error) { return rw, nil } rw.On("SlotHeight", mock.Anything).Return(testSlots[len(testSlots)-1], nil) rw.On("GetBlocksWithLimit", mock.Anything, mock.Anything, mock.Anything). Return(&testSlotsResult, nil) @@ -259,10 +246,10 @@ func TestBlockHistoryEstimator_MultipleBlocks(t *testing.T) { // Error handling scenarios t.Run("failed to get client", func(t *testing.T) { // Setup - rwFailLoader := utils.NewLazyLoad(func() (client.ReaderWriter, error) { + rwFailLoader := func(context.Context) (client.ReaderWriter, error) { // Return error to simulate failure to get client return nil, fmt.Errorf("fail client load") - }) + } cfg := cfgmock.NewConfig(t) setupConfigMock(cfg, defaultPrice, minPrice, pollPeriod, depth) estimator := initializeEstimator(ctx, t, rwFailLoader, cfg, logger.Test(t)) @@ -276,9 +263,7 @@ func TestBlockHistoryEstimator_MultipleBlocks(t *testing.T) { t.Run("failed to get current slot", func(t *testing.T) { // Setup rw := clientmock.NewReaderWriter(t) - rwLoader := utils.NewLazyLoad(func() (client.ReaderWriter, error) { - return rw, nil - }) + rwLoader := func(ctx context.Context) (client.ReaderWriter, error) { return rw, nil } cfg := cfgmock.NewConfig(t) setupConfigMock(cfg, defaultPrice, minPrice, pollPeriod, depth) rw.On("SlotHeight", mock.Anything).Return(uint64(0), fmt.Errorf("failed to get current slot")) // Mock SlotHeight returning error @@ -293,9 +278,7 @@ func TestBlockHistoryEstimator_MultipleBlocks(t *testing.T) { t.Run("current slot is less than desired block count", func(t *testing.T) { // Setup rw := clientmock.NewReaderWriter(t) - rwLoader := utils.NewLazyLoad(func() (client.ReaderWriter, error) { - return rw, nil - }) + rwLoader := func(ctx context.Context) (client.ReaderWriter, error) { return rw, nil } cfg := cfgmock.NewConfig(t) setupConfigMock(cfg, defaultPrice, minPrice, pollPeriod, depth) rw.On("SlotHeight", mock.Anything).Return(depth-1, nil) // Mock SlotHeight returning less than desiredBlockCount @@ -310,9 +293,7 @@ func TestBlockHistoryEstimator_MultipleBlocks(t *testing.T) { t.Run("failed to get blocks with limit", func(t *testing.T) { // Setup rw := clientmock.NewReaderWriter(t) - rwLoader := utils.NewLazyLoad(func() (client.ReaderWriter, error) { - return rw, nil - }) + rwLoader := func(ctx context.Context) (client.ReaderWriter, error) { return rw, nil } cfg := cfgmock.NewConfig(t) setupConfigMock(cfg, defaultPrice, minPrice, pollPeriod, depth) rw.On("SlotHeight", mock.Anything).Return(testSlots[len(testSlots)-1], nil) @@ -329,9 +310,7 @@ func TestBlockHistoryEstimator_MultipleBlocks(t *testing.T) { t.Run("no compute unit prices collected", func(t *testing.T) { // Setup rw := clientmock.NewReaderWriter(t) - rwLoader := utils.NewLazyLoad(func() (client.ReaderWriter, error) { - return rw, nil - }) + rwLoader := func(ctx context.Context) (client.ReaderWriter, error) { return rw, nil } cfg := cfgmock.NewConfig(t) setupConfigMock(cfg, defaultPrice, minPrice, pollPeriod, depth) rw.On("SlotHeight", mock.Anything).Return(testSlots[len(testSlots)-1], nil) @@ -356,7 +335,7 @@ func setupConfigMock(cfg *cfgmock.Config, defaultPrice uint64, minPrice uint64, } // initializeEstimator initializes, starts, and ensures cleanup of the BlockHistoryEstimator. -func initializeEstimator(ctx context.Context, t *testing.T, rwLoader *utils.LazyLoad[client.ReaderWriter], cfg *cfgmock.Config, lgr logger.Logger) *blockHistoryEstimator { +func initializeEstimator(ctx context.Context, t *testing.T, rwLoader func(context.Context) (client.ReaderWriter, error), cfg *cfgmock.Config, lgr logger.Logger) *blockHistoryEstimator { estimator, err := NewBlockHistoryEstimator(rwLoader, cfg, lgr) require.NoError(t, err, "Failed to create BlockHistoryEstimator") require.NoError(t, estimator.Start(ctx), "Failed to start BlockHistoryEstimator") diff --git a/pkg/solana/internal/loader.go b/pkg/solana/internal/loader.go deleted file mode 100644 index ba0bc5ee4..000000000 --- a/pkg/solana/internal/loader.go +++ /dev/null @@ -1,24 +0,0 @@ -package internal - -type Loader[T any] interface { - Get() (T, error) - Reset() -} - -var _ Loader[any] = (*loader[any])(nil) - -type loader[T any] struct { - getClient func() (T, error) -} - -func (c *loader[T]) Get() (T, error) { - return c.getClient() -} - -func (c *loader[T]) Reset() { /* do nothing */ } - -func NewLoader[T any](getClient func() (T, error)) *loader[T] { - return &loader[T]{ - getClient: getClient, - } -} diff --git a/pkg/solana/monitor/balance.go b/pkg/solana/monitor/balance.go index 10ea487db..923ad1cce 100644 --- a/pkg/solana/monitor/balance.go +++ b/pkg/solana/monitor/balance.go @@ -8,9 +8,9 @@ import ( "github.com/smartcontractkit/chainlink-common/pkg/logger" "github.com/smartcontractkit/chainlink-common/pkg/services" - "github.com/smartcontractkit/chainlink-common/pkg/utils" + "github.com/smartcontractkit/chainlink-common/pkg/timeutil" - "github.com/smartcontractkit/chainlink-solana/pkg/solana/internal" + "github.com/smartcontractkit/chainlink-solana/pkg/solana/utils" ) // Config defines the monitor configuration. @@ -28,11 +28,11 @@ type BalanceClient interface { } // NewBalanceMonitor returns a balance monitoring services.Service which reports the SOL balance of all ks keys to prometheus. -func NewBalanceMonitor(chainID string, cfg Config, lggr logger.Logger, ks Keystore, reader internal.Loader[BalanceClient]) services.Service { +func NewBalanceMonitor(chainID string, cfg Config, lggr logger.Logger, ks Keystore, reader utils.Loader[BalanceClient]) services.Service { return newBalanceMonitor(chainID, cfg, lggr, ks, reader) } -func newBalanceMonitor(chainID string, cfg Config, lggr logger.Logger, ks Keystore, reader internal.Loader[BalanceClient]) *balanceMonitor { +func newBalanceMonitor(chainID string, cfg Config, lggr logger.Logger, ks Keystore, reader utils.Loader[BalanceClient]) *balanceMonitor { b := balanceMonitor{ chainID: chainID, cfg: cfg, @@ -54,7 +54,7 @@ type balanceMonitor struct { ks Keystore updateFn func(acc solana.PublicKey, lamports uint64) // overridable for testing - reader internal.Loader[BalanceClient] + reader utils.Loader[BalanceClient] stop services.StopChan done chan struct{} @@ -88,14 +88,15 @@ func (b *balanceMonitor) monitor() { ctx, cancel := b.stop.NewCtx() defer cancel() - tick := time.After(utils.WithJitter(b.cfg.BalancePollPeriod())) + ticker := timeutil.NewTicker(b.cfg.BalancePollPeriod) + defer ticker.Stop() for { select { case <-b.stop: return - case <-tick: + case <-ticker.C: b.updateBalances(ctx) - tick = time.After(utils.WithJitter(b.cfg.BalancePollPeriod())) + ticker.Reset() } } } @@ -112,7 +113,7 @@ func (b *balanceMonitor) updateBalances(ctx context.Context) { if len(keys) == 0 { return } - reader, err := b.reader.Get() + reader, err := b.reader.Get(ctx) if err != nil { b.lggr.Errorw("Failed to get client", "err", err) return diff --git a/pkg/solana/monitor/balance_test.go b/pkg/solana/monitor/balance_test.go index a6cc231c9..b76040a7b 100644 --- a/pkg/solana/monitor/balance_test.go +++ b/pkg/solana/monitor/balance_test.go @@ -18,6 +18,7 @@ import ( "github.com/smartcontractkit/chainlink-solana/pkg/solana/client/mocks" "github.com/smartcontractkit/chainlink-solana/pkg/solana/internal" + "github.com/smartcontractkit/chainlink-solana/pkg/solana/utils" ) func TestBalanceMonitor(t *testing.T) { @@ -63,7 +64,7 @@ func TestBalanceMonitor(t *testing.T) { } } - b.reader = internal.NewLoader[BalanceClient](func() (BalanceClient, error) { + b.reader = utils.NewOnceLoader[BalanceClient](func(context.Context) (BalanceClient, error) { return client, nil }) diff --git a/pkg/solana/txm/txm.go b/pkg/solana/txm/txm.go index 17f17f093..19fba84a0 100644 --- a/pkg/solana/txm/txm.go +++ b/pkg/solana/txm/txm.go @@ -18,15 +18,15 @@ import ( "github.com/smartcontractkit/chainlink-common/pkg/loop" "github.com/smartcontractkit/chainlink-common/pkg/services" commontypes "github.com/smartcontractkit/chainlink-common/pkg/types" - "github.com/smartcontractkit/chainlink-common/pkg/utils" + commonutils "github.com/smartcontractkit/chainlink-common/pkg/utils" bigmath "github.com/smartcontractkit/chainlink-common/pkg/utils/big_math" "github.com/smartcontractkit/chainlink-common/pkg/utils/mathutil" "github.com/smartcontractkit/chainlink-solana/pkg/solana/client" "github.com/smartcontractkit/chainlink-solana/pkg/solana/config" "github.com/smartcontractkit/chainlink-solana/pkg/solana/fees" - "github.com/smartcontractkit/chainlink-solana/pkg/solana/internal" txmutils "github.com/smartcontractkit/chainlink-solana/pkg/solana/txm/utils" + "github.com/smartcontractkit/chainlink-solana/pkg/solana/utils" ) const ( @@ -65,7 +65,7 @@ type Txm struct { cfg config.Config txs PendingTxContext ks SimpleKeystore - client internal.Loader[client.ReaderWriter] + client utils.Loader[client.ReaderWriter] fee fees.Estimator // sendTx is an override for sending transactions rather than using a single client // Enabling MultiNode uses this function to send transactions to all RPCs @@ -73,13 +73,13 @@ type Txm struct { } // NewTxm creates a txm. Uses simulation so should only be used to send txes to trusted contracts i.e. OCR. -func NewTxm(chainID string, client internal.Loader[client.ReaderWriter], +func NewTxm(chainID string, client utils.Loader[client.ReaderWriter], sendTx func(ctx context.Context, tx *solanaGo.Transaction) (solanaGo.Signature, error), cfg config.Config, ks SimpleKeystore, lggr logger.Logger) *Txm { if sendTx == nil { // default sendTx using a single RPC sendTx = func(ctx context.Context, tx *solanaGo.Transaction) (solanaGo.Signature, error) { - c, err := client.Get() + c, err := client.Get(ctx) if err != nil { return solanaGo.Signature{}, err } @@ -110,7 +110,7 @@ func (txm *Txm) Start(ctx context.Context) error { case "fixed": estimator, err = fees.NewFixedPriceEstimator(txm.cfg) case "blockhistory": - estimator, err = fees.NewBlockHistoryEstimator(txm.client, txm.cfg, txm.lggr) + estimator, err = fees.NewBlockHistoryEstimator(txm.client.Get, txm.cfg, txm.lggr) default: err = fmt.Errorf("unknown solana fee estimator type: %s", txm.cfg.FeeEstimatorMode()) } @@ -379,18 +379,19 @@ func (txm *Txm) confirm() { ctx, cancel := txm.chStop.NewCtx() defer cancel() - tick := time.After(0) + ticker := services.NewTicker(txm.cfg.ConfirmPollPeriod()) + defer ticker.Stop() for { select { case <-ctx.Done(): return - case <-tick: + case <-ticker.C: // If no signatures to confirm, we can break loop as there's nothing to process. if txm.InflightTxs() == 0 { break } - client, err := txm.client.Get() + client, err := txm.client.Get(ctx) if err != nil { txm.lggr.Errorw("failed to get client in txm.confirm", "error", err) break @@ -400,7 +401,7 @@ func (txm *Txm) confirm() { txm.rebroadcastExpiredTxs(ctx, client) } } - tick = time.After(utils.WithJitter(txm.cfg.ConfirmPollPeriod())) + ticker.Reset() } } @@ -409,7 +410,7 @@ func (txm *Txm) confirm() { // It handles various scenarios including expirations, errors, and state transitions (broadcasted, processed, confirmed, finalized). // Additionally, it detects and manages re-orgs by removing or rebroadcasting transactions as necessary and determines when to end polling cancelling retry loops. func (txm *Txm) processConfirmations(ctx context.Context, client client.ReaderWriter) { - sigsBatch, err := utils.BatchSplit(txm.txs.ListAllSigs(), MaxSigsToConfirm) + sigsBatch, err := commonutils.BatchSplit(txm.txs.ListAllSigs(), MaxSigsToConfirm) if err != nil { // this should never happen txm.lggr.Fatalw("failed to batch signatures", "error", err) return @@ -686,18 +687,19 @@ func (txm *Txm) reap() { ctx, cancel := txm.chStop.NewCtx() defer cancel() - tick := time.After(0) + ticker := services.NewTicker(TxReapInterval) + defer ticker.Stop() for { select { case <-ctx.Done(): return - case <-tick: + case <-ticker.C: reapCount := txm.txs.TrimFinalizedErroredTxs() if reapCount > 0 { txm.lggr.Debugf("Reaped %d finalized or errored transactions", reapCount) } } - tick = time.After(utils.WithJitter(TxReapInterval)) + ticker.Reset() } } @@ -856,7 +858,7 @@ func (txm *Txm) EstimateComputeUnitLimit(ctx context.Context, tx *solanaGo.Trans // simulateTx simulates transactions using the SimulateTx client method func (txm *Txm) simulateTx(ctx context.Context, tx *solanaGo.Transaction) (res *rpc.SimulateTransactionResult, err error) { // get client - client, err := txm.client.Get() + client, err := txm.client.Get(ctx) if err != nil { txm.lggr.Errorw("failed to get client", "error", err) return diff --git a/pkg/solana/txm/txm_integration_test.go b/pkg/solana/txm/txm_integration_test.go index 1f4275982..59c3451bd 100644 --- a/pkg/solana/txm/txm_integration_test.go +++ b/pkg/solana/txm/txm_integration_test.go @@ -22,8 +22,9 @@ import ( "github.com/smartcontractkit/chainlink-common/pkg/logger" "github.com/smartcontractkit/chainlink-common/pkg/services/servicetest" "github.com/smartcontractkit/chainlink-common/pkg/types" - "github.com/smartcontractkit/chainlink-common/pkg/utils" + commonutils "github.com/smartcontractkit/chainlink-common/pkg/utils" "github.com/smartcontractkit/chainlink-common/pkg/utils/tests" + "github.com/smartcontractkit/chainlink-solana/pkg/solana/utils" relayconfig "github.com/smartcontractkit/chainlink-common/pkg/config" @@ -149,7 +150,7 @@ func setup(t *testing.T, url string, txExpirationRebroadcast bool) (context.Cont lggr, obs := logger.TestObserved(t, zapcore.DebugLevel) client, err := solanaClient.NewClient(url, cfg, 2*time.Second, lggr) require.NoError(t, err) - loader := utils.NewLazyLoad(func() (solanaClient.ReaderWriter, error) { return client, nil }) + loader := utils.NewStaticLoader[solanaClient.ReaderWriter](client) txmInstance := NewTxm("localnet", loader, nil, cfg, mkey, lggr) servicetest.Run(t, txmInstance) @@ -230,8 +231,8 @@ func TestTxm_Integration_Reorg(t *testing.T) { // Start live validator and setup test environment t.Parallel() ledgerDir := t.TempDir() - port := utils.MustRandomPort(t) - faucetPort := utils.MustRandomPort(t) + port := commonutils.MustRandomPort(t) + faucetPort := commonutils.MustRandomPort(t) cmd, url := startValidator(t, ledgerDir, port, faucetPort, true) ctx, cl, txmInstance, senderPubKey, receiverPubKey, obs := setup(t, url, true) diff --git a/pkg/solana/txm/txm_internal_test.go b/pkg/solana/txm/txm_internal_test.go index 0ef477f95..d0229a63e 100644 --- a/pkg/solana/txm/txm_internal_test.go +++ b/pkg/solana/txm/txm_internal_test.go @@ -20,20 +20,20 @@ import ( "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" - "github.com/smartcontractkit/chainlink-solana/pkg/solana/client" - "github.com/smartcontractkit/chainlink-solana/pkg/solana/client/mocks" - "github.com/smartcontractkit/chainlink-solana/pkg/solana/config" - "github.com/smartcontractkit/chainlink-solana/pkg/solana/fees" - keyMocks "github.com/smartcontractkit/chainlink-solana/pkg/solana/txm/mocks" - txmutils "github.com/smartcontractkit/chainlink-solana/pkg/solana/txm/utils" - relayconfig "github.com/smartcontractkit/chainlink-common/pkg/config" "github.com/smartcontractkit/chainlink-common/pkg/logger" "github.com/smartcontractkit/chainlink-common/pkg/types" commontypes "github.com/smartcontractkit/chainlink-common/pkg/types" - "github.com/smartcontractkit/chainlink-common/pkg/utils" bigmath "github.com/smartcontractkit/chainlink-common/pkg/utils/big_math" "github.com/smartcontractkit/chainlink-common/pkg/utils/tests" + + "github.com/smartcontractkit/chainlink-solana/pkg/solana/client" + "github.com/smartcontractkit/chainlink-solana/pkg/solana/client/mocks" + "github.com/smartcontractkit/chainlink-solana/pkg/solana/config" + "github.com/smartcontractkit/chainlink-solana/pkg/solana/fees" + keyMocks "github.com/smartcontractkit/chainlink-solana/pkg/solana/txm/mocks" + txmutils "github.com/smartcontractkit/chainlink-solana/pkg/solana/txm/utils" + "github.com/smartcontractkit/chainlink-solana/pkg/solana/utils" ) type soltxmProm struct { @@ -136,7 +136,7 @@ func TestTxm(t *testing.T) { mkey := keyMocks.NewSimpleKeystore(t) mkey.On("Sign", mock.Anything, mock.Anything, mock.Anything).Return([]byte{}, nil) - loader := utils.NewLazyLoad(func() (client.ReaderWriter, error) { return mc, nil }) + loader := utils.NewStaticLoader[client.ReaderWriter](mc) txm := NewTxm(id, loader, nil, cfg, mkey, lggr) require.NoError(t, txm.Start(ctx)) t.Cleanup(func() { require.NoError(t, txm.Close()) }) @@ -799,7 +799,7 @@ func TestTxm_disabled_confirm_timeout_with_retention(t *testing.T) { mkey := keyMocks.NewSimpleKeystore(t) mkey.On("Sign", mock.Anything, mock.Anything, mock.Anything).Return([]byte{}, nil) - loader := utils.NewLazyLoad(func() (client.ReaderWriter, error) { return mc, nil }) + loader := utils.NewStaticLoader[client.ReaderWriter](mc) txm := NewTxm(id, loader, nil, cfg, mkey, lggr) require.NoError(t, txm.Start(ctx)) t.Cleanup(func() { require.NoError(t, txm.Close()) }) @@ -998,7 +998,7 @@ func TestTxm_compute_unit_limit_estimation(t *testing.T) { mkey := keyMocks.NewSimpleKeystore(t) mkey.On("Sign", mock.Anything, mock.Anything, mock.Anything).Return([]byte{}, nil) - loader := utils.NewLazyLoad(func() (client.ReaderWriter, error) { return mc, nil }) + loader := utils.NewStaticLoader[client.ReaderWriter](mc) txm := NewTxm(id, loader, nil, cfg, mkey, lggr) require.NoError(t, txm.Start(ctx)) t.Cleanup(func() { require.NoError(t, txm.Close()) }) @@ -1175,7 +1175,7 @@ func TestTxm_Enqueue(t *testing.T) { ) require.NoError(t, err) - loader := utils.NewLazyLoad(func() (client.ReaderWriter, error) { return mc, nil }) + loader := utils.NewStaticLoader[client.ReaderWriter](mc) txm := NewTxm("enqueue_test", loader, nil, cfg, mkey, lggr) require.ErrorContains(t, txm.Enqueue(ctx, "txmUnstarted", &solana.Transaction{}, nil, lastValidBlockHeight), "not started") @@ -1284,7 +1284,7 @@ func TestTxm_ExpirationRebroadcast(t *testing.T) { mkey := keyMocks.NewSimpleKeystore(t) mkey.On("Sign", mock.Anything, mock.Anything, mock.Anything).Return([]byte{}, nil) - loader := utils.NewLazyLoad(func() (client.ReaderWriter, error) { return mc, nil }) + loader := utils.NewStaticLoader[client.ReaderWriter](mc) txm := NewTxm(id, loader, nil, cfg, mkey, lggr) require.NoError(t, txm.Start(ctx)) t.Cleanup(func() { require.NoError(t, txm.Close()) }) @@ -1669,7 +1669,7 @@ func TestTxm_OnReorg(t *testing.T) { mkey := keyMocks.NewSimpleKeystore(t) mkey.On("Sign", mock.Anything, mock.Anything, mock.Anything).Return([]byte{}, nil) - loader := utils.NewLazyLoad(func() (client.ReaderWriter, error) { return mc, nil }) + loader := utils.NewStaticLoader[client.ReaderWriter](mc) txm := NewTxm(id, loader, nil, cfg, mkey, lggr) require.NoError(t, txm.Start(ctx)) t.Cleanup(func() { require.NoError(t, txm.Close()) }) diff --git a/pkg/solana/txm/txm_load_test.go b/pkg/solana/txm/txm_load_test.go index 333c95e23..b3cb76cd7 100644 --- a/pkg/solana/txm/txm_load_test.go +++ b/pkg/solana/txm/txm_load_test.go @@ -14,16 +14,16 @@ import ( "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" - solanaClient "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" - relayconfig "github.com/smartcontractkit/chainlink-common/pkg/config" "github.com/smartcontractkit/chainlink-common/pkg/logger" "github.com/smartcontractkit/chainlink-common/pkg/services/servicetest" - "github.com/smartcontractkit/chainlink-common/pkg/utils" "github.com/smartcontractkit/chainlink-common/pkg/utils/tests" + + solanaClient "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" ) func TestTxm_Integration(t *testing.T) { @@ -71,7 +71,7 @@ func TestTxm_Integration(t *testing.T) { cfg.Chain.FeeEstimatorMode = &estimator client, err := solanaClient.NewClient(url, cfg, 2*time.Second, lggr) require.NoError(t, err) - loader := utils.NewLazyLoad(func() (solanaClient.ReaderWriter, error) { return client, nil }) + loader := utils.NewStaticLoader[solanaClient.ReaderWriter](client) txm := txm.NewTxm("localnet", loader, nil, cfg, mkey, lggr) // track initial balance diff --git a/pkg/solana/txm/txm_race_test.go b/pkg/solana/txm/txm_race_test.go index 33ec0f7bf..475263fd9 100644 --- a/pkg/solana/txm/txm_race_test.go +++ b/pkg/solana/txm/txm_race_test.go @@ -9,10 +9,12 @@ import ( "time" solanaGo "github.com/gagliardetto/solana-go" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" "go.uber.org/zap/zapcore" "github.com/smartcontractkit/chainlink-common/pkg/logger" - "github.com/smartcontractkit/chainlink-common/pkg/utils" "github.com/smartcontractkit/chainlink-common/pkg/utils/tests" solanaClient "github.com/smartcontractkit/chainlink-solana/pkg/solana/client" @@ -21,10 +23,7 @@ import ( "github.com/smartcontractkit/chainlink-solana/pkg/solana/fees" feemocks "github.com/smartcontractkit/chainlink-solana/pkg/solana/fees/mocks" ksmocks "github.com/smartcontractkit/chainlink-solana/pkg/solana/txm/mocks" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/mock" - "github.com/stretchr/testify/require" + "github.com/smartcontractkit/chainlink-solana/pkg/solana/utils" ) func NewTestMsg() (msg pendingTx) { @@ -64,9 +63,7 @@ func TestTxm_SendWithRetry_Race(t *testing.T) { msg := NewTestMsg() testRunner := func(t *testing.T, client solanaClient.ReaderWriter) { // build minimal txm - loader := utils.NewLazyLoad(func() (solanaClient.ReaderWriter, error) { - return client, nil - }) + loader := utils.NewStaticLoader(client) txm := NewTxm("retry_race", loader, nil, cfg, ks, lggr) txm.fee = fee diff --git a/pkg/solana/txm/txm_unit_test.go b/pkg/solana/txm/txm_unit_test.go index e8dfcb584..6ed62233a 100644 --- a/pkg/solana/txm/txm_unit_test.go +++ b/pkg/solana/txm/txm_unit_test.go @@ -19,9 +19,9 @@ import ( solanatxm "github.com/smartcontractkit/chainlink-solana/pkg/solana/txm" keyMocks "github.com/smartcontractkit/chainlink-solana/pkg/solana/txm/mocks" txmutils "github.com/smartcontractkit/chainlink-solana/pkg/solana/txm/utils" + "github.com/smartcontractkit/chainlink-solana/pkg/solana/utils" "github.com/smartcontractkit/chainlink-common/pkg/logger" - "github.com/smartcontractkit/chainlink-common/pkg/utils" bigmath "github.com/smartcontractkit/chainlink-common/pkg/utils/big_math" "github.com/smartcontractkit/chainlink-common/pkg/utils/tests" ) @@ -48,7 +48,7 @@ func TestTxm_EstimateComputeUnitLimit(t *testing.T) { lggr := logger.Test(t) cfg := config.NewDefault() client := clientmocks.NewReaderWriter(t) - loader := utils.NewLazyLoad(func() (solanaClient.ReaderWriter, error) { return client, nil }) + loader := utils.NewStaticLoader[solanaClient.ReaderWriter](client) txm := solanatxm.NewTxm("localnet", loader, nil, cfg, mkey, lggr) t.Run("successfully sets estimated compute unit limit", func(t *testing.T) { @@ -162,7 +162,7 @@ func TestTxm_ProcessError(t *testing.T) { lggr := logger.Test(t) cfg := config.NewDefault() client := clientmocks.NewReaderWriter(t) - loader := utils.NewLazyLoad(func() (solanaClient.ReaderWriter, error) { return client, nil }) + loader := utils.NewStaticLoader[solanaClient.ReaderWriter](client) txm := solanatxm.NewTxm("localnet", loader, nil, cfg, mkey, lggr) t.Run("process BlockhashNotFound error", func(t *testing.T) { diff --git a/pkg/solana/utils/loader.go b/pkg/solana/utils/loader.go new file mode 100644 index 000000000..c26543d03 --- /dev/null +++ b/pkg/solana/utils/loader.go @@ -0,0 +1,78 @@ +package utils + +import ( + "context" + "sync" +) + +type Loader[T any] interface { + Get(context.Context) (T, error) + Reset() +} + +var _ Loader[any] = (*onceLoader[any])(nil) + +type onceLoader[T any] struct { + getClient func(ctx context.Context) (T, error) +} + +func (c *onceLoader[T]) Get(ctx context.Context) (T, error) { + return c.getClient(ctx) +} + +func (c *onceLoader[T]) Reset() { /* do nothing */ } + +func NewOnceLoader[T any](getClient func(ctx context.Context) (T, error)) *onceLoader[T] { + return &onceLoader[T]{ + getClient: getClient, + } +} + +var _ Loader[any] = (*loader[any])(nil) + +type loader[T any] struct { + getClient func(ctx context.Context) (T, error) + state T + ok bool + lock sync.Mutex +} + +func (c *loader[T]) Get(ctx context.Context) (out T, err error) { + c.lock.Lock() + defer c.lock.Unlock() + + if c.ok { + return c.state, nil + } + c.state, err = c.getClient(ctx) + c.ok = err == nil + return c.state, err +} + +func (c *loader[T]) Reset() { + c.lock.Lock() + defer c.lock.Unlock() + c.ok = false +} + +func NewLoader[T any](getClient func(ctx context.Context) (T, error)) *loader[T] { + return &loader[T]{ + getClient: getClient, + } +} + +var _ Loader[any] = (*staticLoader[any])(nil) + +type staticLoader[T any] struct { + val T +} + +func (s *staticLoader[T]) Get(ctx context.Context) (T, error) { + return s.val, nil +} + +func (s *staticLoader[T]) Reset() {} + +func NewStaticLoader[T any](v T) Loader[T] { + return &staticLoader[T]{v} +} diff --git a/pkg/solana/internal/loader_test.go b/pkg/solana/utils/loader_test.go similarity index 56% rename from pkg/solana/internal/loader_test.go rename to pkg/solana/utils/loader_test.go index 8d17a27ea..16ddd09dc 100644 --- a/pkg/solana/internal/loader_test.go +++ b/pkg/solana/utils/loader_test.go @@ -1,9 +1,12 @@ -package internal +package utils import ( + "context" "testing" "github.com/stretchr/testify/require" + + "github.com/smartcontractkit/chainlink-common/pkg/utils/tests" ) type testLoader struct { @@ -11,23 +14,24 @@ type testLoader struct { callCount int } -func (t *testLoader) load() (any, error) { +func (t *testLoader) load(context.Context) (any, error) { t.callCount++ return nil, nil } func newTestLoader() *testLoader { loader := testLoader{} - loader.Loader = NewLoader[any](loader.load) + loader.Loader = NewOnceLoader[any](loader.load) return &loader } func TestLoader(t *testing.T) { t.Run("direct loading", func(t *testing.T) { + ctx := tests.Context(t) loader := newTestLoader() - _, _ = loader.Get() - _, _ = loader.Get() - _, _ = loader.Get() + _, _ = loader.Get(ctx) + _, _ = loader.Get(ctx) + _, _ = loader.Get(ctx) require.Equal(t, 3, loader.callCount) }) } diff --git a/pkg/solana/utils/utils.go b/pkg/solana/utils/utils.go index 764c236de..24075b6f0 100644 --- a/pkg/solana/utils/utils.go +++ b/pkg/solana/utils/utils.go @@ -2,23 +2,16 @@ package utils import ( "context" - "encoding/binary" "fmt" - "os" "path/filepath" "runtime" - "strings" "testing" "time" "github.com/gagliardetto/solana-go" "github.com/gagliardetto/solana-go/rpc" - "github.com/pelletier/go-toml/v2" "github.com/stretchr/testify/require" - "github.com/smartcontractkit/chainlink-common/pkg/utils/tests" - - "github.com/smartcontractkit/chainlink-solana/pkg/solana/client" "github.com/smartcontractkit/chainlink-solana/pkg/solana/internal" ) @@ -27,8 +20,7 @@ var ( // 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") + ContractsDir = filepath.Join(ProjectRoot, "contracts", "target", "deploy") ) func LamportsToSol(lamports uint64) float64 { return internal.LamportsToSol(lamports) } @@ -97,123 +89,3 @@ func sendTransaction(ctx context.Context, rpcClient *rpc.Client, t *testing.T, i require.NoError(t, err) return txres } - -var ( - AddressLookupTableProgram = solana.MustPublicKeyFromBase58("AddressLookupTab1e1111111111111111111111111") -) - -const ( - InstructionCreateLookupTable uint32 = iota - InstructionFreezeLookupTable - InstructionExtendLookupTable - InstructionDeactiveLookupTable - InstructionCloseLookupTable -) - -func NewCreateLookupTableInstruction( - authority, funder solana.PublicKey, - slot uint64, -) (solana.PublicKey, solana.Instruction, error) { - // https://github.com/solana-labs/solana-web3.js/blob/c1c98715b0c7900ce37c59bffd2056fa0037213d/src/programs/address-lookup-table/index.ts#L274 - slotLE := make([]byte, 8) - binary.LittleEndian.PutUint64(slotLE, slot) - account, bumpSeed, err := solana.FindProgramAddress([][]byte{authority.Bytes(), slotLE}, AddressLookupTableProgram) - if err != nil { - return solana.PublicKey{}, nil, err - } - - data := binary.LittleEndian.AppendUint32([]byte{}, InstructionCreateLookupTable) - data = binary.LittleEndian.AppendUint64(data, slot) - data = append(data, bumpSeed) - return account, solana.NewInstruction( - AddressLookupTableProgram, - solana.AccountMetaSlice{ - solana.Meta(account).WRITE(), - solana.Meta(authority).SIGNER(), - solana.Meta(funder).SIGNER().WRITE(), - solana.Meta(solana.SystemProgramID), - }, - data, - ), nil -} - -func NewExtendLookupTableInstruction( - table, authority, funder solana.PublicKey, - accounts []solana.PublicKey, -) solana.Instruction { - // https://github.com/solana-labs/solana-web3.js/blob/c1c98715b0c7900ce37c59bffd2056fa0037213d/src/programs/address-lookup-table/index.ts#L113 - - data := binary.LittleEndian.AppendUint32([]byte{}, InstructionExtendLookupTable) - data = binary.LittleEndian.AppendUint64(data, uint64(len(accounts))) // note: this is usually u32 + 8 byte buffer - for _, a := range accounts { - data = append(data, a.Bytes()...) - } - - return solana.NewInstruction( - AddressLookupTableProgram, - solana.AccountMetaSlice{ - solana.Meta(table).WRITE(), - solana.Meta(authority).SIGNER(), - solana.Meta(funder).SIGNER().WRITE(), - solana.Meta(solana.SystemProgramID), - }, - data, - ) -} - -func FundAccounts(t *testing.T, accounts []solana.PrivateKey, solanaGoClient *rpc.Client) { - ctx := tests.Context(t) - sigs := []solana.Signature{} - for _, v := range accounts { - sig, err := solanaGoClient.RequestAirdrop(ctx, v.PublicKey(), 1000*solana.LAMPORTS_PER_SOL, rpc.CommitmentFinalized) - require.NoError(t, err) - sigs = append(sigs, sig) - } - - // wait for confirmation so later transactions don't fail - remaining := len(sigs) - count := 0 - for remaining > 0 { - count++ - statusRes, sigErr := solanaGoClient.GetSignatureStatuses(ctx, true, sigs...) - require.NoError(t, sigErr) - require.NotNil(t, statusRes) - require.NotNil(t, statusRes.Value) - - unconfirmedTxCount := 0 - for _, res := range statusRes.Value { - if res == nil || res.ConfirmationStatus == rpc.ConfirmationStatusProcessed || res.ConfirmationStatus == rpc.ConfirmationStatusConfirmed { - unconfirmedTxCount++ - } - } - remaining = unconfirmedTxCount - - time.Sleep(500 * time.Millisecond) - if count > 60 { - require.NoError(t, fmt.Errorf("unable to find transaction within timeout")) - } - } -} - -func SetupTestValidatorWithAnchorPrograms(t *testing.T, upgradeAuthority string, programs []string) (string, string) { - anchorData := struct { - Programs struct { - Localnet map[string]string - } - }{} - - // upload programs to validator - anchorBytes, err := os.ReadFile(PathToAnchorConfig) - require.NoError(t, err) - require.NoError(t, toml.Unmarshal(anchorBytes, &anchorData)) - - flags := []string{"--warp-slot", "42"} - for i := range programs { - k := programs[i] - v := anchorData.Programs.Localnet[k] - k = strings.Replace(k, "-", "_", -1) - flags = append(flags, "--upgradeable-program", v, filepath.Join(ContractsDir, k+".so"), upgradeAuthority) - } - rpcURL, wsURL := client.SetupLocalSolNodeWithFlags(t, flags...) - return rpcURL, wsURL -}