diff --git a/core/chains/solana/chain.go b/core/chains/solana/chain.go index 273ded3626f..fe1cc7e57bb 100644 --- a/core/chains/solana/chain.go +++ b/core/chains/solana/chain.go @@ -17,10 +17,11 @@ import ( solanaclient "github.com/smartcontractkit/chainlink-solana/pkg/solana/client" "github.com/smartcontractkit/chainlink-solana/pkg/solana/config" "github.com/smartcontractkit/chainlink-solana/pkg/solana/db" + soltxm "github.com/smartcontractkit/chainlink-solana/pkg/solana/txm" + v2 "github.com/smartcontractkit/chainlink/core/config/v2" "github.com/smartcontractkit/chainlink/core/chains/solana/monitor" - "github.com/smartcontractkit/chainlink/core/chains/solana/soltxm" "github.com/smartcontractkit/chainlink/core/logger" "github.com/smartcontractkit/chainlink/core/services" "github.com/smartcontractkit/chainlink/core/services/keystore" diff --git a/core/chains/solana/fees/computebudget.go b/core/chains/solana/fees/computebudget.go deleted file mode 100644 index f0c4a1ec2b2..00000000000 --- a/core/chains/solana/fees/computebudget.go +++ /dev/null @@ -1,121 +0,0 @@ -package fees - -import ( - "bytes" - "encoding/binary" - - "github.com/gagliardetto/solana-go" -) - -// https://github.com/solana-labs/solana/blob/60858d043ca612334de300805d93ea3014e8ab37/sdk/src/compute_budget.rs#L25 -const ( - // deprecated: will not support for building instruction - Instruction_RequestUnitsDeprecated uint8 = iota - - // Request a specific transaction-wide program heap region size in bytes. - // The value requested must be a multiple of 1024. This new heap region - // size applies to each program executed in the transaction, including all - // calls to CPIs. - // note: uses ag_binary.Varuint32 - Instruction_RequestHeapFrame - - // Set a specific compute unit limit that the transaction is allowed to consume. - // note: uses ag_binary.Varuint32 - Instruction_SetComputeUnitLimit - - // Set a compute unit price in "micro-lamports" to pay a higher transaction - // fee for higher transaction prioritization. - // note: uses ag_binary.Uint64 - Instruction_SetComputeUnitPrice -) - -const ( - COMPUTE_BUDGET_PROGRAM = "ComputeBudget111111111111111111111111111111" -) - -// https://docs.solana.com/developing/programming-model/runtime -type ComputeUnitPrice uint64 - -// returns the compute budget program -func (val ComputeUnitPrice) ProgramID() solana.PublicKey { - return solana.MustPublicKeyFromBase58(COMPUTE_BUDGET_PROGRAM) -} - -// No accounts needed -func (val ComputeUnitPrice) Accounts() (accounts []*solana.AccountMeta) { - return accounts -} - -// simple encoding into program expected format -func (val ComputeUnitPrice) Data() ([]byte, error) { - buf := new(bytes.Buffer) - - // encode method identifier - if err := buf.WriteByte(Instruction_SetComputeUnitPrice); err != nil { - return []byte{}, err - } - - // encode value - if err := binary.Write(buf, binary.LittleEndian, val); err != nil { - return []byte{}, err - } - - return buf.Bytes(), nil -} - -// modifies passed in tx to set compute unit price -func SetComputeUnitPrice(tx *solana.Transaction, price ComputeUnitPrice) error { - // find ComputeBudget program to accounts if it exists - // reimplements HasAccount to retrieve index: https://github.com/gagliardetto/solana-go/blob/618f56666078f8131a384ab27afd918d248c08b7/message.go#L233 - var exists bool - var programIdx uint16 - for i, a := range tx.Message.AccountKeys { - if a.Equals(price.ProgramID()) { - exists = true - programIdx = uint16(i) - break - } - } - // if it doesn't exist, add to account keys - if !exists { - tx.Message.AccountKeys = append(tx.Message.AccountKeys, price.ProgramID()) - programIdx = uint16(len(tx.Message.AccountKeys) - 1) // last index of account keys - - // https://github.com/gagliardetto/solana-go/blob/618f56666078f8131a384ab27afd918d248c08b7/transaction.go#L293 - tx.Message.Header.NumReadonlyUnsignedAccounts++ - } - - // get instruction data - data, err := price.Data() - if err != nil { - return err - } - - // compiled instruction - instruction := solana.CompiledInstruction{ - ProgramIDIndex: programIdx, - Data: data, - } - - // check if there is an instruction for setcomputeunitprice - var found bool - var instructionIdx int - for i := range tx.Message.Instructions { - if tx.Message.Instructions[i].ProgramIDIndex == programIdx && - len(tx.Message.Instructions[i].Data) > 0 && - tx.Message.Instructions[i].Data[0] == Instruction_SetComputeUnitPrice { - found = true - instructionIdx = i - break - } - } - - if found { - tx.Message.Instructions[instructionIdx] = instruction - } else { - // build with first instruction as set compute unit price - tx.Message.Instructions = append([]solana.CompiledInstruction{instruction}, tx.Message.Instructions...) - } - - return nil -} diff --git a/core/chains/solana/fees/computebudget_test.go b/core/chains/solana/fees/computebudget_test.go deleted file mode 100644 index b4003281e45..00000000000 --- a/core/chains/solana/fees/computebudget_test.go +++ /dev/null @@ -1,103 +0,0 @@ -package fees - -import ( - "testing" - - "github.com/gagliardetto/solana-go" - "github.com/gagliardetto/solana-go/programs/system" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestSetComputeUnitPrice(t *testing.T) { - key, err := solana.NewRandomPrivateKey() - require.NoError(t, err) - - t.Run("noAccount_nofee", func(t *testing.T) { - // build base tx (no fee) - tx, err := solana.NewTransaction([]solana.Instruction{ - system.NewTransferInstruction( - 0, - key.PublicKey(), - key.PublicKey(), - ).Build(), - }, solana.Hash{}) - require.NoError(t, err) - instructionCount := len(tx.Message.Instructions) - - // add fee - require.NoError(t, SetComputeUnitPrice(tx, 1)) - - // evaluate - currentCount := len(tx.Message.Instructions) - assert.Greater(t, currentCount, instructionCount) - assert.Equal(t, 2, currentCount) - assert.Equal(t, COMPUTE_BUDGET_PROGRAM, tx.Message.AccountKeys[tx.Message.Instructions[0].ProgramIDIndex].String()) - data, err := ComputeUnitPrice(1).Data() - assert.NoError(t, err) - assert.Equal(t, data, []byte(tx.Message.Instructions[0].Data)) - }) - - t.Run("accountExists_noFee", func(t *testing.T) { - // build base tx (no fee) - tx, err := solana.NewTransaction([]solana.Instruction{ - system.NewTransferInstruction( - 0, - key.PublicKey(), - key.PublicKey(), - ).Build(), - }, solana.Hash{}) - require.NoError(t, err) - accountCount := len(tx.Message.AccountKeys) - tx.Message.AccountKeys = append(tx.Message.AccountKeys, ComputeUnitPrice(0).ProgramID()) - accountCount++ - - // add fee - require.NoError(t, SetComputeUnitPrice(tx, 1)) - - // accounts should not have changed - assert.Equal(t, accountCount, len(tx.Message.AccountKeys)) - assert.Equal(t, 2, len(tx.Message.Instructions)) - assert.Equal(t, COMPUTE_BUDGET_PROGRAM, tx.Message.AccountKeys[tx.Message.Instructions[0].ProgramIDIndex].String()) - data, err := ComputeUnitPrice(1).Data() - assert.NoError(t, err) - assert.Equal(t, data, []byte(tx.Message.Instructions[0].Data)) - - }) - - // // not a valid test, account must exist for tx to be added - // t.Run("noAccount_feeExists", func(t *testing.T) {}) - - t.Run("exists_notFirst", func(t *testing.T) { - // build base tx (no fee) - tx, err := solana.NewTransaction([]solana.Instruction{ - system.NewTransferInstruction( - 0, - key.PublicKey(), - key.PublicKey(), - ).Build(), - }, solana.Hash{}) - require.NoError(t, err) - transferInstruction := tx.Message.Instructions[0] - - // add fee - require.NoError(t, SetComputeUnitPrice(tx, 0)) - - // swap order of instructions - tx.Message.Instructions[0], tx.Message.Instructions[1] = tx.Message.Instructions[1], tx.Message.Instructions[0] - require.Equal(t, transferInstruction, tx.Message.Instructions[0]) - oldFeeInstruction := tx.Message.Instructions[1] - accountCount := len(tx.Message.AccountKeys) - - // set fee with existing fee instruction - require.NoError(t, SetComputeUnitPrice(tx, 100)) - require.Equal(t, transferInstruction, tx.Message.Instructions[0]) // transfer should not have been touched - assert.NotEqual(t, oldFeeInstruction, tx.Message.Instructions[1]) - assert.Equal(t, accountCount, len(tx.Message.AccountKeys)) - assert.Equal(t, 2, len(tx.Message.Instructions)) // instruction count did not change - data, err := ComputeUnitPrice(100).Data() - assert.NoError(t, err) - assert.Equal(t, data, []byte(tx.Message.Instructions[1].Data)) - }) - -} diff --git a/core/chains/solana/fees/estimator.go b/core/chains/solana/fees/estimator.go deleted file mode 100644 index f09fec95be8..00000000000 --- a/core/chains/solana/fees/estimator.go +++ /dev/null @@ -1,9 +0,0 @@ -package fees - -import "context" - -type Estimator interface { - Start(context.Context) error - Close() error - BaseComputeUnitPrice() uint64 -} diff --git a/core/chains/solana/fees/fixed_price.go b/core/chains/solana/fees/fixed_price.go deleted file mode 100644 index 3a748c301c9..00000000000 --- a/core/chains/solana/fees/fixed_price.go +++ /dev/null @@ -1,38 +0,0 @@ -package fees - -import ( - "context" - "fmt" - - "github.com/smartcontractkit/chainlink-solana/pkg/solana/config" -) - -var _ Estimator = &fixedPriceEstimator{} - -type fixedPriceEstimator struct { - cfg config.Config -} - -func NewFixedPriceEstimator(cfg config.Config) (Estimator, error) { - defaultPrice, min, max := cfg.ComputeUnitPriceDefault(), cfg.ComputeUnitPriceMin(), cfg.ComputeUnitPriceMax() - - if defaultPrice < min || defaultPrice > max { - return nil, fmt.Errorf("default price (%d) is not within the min (%d) and max (%d) price bounds", defaultPrice, min, max) - } - - return &fixedPriceEstimator{ - cfg: cfg, - }, nil -} - -func (est *fixedPriceEstimator) Start(ctx context.Context) error { - return nil -} - -func (est *fixedPriceEstimator) Close() error { - return nil -} - -func (est *fixedPriceEstimator) BaseComputeUnitPrice() uint64 { - return est.cfg.ComputeUnitPriceDefault() -} diff --git a/core/chains/solana/fees/recent_fees.go b/core/chains/solana/fees/recent_fees.go deleted file mode 100644 index 165fab0fcaa..00000000000 --- a/core/chains/solana/fees/recent_fees.go +++ /dev/null @@ -1,83 +0,0 @@ -package fees - -import ( - "context" - "fmt" - "sync" - "time" - - "github.com/smartcontractkit/chainlink-solana/pkg/solana/config" - "github.com/smartcontractkit/chainlink/core/utils" -) - -var ( - feePolling = 5 * time.Second // TODO: make configurable -) - -var _ Estimator = &recentFeeEstimator{} - -type recentFeeEstimator struct { - starter utils.StartStopOnce - chStop chan struct{} - done sync.WaitGroup - - cfg config.Config - - price uint64 - lock sync.RWMutex -} - -func NewRecentFeeEstimator(cfg config.Config) (Estimator, error) { - return &recentFeeEstimator{ - chStop: make(chan struct{}), - }, fmt.Errorf("estimator not available - RPC method not released") // TODO: implement when RPC method available -} - -func (est *recentFeeEstimator) Start(ctx context.Context) error { - return est.starter.StartOnce("solana_recentFeeEstimator", func() error { - est.done.Add(1) - go est.run() - return nil - }) -} - -func (est *recentFeeEstimator) run() { - defer est.done.Done() - - tick := time.After(0) - for { - select { - case <-est.chStop: - return - case <-tick: - // TODO: query endpoint - not available yet - - est.lock.Lock() - est.price = 0 - est.lock.Unlock() - } - - tick = time.After(utils.WithJitter(feePolling)) - } -} - -func (est *recentFeeEstimator) Close() error { - close(est.chStop) - est.done.Wait() - return nil -} - -func (est *recentFeeEstimator) BaseComputeUnitPrice() uint64 { - est.lock.RLock() - defer est.lock.RUnlock() - - if est.price >= est.cfg.ComputeUnitPriceMin() && est.price <= est.cfg.ComputeUnitPriceMax() { - return est.price - } - - if est.price < est.cfg.ComputeUnitPriceMin() { - return est.cfg.ComputeUnitPriceMin() - } - - return est.cfg.ComputeUnitPriceMax() -} diff --git a/core/chains/solana/fees/utils.go b/core/chains/solana/fees/utils.go deleted file mode 100644 index dd5b5dd7a34..00000000000 --- a/core/chains/solana/fees/utils.go +++ /dev/null @@ -1,29 +0,0 @@ -package fees - -// returns new fee based on number of times bumped -func CalculateFee(base, max, min uint64, count uint) uint64 { - amount := base - - for i := uint(0); i < count; i++ { - if base == 0 && i == 0 { - amount = 1 - } else { - // check for overflow - if next := amount + amount; next > amount { - amount = next - } else { - amount = max - break // exit loop if value overflowed - } - } - } - - // respect bounds - if amount < min { - return min - } - if amount > max { - return max - } - return amount -} diff --git a/core/chains/solana/fees/utils_test.go b/core/chains/solana/fees/utils_test.go deleted file mode 100644 index 54c454635f6..00000000000 --- a/core/chains/solana/fees/utils_test.go +++ /dev/null @@ -1,33 +0,0 @@ -package fees - -import ( - "fmt" - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestCalculateFee(t *testing.T) { - inputs := []struct { - base, max, min uint64 - count uint - expected uint64 - }{ - {0, 0, 0, 100, 0}, // test max - {0, 10, 1, 0, 1}, // test min - {0, 10, 0, 0, 0}, // test 0 count should return base - {0, 10, 0, 1, 1}, // test 1 count on 0 base should return 1 - {0, 10, 0, 2, 2}, // test 2 count on 0 base should return 2 - {0, 10, 0, 3, 4}, // test 3 count on 0 base should return 4 - {0, 10, 0, 4, 8}, // test 4 count on 0 base should return 8 - {1, 10, 0, 0, 1}, // test 0 count on 1 base should return 1 - {1, 10, 0, 1, 2}, // test 1 count on 1 base should return 2 - {1, 100, 0, 64, 100}, // test 64 bcount on 1 base should return max (overflow) - } - - for i, v := range inputs { - t.Run(fmt.Sprintf("inputs[%d]", i), func(t *testing.T) { - assert.Equal(t, v.expected, CalculateFee(v.base, v.max, v.min, v.count)) - }) - } -} diff --git a/core/chains/solana/mocks/tx_manager.go b/core/chains/solana/mocks/tx_manager.go deleted file mode 100644 index e1af37f3cc5..00000000000 --- a/core/chains/solana/mocks/tx_manager.go +++ /dev/null @@ -1,43 +0,0 @@ -// Code generated by mockery v2.20.0. DO NOT EDIT. - -package mocks - -import ( - mock "github.com/stretchr/testify/mock" - - solana "github.com/gagliardetto/solana-go" -) - -// TxManager is an autogenerated mock type for the TxManager type -type TxManager struct { - mock.Mock -} - -// Enqueue provides a mock function with given fields: accountID, msg -func (_m *TxManager) Enqueue(accountID string, msg *solana.Transaction) error { - ret := _m.Called(accountID, msg) - - var r0 error - if rf, ok := ret.Get(0).(func(string, *solana.Transaction) error); ok { - r0 = rf(accountID, msg) - } else { - r0 = ret.Error(0) - } - - return r0 -} - -type mockConstructorTestingTNewTxManager interface { - mock.TestingT - Cleanup(func()) -} - -// NewTxManager creates a new instance of TxManager. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. -func NewTxManager(t mockConstructorTestingTNewTxManager) *TxManager { - mock := &TxManager{} - mock.Mock.Test(t) - - t.Cleanup(func() { mock.AssertExpectations(t) }) - - return mock -} diff --git a/core/chains/solana/soltxm/pendingtx.go b/core/chains/solana/soltxm/pendingtx.go deleted file mode 100644 index 67360311c6a..00000000000 --- a/core/chains/solana/soltxm/pendingtx.go +++ /dev/null @@ -1,243 +0,0 @@ -package soltxm - -import ( - "context" - "errors" - "sync" - "time" - - "github.com/gagliardetto/solana-go" - "github.com/google/uuid" - "golang.org/x/exp/maps" -) - -type PendingTxContext interface { - New(sig solana.Signature, cancel context.CancelFunc) (uuid.UUID, error) - Add(id uuid.UUID, sig solana.Signature) error - Remove(sig solana.Signature) uuid.UUID - ListAll() []solana.Signature - Expired(sig solana.Signature, lifespan time.Duration) bool - // state change hooks - OnSuccess(sig solana.Signature) uuid.UUID - OnError(sig solana.Signature, errType int) uuid.UUID // match err type using enum -} - -var _ PendingTxContext = &pendingTxContext{} - -type pendingTxContext struct { - cancelBy map[uuid.UUID]context.CancelFunc - timestamp map[uuid.UUID]time.Time - sigToId map[solana.Signature]uuid.UUID - idToSigs map[uuid.UUID][]solana.Signature - lock sync.RWMutex -} - -func newPendingTxContext() *pendingTxContext { - return &pendingTxContext{ - cancelBy: map[uuid.UUID]context.CancelFunc{}, - timestamp: map[uuid.UUID]time.Time{}, - sigToId: map[solana.Signature]uuid.UUID{}, - idToSigs: map[uuid.UUID][]solana.Signature{}, - } -} - -func (c *pendingTxContext) New(sig solana.Signature, cancel context.CancelFunc) (uuid.UUID, error) { - // validate signature does not exist - c.lock.RLock() - if _, exists := c.sigToId[sig]; exists { - c.lock.RUnlock() - return uuid.UUID{}, errors.New("signature already exists") - } - c.lock.RUnlock() - - // upgrade to write lock if sig does not exist - c.lock.Lock() - defer c.lock.Unlock() - if _, exists := c.sigToId[sig]; exists { - return uuid.UUID{}, errors.New("signature already exists") - } - // save cancel func - id := uuid.New() - c.cancelBy[id] = cancel - c.timestamp[id] = time.Now() - c.sigToId[sig] = id - c.idToSigs[id] = []solana.Signature{sig} - return id, nil -} - -func (c *pendingTxContext) Add(id uuid.UUID, sig solana.Signature) error { - // already exists - c.lock.RLock() - if _, exists := c.sigToId[sig]; exists { - c.lock.RUnlock() - return errors.New("signature already exists") - } - if _, exists := c.idToSigs[id]; !exists { - c.lock.RUnlock() - return errors.New("id does not exist") - } - c.lock.RUnlock() - - // upgrade to write lock if sig does not exist - c.lock.Lock() - defer c.lock.Unlock() - if _, exists := c.sigToId[sig]; exists { - return errors.New("signature already exists") - } - if _, exists := c.idToSigs[id]; !exists { - return errors.New("id does not exist - tx likely confirmed by other signature") - } - // save signature - c.sigToId[sig] = id - c.idToSigs[id] = append(c.idToSigs[id], sig) - return nil -} - -// returns the id if removed (otherwise returns 0-id) -func (c *pendingTxContext) Remove(sig solana.Signature) (id uuid.UUID) { - // check if already cancelled - c.lock.RLock() - id, sigExists := c.sigToId[sig] - if !sigExists { - c.lock.RUnlock() - return id - } - if _, idExists := c.idToSigs[id]; !idExists { - c.lock.RUnlock() - return id - } - c.lock.RUnlock() - - // upgrade to write lock if sig does not exist - c.lock.Lock() - defer c.lock.Unlock() - id, sigExists = c.sigToId[sig] - if !sigExists { - return id - } - sigs, idExists := c.idToSigs[id] - if !idExists { - return id - } - - // call cancel func + remove from map - c.cancelBy[id]() // cancel context - delete(c.cancelBy, id) - delete(c.timestamp, id) - delete(c.idToSigs, id) - for _, s := range sigs { - delete(c.sigToId, s) - } - return id -} - -func (c *pendingTxContext) ListAll() []solana.Signature { - c.lock.RLock() - defer c.lock.RUnlock() - return maps.Keys(c.sigToId) -} - -// Expired returns if the timeout for trying to confirm a signature has been reached -func (c *pendingTxContext) Expired(sig solana.Signature, lifespan time.Duration) bool { - c.lock.RLock() - defer c.lock.RUnlock() - id, exists := c.sigToId[sig] - if !exists { - return false // return expired = false if timestamp does not exist (likely cleaned up by something else previously) - } - - timestamp, exists := c.timestamp[id] - if !exists { - return false // return expired = false if timestamp does not exist (likely cleaned up by something else previously) - } - - return time.Since(timestamp) > lifespan -} - -func (c *pendingTxContext) OnSuccess(sig solana.Signature) uuid.UUID { - return c.Remove(sig) -} - -func (c *pendingTxContext) OnError(sig solana.Signature, _ int) uuid.UUID { - return c.Remove(sig) -} - -var _ PendingTxContext = &pendingTxContextWithProm{} - -type pendingTxContextWithProm struct { - pendingTx *pendingTxContext - chainID string -} - -const ( - TxFailRevert = iota - TxFailReject - TxFailDrop - TxFailSimRevert - TxFailSimOther -) - -func newPendingTxContextWithProm(id string) *pendingTxContextWithProm { - return &pendingTxContextWithProm{ - chainID: id, - pendingTx: newPendingTxContext(), - } -} - -func (c *pendingTxContextWithProm) New(sig solana.Signature, cancel context.CancelFunc) (uuid.UUID, error) { - return c.pendingTx.New(sig, cancel) -} - -func (c *pendingTxContextWithProm) Add(id uuid.UUID, sig solana.Signature) error { - return c.pendingTx.Add(id, sig) -} - -func (c *pendingTxContextWithProm) Remove(sig solana.Signature) uuid.UUID { - return c.pendingTx.Remove(sig) -} - -func (c *pendingTxContextWithProm) ListAll() []solana.Signature { - sigs := c.pendingTx.ListAll() - promSolTxmPendingTxs.WithLabelValues(c.chainID).Set(float64(len(sigs))) - return sigs -} - -func (c *pendingTxContextWithProm) Expired(sig solana.Signature, lifespan time.Duration) bool { - return c.pendingTx.Expired(sig, lifespan) -} - -// Success - tx included in block and confirmed -func (c *pendingTxContextWithProm) OnSuccess(sig solana.Signature) uuid.UUID { - id := c.pendingTx.OnSuccess(sig) // empty ID indicates already previously removed - if id != uuid.Nil { // increment if tx was not removed - promSolTxmSuccessTxs.WithLabelValues(c.chainID).Add(1) - } - return id -} - -func (c *pendingTxContextWithProm) OnError(sig solana.Signature, errType int) uuid.UUID { - // special RPC rejects transaction (signature will not be valid) - if errType == TxFailReject { - promSolTxmRejectTxs.WithLabelValues(c.chainID).Add(1) - promSolTxmErrorTxs.WithLabelValues(c.chainID).Add(1) - return uuid.Nil - } - - id := c.pendingTx.OnError(sig, errType) // empty ID indicates already removed - if id != uuid.Nil { - switch errType { - case TxFailRevert: - promSolTxmRevertTxs.WithLabelValues(c.chainID).Add(1) - case TxFailDrop: - promSolTxmDropTxs.WithLabelValues(c.chainID).Add(1) - case TxFailSimRevert: - promSolTxmSimRevertTxs.WithLabelValues(c.chainID).Add(1) - case TxFailSimOther: - promSolTxmSimOtherTxs.WithLabelValues(c.chainID).Add(1) - } - // increment total errors - promSolTxmErrorTxs.WithLabelValues(c.chainID).Add(1) - } - - return id -} diff --git a/core/chains/solana/soltxm/pendingtx_test.go b/core/chains/solana/soltxm/pendingtx_test.go deleted file mode 100644 index d941e77b6c3..00000000000 --- a/core/chains/solana/soltxm/pendingtx_test.go +++ /dev/null @@ -1,143 +0,0 @@ -package soltxm - -import ( - "context" - "crypto/rand" - "sync" - "testing" - "time" - - "github.com/gagliardetto/solana-go" - "github.com/google/uuid" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/smartcontractkit/chainlink/core/internal/testutils" -) - -func TestPendingTxContext(t *testing.T) { - // setup - var wg sync.WaitGroup - ctx := testutils.Context(t) - - newProcess := func(i int) (solana.Signature, context.CancelFunc) { - // make random signature - sig := make([]byte, 64) - _, err := rand.Read(sig) - require.NoError(t, err) - - // start subprocess to wait for context - processCtx, cancel := context.WithCancel(ctx) - wg.Add(1) - go func() { - <-processCtx.Done() - wg.Done() - }() - return solana.SignatureFromBytes(sig), cancel - } - - // init inflight txs map + store some signatures and cancelFunc - txs := newPendingTxContext() - ids := map[solana.Signature]uuid.UUID{} - n := 5 - for i := 0; i < n; i++ { - sig, cancel := newProcess(i) - id, err := txs.New(sig, cancel) - assert.NoError(t, err) - ids[sig] = id - } - - // cannot add signature for non existent ID - require.Error(t, txs.Add(uuid.New(), solana.Signature{})) - - // return list of signatures - list := txs.ListAll() - assert.Equal(t, n, len(list)) - - // stop all sub processes - for i := 0; i < len(list); i++ { - id := txs.Remove(list[i]) - assert.Equal(t, n-i-1, len(txs.ListAll())) - assert.Equal(t, ids[list[i]], id) - - // second remove should not return valid id - already removed - assert.Equal(t, uuid.Nil, txs.Remove(list[i])) - } - wg.Wait() -} - -func TestPendingTxContext_expired(t *testing.T) { - _, cancel := context.WithCancel(testutils.Context(t)) - sig := solana.Signature{} - txs := newPendingTxContext() - - id, err := txs.New(sig, cancel) - assert.NoError(t, err) - - assert.True(t, txs.Expired(sig, 0*time.Second)) // expired for 0s lifetime - assert.False(t, txs.Expired(sig, 60*time.Second)) // not expired for 60s lifetime - - assert.Equal(t, id, txs.Remove(sig)) - assert.False(t, txs.Expired(sig, 60*time.Second)) // no longer exists, should return false -} - -func TestPendingTxContext_race(t *testing.T) { - t.Run("new", func(t *testing.T) { - txCtx := newPendingTxContext() - var wg sync.WaitGroup - wg.Add(2) - var err [2]error - - go func() { - _, err[0] = txCtx.New(solana.Signature{}, func() {}) - wg.Done() - }() - go func() { - _, err[1] = txCtx.New(solana.Signature{}, func() {}) - wg.Done() - }() - - wg.Wait() - assert.True(t, (err[0] != nil && err[1] == nil) || (err[0] == nil && err[1] != nil), "one and only one 'add' should have errored") - }) - - t.Run("add", func(t *testing.T) { - txCtx := newPendingTxContext() - id, createErr := txCtx.New(solana.Signature{}, func() {}) - require.NoError(t, createErr) - var wg sync.WaitGroup - wg.Add(2) - var err [2]error - - go func() { - err[0] = txCtx.Add(id, solana.Signature{1}) - wg.Done() - }() - go func() { - err[1] = txCtx.Add(id, solana.Signature{1}) - wg.Done() - }() - - wg.Wait() - assert.True(t, (err[0] != nil && err[1] == nil) || (err[0] == nil && err[1] != nil), "one and only one 'add' should have errored") - }) - - t.Run("remove", func(t *testing.T) { - txCtx := newPendingTxContext() - _, err := txCtx.New(solana.Signature{}, func() {}) - require.NoError(t, err) - var wg sync.WaitGroup - wg.Add(2) - - go func() { - assert.NotPanics(t, func() { txCtx.Remove(solana.Signature{}) }) - wg.Done() - }() - go func() { - assert.NotPanics(t, func() { txCtx.Remove(solana.Signature{}) }) - wg.Done() - }() - - wg.Wait() - }) -} diff --git a/core/chains/solana/soltxm/prom.go b/core/chains/solana/soltxm/prom.go deleted file mode 100644 index fb45c30e5e2..00000000000 --- a/core/chains/solana/soltxm/prom.go +++ /dev/null @@ -1,46 +0,0 @@ -package soltxm - -import ( - "github.com/prometheus/client_golang/prometheus" - "github.com/prometheus/client_golang/prometheus/promauto" -) - -var ( - // successful transactions - promSolTxmSuccessTxs = promauto.NewCounterVec(prometheus.CounterOpts{ - Name: "solana_txm_tx_success", - Help: "Number of transactions that are included and successfully executed on chain", - }, []string{"chainID"}) - - // inflight transactions - promSolTxmPendingTxs = promauto.NewGaugeVec(prometheus.GaugeOpts{ - Name: "solana_txm_tx_pending", - Help: "Number of transactions that are pending confirmation", - }, []string{"chainID"}) - - // error cases - promSolTxmErrorTxs = promauto.NewCounterVec(prometheus.CounterOpts{ - Name: "solana_txm_tx_error", - Help: "Number of transactions that have errored across all cases", - }, []string{"chainID"}) - promSolTxmRevertTxs = promauto.NewCounterVec(prometheus.CounterOpts{ - Name: "solana_txm_tx_error_revert", - Help: "Number of transactions that are included and failed onchain", - }, []string{"chainID"}) - promSolTxmRejectTxs = promauto.NewCounterVec(prometheus.CounterOpts{ - Name: "solana_txm_tx_error_reject", - Help: "Number of transactions that the RPC immediately rejected", - }, []string{"chainID"}) - promSolTxmDropTxs = promauto.NewCounterVec(prometheus.CounterOpts{ - Name: "solana_txm_tx_error_drop", - Help: "Number of transactions that timed out during confirmation. Note: tx is likely dropped from the chain, but may still be included.", - }, []string{"chainID"}) - promSolTxmSimRevertTxs = promauto.NewCounterVec(prometheus.CounterOpts{ - Name: "solana_txm_tx_error_sim_revert", - Help: "Number of transactions that reverted during simulation. Note: tx may still be included onchain", - }, []string{"chainID"}) - promSolTxmSimOtherTxs = promauto.NewCounterVec(prometheus.CounterOpts{ - Name: "solana_txm_tx_error_sim_other", - Help: "Number of transactions that failed simulation with an unrecognized error. Note: tx may still be included onchain", - }, []string{"chainID"}) -) diff --git a/core/chains/solana/soltxm/txm.go b/core/chains/solana/soltxm/txm.go deleted file mode 100644 index 9185d74fb1f..00000000000 --- a/core/chains/solana/soltxm/txm.go +++ /dev/null @@ -1,536 +0,0 @@ -package soltxm - -import ( - "context" - "fmt" - "strings" - "sync" - "time" - - solanaGo "github.com/gagliardetto/solana-go" - "github.com/gagliardetto/solana-go/rpc" - "github.com/google/uuid" - "github.com/pkg/errors" - - "github.com/smartcontractkit/chainlink-solana/pkg/solana" - solanaClient "github.com/smartcontractkit/chainlink-solana/pkg/solana/client" - "github.com/smartcontractkit/chainlink-solana/pkg/solana/config" - - "github.com/smartcontractkit/chainlink/core/chains/solana/fees" - "github.com/smartcontractkit/chainlink/core/logger" - "github.com/smartcontractkit/chainlink/core/services" - "github.com/smartcontractkit/chainlink/core/services/keystore" - "github.com/smartcontractkit/chainlink/core/utils" -) - -const ( - MaxQueueLen = 1000 - MaxRetryTimeMs = 250 // max tx retry time (exponential retry will taper to retry every 0.25s) - MaxSigsToConfirm = 256 // max number of signatures in GetSignatureStatus call -) - -var ( - _ services.ServiceCtx = (*Txm)(nil) - _ solana.TxManager = (*Txm)(nil) -) - -// Txm manages transactions for the solana blockchain. -// simple implementation with no persistently stored txs -type Txm struct { - starter utils.StartStopOnce - lggr logger.Logger - chSend chan pendingTx - chSim chan pendingTx - chStop chan struct{} - done sync.WaitGroup - cfg config.Config - txs PendingTxContext - ks keystore.Solana - client *utils.LazyLoad[solanaClient.ReaderWriter] - fee fees.Estimator -} - -type pendingTx struct { - tx *solanaGo.Transaction - timeout time.Duration - signature solanaGo.Signature - id uuid.UUID -} - -// NewTxm creates a txm. Uses simulation so should only be used to send txes to trusted contracts i.e. OCR. -func NewTxm(chainID string, tc func() (solanaClient.ReaderWriter, error), cfg config.Config, ks keystore.Solana, lggr logger.Logger) *Txm { - lggr = lggr.Named("Txm") - return &Txm{ - starter: utils.StartStopOnce{}, - lggr: lggr, - chSend: make(chan pendingTx, MaxQueueLen), // queue can support 1000 pending txs - chSim: make(chan pendingTx, MaxQueueLen), // queue can support 1000 pending txs - chStop: make(chan struct{}), - cfg: cfg, - txs: newPendingTxContextWithProm(chainID), - ks: ks, - client: utils.NewLazyLoad(tc), - } -} - -// Start subscribes to queuing channel and processes them. -func (txm *Txm) Start(ctx context.Context) error { - return txm.starter.StartOnce("solana_txm", func() error { - // determine estimator type - var estimator fees.Estimator - var err error - switch strings.ToLower(txm.cfg.FeeEstimatorMode()) { - case "fixed": - estimator, err = fees.NewFixedPriceEstimator(txm.cfg) - case "recentfees": - estimator, err = fees.NewRecentFeeEstimator(txm.cfg) - default: - err = fmt.Errorf("unknown solana fee estimator type: %s", txm.cfg.FeeEstimatorMode()) - } - if err != nil { - return err - } - txm.fee = estimator - if err := txm.fee.Start(ctx); err != nil { - return err - } - - txm.done.Add(3) // waitgroup: tx retry, confirmer, simulator - go txm.run() - return nil - }) -} - -func (txm *Txm) run() { - defer txm.done.Done() - ctx, cancel := utils.ContextFromChan(txm.chStop) - defer cancel() - - // start confirmer + simulator - go txm.confirm(ctx) - go txm.simulate(ctx) - - for { - select { - case msg := <-txm.chSend: - // process tx (pass tx copy) - tx, id, sig, err := txm.sendWithRetry(ctx, *msg.tx, msg.timeout) - if err != nil { - txm.lggr.Errorw("failed to send transaction", "error", err) - txm.client.Reset() // clear client if tx fails immediately (potentially bad RPC) - continue // skip remainining - } - - // send tx + signature to simulation queue - msg.tx = &tx - msg.signature = sig - msg.id = id - select { - case txm.chSim <- msg: - default: - txm.lggr.Warnw("failed to enqeue tx for simulation", "queueFull", len(txm.chSend) == MaxQueueLen, "tx", msg) - } - - txm.lggr.Debugw("transaction sent", "signature", sig.String(), "id", id) - case <-txm.chStop: - return - } - } -} - -func (txm *Txm) sendWithRetry(chanCtx context.Context, baseTx solanaGo.Transaction, timeout time.Duration) (solanaGo.Transaction, uuid.UUID, solanaGo.Signature, error) { - // fetch client - client, clientErr := txm.client.Get() - if clientErr != nil { - return solanaGo.Transaction{}, uuid.Nil, solanaGo.Signature{}, errors.Wrap(clientErr, "failed to get client in soltxm.sendWithRetry") - } - - // get key - // fee payer account is index 0 account - // https://github.com/gagliardetto/solana-go/blob/main/transaction.go#L252 - key, keyErr := txm.ks.Get(baseTx.Message.AccountKeys[0].String()) - if keyErr != nil { - return solanaGo.Transaction{}, uuid.Nil, solanaGo.Signature{}, errors.Wrap(keyErr, "error in soltxm.Enqueue.GetKey") - } - - getFee := func(count uint) fees.ComputeUnitPrice { - fee := fees.CalculateFee( - txm.fee.BaseComputeUnitPrice(), - txm.cfg.ComputeUnitPriceMax(), - txm.cfg.ComputeUnitPriceMin(), - count, - ) - return fees.ComputeUnitPrice(fee) - } - - buildTx := func(base solanaGo.Transaction, retryCount uint) (solanaGo.Transaction, error) { - newTx := base // make copy - - // set fee - // fee bumping can be enabled by moving the setting & signing logic to the broadcaster - if computeUnitErr := fees.SetComputeUnitPrice(&newTx, getFee(retryCount)); computeUnitErr != nil { - return solanaGo.Transaction{}, computeUnitErr - } - - // sign tx - txMsg, marshalErr := newTx.Message.MarshalBinary() - if marshalErr != nil { - return solanaGo.Transaction{}, errors.Wrap(marshalErr, "error in soltxm.SendWithRetry.MarshalBinary") - } - sigBytes, signErr := key.Sign(txMsg) - if signErr != nil { - return solanaGo.Transaction{}, errors.Wrap(signErr, "error in soltxm.SendWithRetry.Sign") - } - var finalSig [64]byte - copy(finalSig[:], sigBytes) - newTx.Signatures = append(newTx.Signatures, finalSig) - - return newTx, nil - } - - initTx, initBuildErr := buildTx(baseTx, 0) - if initBuildErr != nil { - return solanaGo.Transaction{}, uuid.Nil, solanaGo.Signature{}, initBuildErr - } - - // create timeout context - ctx, cancel := context.WithTimeout(chanCtx, timeout) - - // send initial tx (do not retry and exit early if fails) - sig, initSendErr := client.SendTx(ctx, &initTx) - if initSendErr != nil { - cancel() // cancel context when exiting early - txm.txs.OnError(sig, TxFailReject) // increment failed metric - return solanaGo.Transaction{}, uuid.Nil, solanaGo.Signature{}, errors.Wrap(initSendErr, "tx failed initial transmit") - } - - var sigsLock sync.RWMutex - sigs := []solanaGo.Signature{sig} - - // store tx signature + cancel function - id, initStoreErr := txm.txs.New(sig, cancel) - if initStoreErr != nil { - cancel() // cancel context when exiting early - return solanaGo.Transaction{}, uuid.Nil, solanaGo.Signature{}, errors.Wrapf(initStoreErr, "failed to save tx signature (%s) to inflight txs", sig) - } - - // retry with exponential backoff - // until context cancelled by timeout or called externally - // pass in copy of baseTx (used to build new tx with bumped fee) and broadcasted tx == initTx (used to retry tx without bumping) - go func(baseTx, currentTx solanaGo.Transaction) { - deltaT := 1 // ms - tick := time.After(0) - bumpCount := uint(0) - bumpTime := time.Now() - for { - select { - case <-ctx.Done(): - // stop sending tx after retry tx ctx times out (does not stop confirmation polling for tx) - txm.lggr.Debugw("stopped tx retry", "id", id, "signatures", sigs) - return - case <-tick: - var shouldBump bool - if time.Since(bumpTime) > txm.cfg.FeeBumpPeriod() { - bumpCount++ - bumpTime = time.Now() - shouldBump = true - } - - // if fee should be bumped, build new tx and replace currentTx - if shouldBump { - var retryBuildErr error - currentTx, retryBuildErr = buildTx(baseTx, bumpCount) - if retryBuildErr != nil { - txm.lggr.Errorw("failed to build bumped retry tx", "error", retryBuildErr, "id", id) - return // exit func if cannot build tx for retrying - } - - } - - // take currentTx and broadcast, if bumped fee -> save signature to list - go func(bump bool, count uint, retryTx solanaGo.Transaction) { - retrySig, retrySendErr := client.SendTx(ctx, &retryTx) - // this could occur if endpoint goes down or if ctx cancelled - if retrySendErr != nil { - if strings.Contains(retrySendErr.Error(), "context canceled") || strings.Contains(retrySendErr.Error(), "context deadline exceeded") { - txm.lggr.Debugw("ctx error on send retry transaction", "error", retrySendErr, "signatures", sigs, "id", id) - } else { - txm.lggr.Warnw("failed to send retry transaction", "error", retrySendErr, "signatures", sigs, "id", id) - } - return - } - - // save new signature if fee bumped - if bump { - if retryStoreErr := txm.txs.Add(id, retrySig); retryStoreErr != nil { - txm.lggr.Warnw("error in adding retry transaction", "error", retryStoreErr, "id", id) - return - } - sigsLock.Lock() - sigs = append(sigs, retrySig) - sigsLock.Unlock() - txm.lggr.Debugw("tx rebroadcast with bumped fee", "id", id, "fee", getFee(count), "signatures", sigs) - } - - // this should never happen (should match the last signature saved to sigs) - sigsLock.RLock() - if len(sigs) == 0 || retrySig != sigs[len(sigs)-1] { - txm.lggr.Criticalw("original signature does not match retry signature", "expectedSignatures", sigs, "receivedSignature", retrySig) - } - sigsLock.RUnlock() - }(shouldBump, bumpCount, currentTx) - } - - // exponential increase in wait time, capped at 250ms - deltaT *= 2 - if deltaT > MaxRetryTimeMs { - deltaT = MaxRetryTimeMs - } - tick = time.After(time.Duration(deltaT) * time.Millisecond) - } - }(baseTx, initTx) - - // return signed tx, id, signature for use in simulation - return initTx, id, sig, nil -} - -// goroutine that polls to confirm implementation -// cancels the exponential retry once confirmed -func (txm *Txm) confirm(ctx context.Context) { - defer txm.done.Done() - - tick := time.After(0) - for { - select { - case <-ctx.Done(): - return - case <-tick: - // get list of tx signatures to confirm - sigs := txm.txs.ListAll() - - // exit switch if not txs to confirm - if len(sigs) == 0 { - break - } - - // get client - client, err := txm.client.Get() - if err != nil { - txm.lggr.Errorw("failed to get client in soltxm.confirm", "error", err) - break // exit switch - } - - // batch sigs no more than MaxSigsToConfirm each - sigsBatch, err := utils.BatchSplit(sigs, MaxSigsToConfirm) - if err != nil { // this should never happen - txm.lggr.Criticalw("failed to batch signatures", "error", err) - break // exit switch - } - - // process signatures - processSigs := func(s []solanaGo.Signature, res []*rpc.SignatureStatusesResult) { - // sort signatures and results process successful first - s, res, err := SortSignaturesAndResults(s, res) - if err != nil { - txm.lggr.Errorw("sorting error", "error", err) - return - } - - for i := 0; i < len(res); i++ { - // if status is nil (sig not found), continue polling - // sig not found could mean invalid tx or not picked up yet - if res[i] == nil { - txm.lggr.Debugw("tx state: not found", - "signature", s[i], - ) - - // check confirm timeout exceeded - if txm.txs.Expired(s[i], txm.cfg.TxConfirmTimeout()) { - id := txm.txs.OnError(s[i], TxFailDrop) - txm.lggr.Warnw("failed to find transaction within confirm timeout", "id", id, "signature", s[i], "timeoutSeconds", txm.cfg.TxConfirmTimeout()) - } - continue - } - - // if signature has an error, end polling - if res[i].Err != nil { - id := txm.txs.OnError(s[i], TxFailRevert) - txm.lggr.Errorw("tx state: failed", - "id", id, - "signature", s[i], - "error", res[i].Err, - "status", res[i].ConfirmationStatus, - ) - continue - } - - // if signature is processed, keep polling - if res[i].ConfirmationStatus == rpc.ConfirmationStatusProcessed { - txm.lggr.Debugw("tx state: processed", - "signature", s[i], - ) - - // check confirm timeout exceeded - if txm.txs.Expired(s[i], txm.cfg.TxConfirmTimeout()) { - id := txm.txs.OnError(s[i], TxFailDrop) - txm.lggr.Warnw("tx failed to move beyond 'processed' within confirm timeout", "id", id, "signature", s[i], "timeoutSeconds", txm.cfg.TxConfirmTimeout()) - } - continue - } - - // if signature is confirmed/finalized, end polling - if res[i].ConfirmationStatus == rpc.ConfirmationStatusConfirmed || res[i].ConfirmationStatus == rpc.ConfirmationStatusFinalized { - id := txm.txs.OnSuccess(s[i]) - txm.lggr.Debugw(fmt.Sprintf("tx state: %s", res[i].ConfirmationStatus), - "id", id, - "signature", s[i], - ) - continue - } - } - } - - // waitgroup for processing - var wg sync.WaitGroup - wg.Add(len(sigsBatch)) - - // loop through batch - for i := 0; i < len(sigsBatch); i++ { - // fetch signature statuses - statuses, err := client.SignatureStatuses(ctx, sigsBatch[i]) - if err != nil { - txm.lggr.Errorw("failed to get signature statuses in soltxm.confirm", "error", err) - wg.Done() // don't block if exit early - break // exit for loop - } - - // nonblocking: process batches as soon as they come in - go func(index int) { - defer wg.Done() - processSigs(sigsBatch[index], statuses) - }(i) - } - wg.Wait() // wait for processing to finish - } - tick = time.After(utils.WithJitter(txm.cfg.ConfirmPollPeriod())) - } -} - -// goroutine that simulates tx (use a bounded number of goroutines to pick from queue?) -// simulate can cancel the send retry function early in the tx management process -// additionally, it can provide reasons for why a tx failed in the logs -func (txm *Txm) simulate(ctx context.Context) { - defer txm.done.Done() - - for { - select { - case <-ctx.Done(): - return - case msg := <-txm.chSim: - // get client - client, err := txm.client.Get() - if err != nil { - txm.lggr.Errorw("failed to get client in soltxm.simulate", "error", err) - continue - } - - res, err := client.SimulateTx(ctx, msg.tx, nil) // use default options (does not verify signatures) - if err != nil { - // this error can occur if endpoint goes down or if invalid signature (invalid signature should occur further upstream in sendWithRetry) - // allow retry to continue in case temporary endpoint failure (if still invalid, confirm or timeout will cleanup) - txm.lggr.Errorw("failed to simulate tx", "id", msg.id, "signature", msg.signature, "error", err) - continue - } - - // continue if simulation does not return error continue - if res.Err == nil { - continue - } - - // handle various errors - // https://github.com/solana-labs/solana/blob/master/sdk/src/transaction/error.rs - // --- - errStr := fmt.Sprintf("%v", res.Err) // convert to string to handle various interfaces - switch { - // blockhash not found when simulating, occurs when network bank has not seen the given blockhash or tx is too old - // let confirmation process clean up - case strings.Contains(errStr, "BlockhashNotFound"): - txm.lggr.Warnw("simulate: BlockhashNotFound", "id", msg.id, "signature", msg.signature, "result", res) - continue - // transaction will encounter execution error/revert, mark as reverted to remove from confirmation + retry - case strings.Contains(errStr, "InstructionError"): - txm.txs.OnError(msg.signature, TxFailSimRevert) // cancel retry - txm.lggr.Warnw("simulate: InstructionError", "id", msg.id, "signature", msg.signature, "result", res) - continue - // transaction is already processed in the chain, letting txm confirmation handle - case strings.Contains(errStr, "AlreadyProcessed"): - txm.lggr.Debugw("simulate: AlreadyProcessed", "id", msg.id, "signature", msg.signature, "result", res) - continue - // unrecognized errors (indicates more concerning failures) - default: - txm.txs.OnError(msg.signature, TxFailSimOther) // cancel retry - txm.lggr.Errorw("simulate: unrecognized error", "id", msg.id, "signature", msg.signature, "result", res) - continue - } - } - } -} - -// Enqueue enqueue a msg destined for the solana chain. -func (txm *Txm) Enqueue(accountID string, tx *solanaGo.Transaction) error { - // validate nil pointer - if tx == nil { - return errors.New("error in soltxm.Enqueue: tx is nil pointer") - } - // validate account keys slice - if len(tx.Message.AccountKeys) == 0 { - return errors.New("error in soltxm.Enqueue: not enough account keys in tx") - } - - // validate expected key exists - // fee payer account is index 0 account - // https://github.com/gagliardetto/solana-go/blob/main/transaction.go#L252 - _, err := txm.ks.Get(tx.Message.AccountKeys[0].String()) - if err != nil { - return errors.Wrap(err, "error in soltxm.Enqueue.GetKey") - } - - msg := pendingTx{ - tx: tx, - timeout: txm.cfg.TxRetryTimeout(), - } - - select { - case txm.chSend <- msg: - default: - txm.lggr.Errorw("failed to enqeue tx", "queueFull", len(txm.chSend) == MaxQueueLen, "tx", msg) - return errors.Errorf("failed to enqueue transaction for %s", accountID) - } - return nil -} - -func (txm *Txm) InflightTxs() int { - return len(txm.txs.ListAll()) -} - -// Close close service -func (txm *Txm) Close() error { - return txm.starter.StopOnce("solanatxm", func() error { - close(txm.chStop) - txm.done.Wait() - return txm.fee.Close() - }) -} -func (txm *Txm) Name() string { return "solanatxm" } - -// Healthy service is healthy -func (txm *Txm) Healthy() error { - return nil -} - -// Ready service is ready -func (txm *Txm) Ready() error { - return nil -} - -func (txm *Txm) HealthReport() map[string]error { return map[string]error{txm.Name(): txm.Healthy()} } diff --git a/core/chains/solana/soltxm/txm_internal_test.go b/core/chains/solana/soltxm/txm_internal_test.go deleted file mode 100644 index 6f2edc2a5b4..00000000000 --- a/core/chains/solana/soltxm/txm_internal_test.go +++ /dev/null @@ -1,625 +0,0 @@ -package soltxm - -import ( - "context" - "math/rand" - "sync" - "testing" - "time" - - "github.com/gagliardetto/solana-go" - "github.com/gagliardetto/solana-go/programs/system" - "github.com/gagliardetto/solana-go/rpc" - "github.com/pkg/errors" - "github.com/prometheus/client_golang/prometheus/testutil" - "github.com/stretchr/testify/assert" - "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/db" - - "github.com/smartcontractkit/chainlink/core/chains/solana/fees" - "github.com/smartcontractkit/chainlink/core/internal/testutils" - "github.com/smartcontractkit/chainlink/core/logger" - "github.com/smartcontractkit/chainlink/core/services/keystore" - "github.com/smartcontractkit/chainlink/core/services/keystore/keys/solkey" - keyMocks "github.com/smartcontractkit/chainlink/core/services/keystore/mocks" -) - -type soltxmProm struct { - id string - success, error, revert, reject, drop, simRevert, simOther float64 -} - -func (p soltxmProm) assertEqual(t *testing.T) { - assert.Equal(t, p.success, testutil.ToFloat64(promSolTxmSuccessTxs.WithLabelValues(p.id)), "mismatch: success") - assert.Equal(t, p.error, testutil.ToFloat64(promSolTxmErrorTxs.WithLabelValues(p.id)), "mismatch: error") - assert.Equal(t, p.revert, testutil.ToFloat64(promSolTxmRevertTxs.WithLabelValues(p.id)), "mismatch: revert") - assert.Equal(t, p.reject, testutil.ToFloat64(promSolTxmRejectTxs.WithLabelValues(p.id)), "mismatch: reject") - assert.Equal(t, p.drop, testutil.ToFloat64(promSolTxmDropTxs.WithLabelValues(p.id)), "mismatch: drop") - assert.Equal(t, p.simRevert, testutil.ToFloat64(promSolTxmSimRevertTxs.WithLabelValues(p.id)), "mismatch: simRevert") - assert.Equal(t, p.simOther, testutil.ToFloat64(promSolTxmSimOtherTxs.WithLabelValues(p.id)), "mismatch: simOther") -} - -func (p soltxmProm) getInflight() float64 { - return testutil.ToFloat64(promSolTxmPendingTxs.WithLabelValues(p.id)) -} - -// create placeholder transaction and returns func for signed tx with fee -func getTx(t *testing.T, val uint64, key solkey.Key, price fees.ComputeUnitPrice) (*solana.Transaction, func(fees.ComputeUnitPrice) *solana.Transaction) { - pubkey := key.PublicKey() - - // create transfer tx - tx, err := solana.NewTransaction( - []solana.Instruction{ - system.NewTransferInstruction( - val, - pubkey, - pubkey, - ).Build(), - }, - solana.Hash{}, - solana.TransactionPayer(pubkey), - ) - require.NoError(t, err) - - base := *tx // tx to send to txm, txm will add fee & sign - - return &base, func(price fees.ComputeUnitPrice) *solana.Transaction { - tx := base - // add fee - require.NoError(t, fees.SetComputeUnitPrice(&tx, price)) - - // sign tx - txMsg, err := tx.Message.MarshalBinary() - require.NoError(t, err) - sigBytes, err := key.Sign(txMsg) - require.NoError(t, err) - var finalSig [64]byte - copy(finalSig[:], sigBytes) - tx.Signatures = append(tx.Signatures, finalSig) - return &tx - } -} - -func newReaderWriterMock(t *testing.T) *mocks.ReaderWriter { - m := new(mocks.ReaderWriter) - m.Test(t) - t.Cleanup(func() { m.AssertExpectations(t) }) - return m -} - -func TestTxm(t *testing.T) { - // set up configs needed in txm - id := "mocknet" - lggr := logger.TestLogger(t) - cfg := config.NewConfig(db.ChainCfg{}, lggr) - mc := newReaderWriterMock(t) - - // mock solana keystore - key, err := solkey.New() - require.NoError(t, err) - - require.NoError(t, err) - mkey := keyMocks.NewSolana(t) - mkey.On("Get", key.ID()).Return(key, nil) - - txm := NewTxm(id, func() (client.ReaderWriter, error) { - return mc, nil - }, cfg, mkey, lggr) - require.NoError(t, txm.Start(testutils.Context(t))) - - // tracking prom metrics - prom := soltxmProm{id: id} - - // create random signature - getSig := func() solana.Signature { - sig := make([]byte, 64) - rand.Read(sig) - return solana.SignatureFromBytes(sig) - } - - // check if cached transaction is cleared - empty := func() bool { - count := txm.InflightTxs() - assert.Equal(t, float64(count), prom.getInflight()) // validate prom metric and txs length - return count == 0 - } - - // adjust wait time based on config - waitDuration := cfg.TxConfirmTimeout() - waitFor := func(f func() bool) { - for i := 0; i < int(waitDuration.Seconds()*1.5); i++ { - if f() { - return - } - time.Sleep(time.Second) - } - assert.NoError(t, errors.New("unable to confirm inflight txs is empty")) - } - - // handle signature statuses calls - statuses := map[solana.Signature]func() *rpc.SignatureStatusesResult{} - mc.On("SignatureStatuses", mock.Anything, mock.AnythingOfType("[]solana.Signature")).Return( - func(_ context.Context, sigs []solana.Signature) (out []*rpc.SignatureStatusesResult) { - for i := range sigs { - get, exists := statuses[sigs[i]] - if !exists { - out = append(out, nil) - continue - } - out = append(out, get()) - } - return out - }, nil, - ) - - // happy path (send => simulate success => tx: nil => tx: processed => tx: confirmed => done) - t.Run("happyPath", func(t *testing.T) { - sig := getSig() - tx, signed := getTx(t, 0, key, 0) - var wg sync.WaitGroup - wg.Add(3) - - sendCount := 0 - var countRW sync.RWMutex - mc.On("SendTx", mock.Anything, signed(0)).Run(func(mock.Arguments) { - countRW.Lock() - sendCount++ - countRW.Unlock() - }).After(500*time.Millisecond).Return(sig, nil) - mc.On("SimulateTx", mock.Anything, signed(0), mock.Anything).Return(&rpc.SimulateTransactionResult{}, nil).Once() - - // handle signature status calls - count := 0 - statuses[sig] = func() (out *rpc.SignatureStatusesResult) { - defer func() { count++ }() - defer wg.Done() - - out = &rpc.SignatureStatusesResult{} - if count == 1 { - out.ConfirmationStatus = rpc.ConfirmationStatusProcessed - return - } - - if count == 2 { - out.ConfirmationStatus = rpc.ConfirmationStatusConfirmed - return - } - return nil - } - - // send tx - assert.NoError(t, txm.Enqueue(t.Name(), tx)) - wg.Wait() - - // no transactions stored inflight txs list - waitFor(empty) - // transaction should be sent more than twice - countRW.RLock() - t.Logf("sendTx received %d calls", sendCount) - assert.Greater(t, sendCount, 2) - countRW.RUnlock() - - // panic if sendTx called after context cancelled - mc.On("SendTx", mock.Anything, tx).Panic("SendTx should not be called anymore").Maybe() - - // check prom metric - prom.success++ - prom.assertEqual(t) - }) - - // fail on initial transmit (RPC immediate rejects) - t.Run("fail_initialTx", func(t *testing.T) { - tx, signed := getTx(t, 1, key, 0) - var wg sync.WaitGroup - wg.Add(1) - - // should only be called once (tx does not start retry, confirming, or simulation) - mc.On("SendTx", mock.Anything, signed(0)).Run(func(mock.Arguments) { - wg.Done() - }).Return(solana.Signature{}, errors.New("FAIL")).Once() - - // tx should be able to queue - assert.NoError(t, txm.Enqueue(t.Name(), tx)) - wg.Wait() // wait to be picked up and processed - - // no transactions stored inflight txs list - waitFor(empty) - - // check prom metric - prom.error++ - prom.reject++ - prom.assertEqual(t) - }) - - // tx fails simulation (simulation error) - t.Run("fail_simulation", func(t *testing.T) { - tx, signed := getTx(t, 2, key, 0) - sig := getSig() - var wg sync.WaitGroup - wg.Add(1) - - mc.On("SendTx", mock.Anything, signed(0)).Return(sig, nil) - mc.On("SimulateTx", mock.Anything, signed(0), mock.Anything).Run(func(mock.Arguments) { - wg.Done() - }).Return(&rpc.SimulateTransactionResult{ - Err: "FAIL", - }, nil).Once() - // signature status is nil (handled automatically) - - // tx should be able to queue - assert.NoError(t, txm.Enqueue(t.Name(), tx)) - wg.Wait() // wait to be picked up and processed - waitFor(empty) // txs cleared quickly - - // check prom metric - prom.error++ - prom.simOther++ - prom.assertEqual(t) - }) - - // tx fails simulation (rpc error, timeout should clean up b/c sig status will be nil) - t.Run("fail_simulation_confirmNil", func(t *testing.T) { - tx, signed := getTx(t, 3, key, 0) - sig := getSig() - retry0 := getSig() - retry1 := getSig() - retry2 := getSig() - retry3 := getSig() - var wg sync.WaitGroup - wg.Add(1) - - mc.On("SendTx", mock.Anything, signed(0)).Return(sig, nil) - mc.On("SendTx", mock.Anything, signed(1)).Return(retry0, nil) - mc.On("SendTx", mock.Anything, signed(2)).Return(retry1, nil) - mc.On("SendTx", mock.Anything, signed(3)).Return(retry2, nil).Maybe() - mc.On("SendTx", mock.Anything, signed(4)).Return(retry3, nil).Maybe() - mc.On("SimulateTx", mock.Anything, signed(0), mock.Anything).Run(func(mock.Arguments) { - wg.Done() - }).Return(&rpc.SimulateTransactionResult{}, errors.New("FAIL")).Once() - // all signature statuses are nil, handled automatically - - // tx should be able to queue - assert.NoError(t, txm.Enqueue(t.Name(), tx)) - wg.Wait() // wait to be picked up and processed - waitFor(empty) // txs cleared after timeout - - // check prom metric - prom.error++ - prom.drop++ - prom.assertEqual(t) - - // panic if sendTx called after context cancelled - mc.On("SendTx", mock.Anything, tx).Panic("SendTx should not be called anymore").Maybe() - }) - - // tx fails simulation with an InstructionError (indicates reverted execution) - // manager should cancel sending retry immediately + increment reverted prom metric - t.Run("fail_simulation_instructionError", func(t *testing.T) { - tx, signed := getTx(t, 4, key, 0) - sig := getSig() - var wg sync.WaitGroup - wg.Add(1) - - // {"InstructionError":[0,{"Custom":6003}]} - tempErr := map[string][]interface{}{ - "InstructionError": { - 0, map[string]int{"Custom": 6003}, - }, - } - mc.On("SendTx", mock.Anything, signed(0)).Return(sig, nil) - mc.On("SimulateTx", mock.Anything, signed(0), mock.Anything).Run(func(mock.Arguments) { - wg.Done() - }).Return(&rpc.SimulateTransactionResult{ - Err: tempErr, - }, nil).Once() - // all signature statuses are nil, handled automatically - - // tx should be able to queue - assert.NoError(t, txm.Enqueue(t.Name(), tx)) - wg.Wait() // wait to be picked up and processed - waitFor(empty) // txs cleared after timeout - - // check prom metric - prom.error++ - prom.simRevert++ - prom.assertEqual(t) - - // panic if sendTx called after context cancelled - mc.On("SendTx", mock.Anything, tx).Panic("SendTx should not be called anymore").Maybe() - }) - - // tx fails simulation with BlockHashNotFound error - // txm should continue to confirm tx (in this case it will succeed) - t.Run("fail_simulation_blockhashNotFound", func(t *testing.T) { - tx, signed := getTx(t, 5, key, 0) - sig := getSig() - var wg sync.WaitGroup - wg.Add(3) - - mc.On("SendTx", mock.Anything, signed(0)).Return(sig, nil) - mc.On("SimulateTx", mock.Anything, signed(0), mock.Anything).Run(func(mock.Arguments) { - wg.Done() - }).Return(&rpc.SimulateTransactionResult{ - Err: "BlockhashNotFound", - }, nil).Once() - - // handle signature status calls - count := 0 - statuses[sig] = func() (out *rpc.SignatureStatusesResult) { - defer func() { count++ }() - defer wg.Done() - - out = &rpc.SignatureStatusesResult{} - if count == 1 { - out.ConfirmationStatus = rpc.ConfirmationStatusConfirmed - return - } - return nil - } - - // tx should be able to queue - assert.NoError(t, txm.Enqueue(t.Name(), tx)) - wg.Wait() // wait to be picked up and processed - waitFor(empty) // txs cleared after timeout - - // check prom metric - prom.success++ - prom.assertEqual(t) - - // panic if sendTx called after context cancelled - mc.On("SendTx", mock.Anything, tx).Panic("SendTx should not be called anymore").Maybe() - }) - - // tx fails simulation with AlreadyProcessed error - // txm should continue to confirm tx (in this case it will revert) - t.Run("fail_simulation_alreadyProcessed", func(t *testing.T) { - tx, signed := getTx(t, 6, key, 0) - sig := getSig() - var wg sync.WaitGroup - wg.Add(2) - - mc.On("SendTx", mock.Anything, signed(0)).Return(sig, nil) - mc.On("SimulateTx", mock.Anything, signed(0), mock.Anything).Run(func(mock.Arguments) { - wg.Done() - }).Return(&rpc.SimulateTransactionResult{ - Err: "AlreadyProcessed", - }, nil).Once() - - // handle signature status calls - statuses[sig] = func() (out *rpc.SignatureStatusesResult) { - wg.Done() - return &rpc.SignatureStatusesResult{ - Err: "ERROR", - ConfirmationStatus: rpc.ConfirmationStatusConfirmed, - } - } - - // tx should be able to queue - assert.NoError(t, txm.Enqueue(t.Name(), tx)) - wg.Wait() // wait to be picked up and processed - waitFor(empty) // txs cleared after timeout - - // check prom metric - prom.revert++ - prom.error++ - prom.assertEqual(t) - - // panic if sendTx called after context cancelled - mc.On("SendTx", mock.Anything, tx).Panic("SendTx should not be called anymore").Maybe() - }) - - // tx passes sim, never passes processed (timeout should cleanup) - t.Run("fail_confirm_processed", func(t *testing.T) { - tx, signed := getTx(t, 7, key, 0) - sig := getSig() - retry0 := getSig() - retry1 := getSig() - retry2 := getSig() - retry3 := getSig() - var wg sync.WaitGroup - wg.Add(1) - - mc.On("SendTx", mock.Anything, signed(0)).Return(sig, nil) - mc.On("SendTx", mock.Anything, signed(1)).Return(retry0, nil) - mc.On("SendTx", mock.Anything, signed(2)).Return(retry1, nil) - mc.On("SendTx", mock.Anything, signed(3)).Return(retry2, nil).Maybe() - mc.On("SendTx", mock.Anything, signed(4)).Return(retry3, nil).Maybe() - mc.On("SimulateTx", mock.Anything, signed(0), mock.Anything).Run(func(mock.Arguments) { - wg.Done() - }).Return(&rpc.SimulateTransactionResult{}, nil).Once() - - // handle signature status calls (initial stays processed, others don't exist) - statuses[sig] = func() (out *rpc.SignatureStatusesResult) { - return &rpc.SignatureStatusesResult{ - ConfirmationStatus: rpc.ConfirmationStatusProcessed, - } - } - - // tx should be able to queue - assert.NoError(t, txm.Enqueue(t.Name(), tx)) - wg.Wait() // wait to be picked up and processed - waitFor(empty) // inflight txs cleared after timeout - - // check prom metric - prom.error++ - prom.drop++ - prom.assertEqual(t) - - // panic if sendTx called after context cancelled - mc.On("SendTx", mock.Anything, tx).Panic("SendTx should not be called anymore").Maybe() - }) - - // tx passes sim, shows processed, moves to nil (timeout should cleanup) - t.Run("fail_confirm_processedToNil", func(t *testing.T) { - tx, signed := getTx(t, 8, key, 0) - sig := getSig() - retry0 := getSig() - retry1 := getSig() - retry2 := getSig() - retry3 := getSig() - var wg sync.WaitGroup - wg.Add(1) - - mc.On("SendTx", mock.Anything, signed(0)).Return(sig, nil) - mc.On("SendTx", mock.Anything, signed(1)).Return(retry0, nil) - mc.On("SendTx", mock.Anything, signed(2)).Return(retry1, nil) - mc.On("SendTx", mock.Anything, signed(3)).Return(retry2, nil).Maybe() - mc.On("SendTx", mock.Anything, signed(4)).Return(retry3, nil).Maybe() - mc.On("SimulateTx", mock.Anything, signed(0), mock.Anything).Run(func(mock.Arguments) { - wg.Done() - }).Return(&rpc.SimulateTransactionResult{}, nil).Once() - - // handle signature status calls (initial stays processed => nil, others don't exist) - count := 0 - statuses[sig] = func() (out *rpc.SignatureStatusesResult) { - defer func() { count++ }() - - if count > 2 { - return nil - } - - return &rpc.SignatureStatusesResult{ - ConfirmationStatus: rpc.ConfirmationStatusProcessed, - } - } - - // tx should be able to queue - assert.NoError(t, txm.Enqueue(t.Name(), tx)) - wg.Wait() // wait to be picked up and processed - waitFor(empty) // inflight txs cleared after timeout - - // check prom metric - prom.error++ - prom.drop++ - prom.assertEqual(t) - - // panic if sendTx called after context cancelled - mc.On("SendTx", mock.Anything, tx).Panic("SendTx should not be called anymore").Maybe() - }) - - // tx passes sim, errors on confirm - t.Run("fail_confirm_revert", func(t *testing.T) { - tx, signed := getTx(t, 9, key, 0) - sig := getSig() - var wg sync.WaitGroup - wg.Add(1) - - mc.On("SendTx", mock.Anything, signed(0)).Return(sig, nil) - mc.On("SimulateTx", mock.Anything, signed(0), mock.Anything).Run(func(mock.Arguments) { - wg.Done() - }).Return(&rpc.SimulateTransactionResult{}, nil).Once() - - // handle signature status calls - statuses[sig] = func() (out *rpc.SignatureStatusesResult) { - return &rpc.SignatureStatusesResult{ - ConfirmationStatus: rpc.ConfirmationStatusProcessed, - Err: "ERROR", - } - } - - // tx should be able to queue - assert.NoError(t, txm.Enqueue(t.Name(), tx)) - wg.Wait() // wait to be picked up and processed - waitFor(empty) // inflight txs cleared after timeout - - // check prom metric - prom.error++ - prom.revert++ - prom.assertEqual(t) - - // panic if sendTx called after context cancelled - mc.On("SendTx", mock.Anything, tx).Panic("SendTx should not be called anymore").Maybe() - }) - - // tx passes sim, first retried TXs get dropped - t.Run("success_retryTx", func(t *testing.T) { - tx, signed := getTx(t, 10, key, 0) - sig := getSig() - retry0 := getSig() - retry1 := getSig() - retry2 := getSig() - retry3 := getSig() - var wg sync.WaitGroup - wg.Add(2) - - mc.On("SendTx", mock.Anything, signed(0)).Return(sig, nil) - mc.On("SendTx", mock.Anything, signed(1)).Return(retry0, nil) - mc.On("SendTx", mock.Anything, signed(2)).Return(retry1, nil) - mc.On("SendTx", mock.Anything, signed(3)).Return(retry2, nil).Maybe() - mc.On("SendTx", mock.Anything, signed(4)).Return(retry3, nil).Maybe() - mc.On("SimulateTx", mock.Anything, signed(0), mock.Anything).Run(func(mock.Arguments) { - wg.Done() - }).Return(&rpc.SimulateTransactionResult{}, nil).Once() - - // handle signature status calls - statuses[retry1] = func() (out *rpc.SignatureStatusesResult) { - defer wg.Done() - return &rpc.SignatureStatusesResult{ - ConfirmationStatus: rpc.ConfirmationStatusConfirmed, - } - } - - // send tx - assert.NoError(t, txm.Enqueue(t.Name(), tx)) - wg.Wait() - - // no transactions stored inflight txs list - waitFor(empty) - - // panic if sendTx called after context cancelled - mc.On("SendTx", mock.Anything, tx).Panic("SendTx should not be called anymore").Maybe() - - // check prom metric - prom.success++ - prom.assertEqual(t) - }) -} - -func TestTxm_Enqueue(t *testing.T) { - // set up configs needed in txm - lggr := logger.TestLogger(t) - cfg := config.NewConfig(db.ChainCfg{}, lggr) - mc := newReaderWriterMock(t) - - // mock solana keystore - key, err := solkey.New() - require.NoError(t, err) - tx, _ := getTx(t, 0, key, 0) - - mkey := keyMocks.NewSolana(t) - mkey.On("Get", key.ID()).Return(key, nil) - invalidKey, err := solkey.New() - require.NoError(t, err) - invalidTx, _ := getTx(t, 0, invalidKey, 0) - mkey.On("Get", invalidKey.ID()).Return(solkey.Key{}, keystore.KeyNotFoundError{ID: invalidKey.ID(), KeyType: "Solana"}) - - txm := NewTxm("enqueue_test", func() (client.ReaderWriter, error) { - return mc, nil - }, cfg, mkey, lggr) - - txs := []struct { - name string - tx *solana.Transaction - fail bool - }{ - {"success", tx, false}, - {"invalid_key", invalidTx, true}, - {"nil_pointer", nil, true}, - {"empty_tx", &solana.Transaction{}, true}, - } - - for _, run := range txs { - t.Run(run.name, func(t *testing.T) { - if !run.fail { - assert.NoError(t, txm.Enqueue(run.name, run.tx)) - return - } - assert.Error(t, txm.Enqueue(run.name, run.tx)) - }) - } -} diff --git a/core/chains/solana/soltxm/txm_test.go b/core/chains/solana/soltxm/txm_test.go deleted file mode 100644 index 71efed3db74..00000000000 --- a/core/chains/solana/soltxm/txm_test.go +++ /dev/null @@ -1,142 +0,0 @@ -//go:build integration - -package soltxm_test - -import ( - "context" - "fmt" - "testing" - "time" - - "github.com/gagliardetto/solana-go" - "github.com/gagliardetto/solana-go/programs/system" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - relayutils "github.com/smartcontractkit/chainlink-relay/pkg/utils" - - "github.com/smartcontractkit/chainlink/core/chains/solana/soltxm" - "github.com/smartcontractkit/chainlink/core/internal/testutils" - "github.com/smartcontractkit/chainlink/core/logger" - "github.com/smartcontractkit/chainlink/core/services/keystore" - "github.com/smartcontractkit/chainlink/core/services/keystore/keys/solkey" - "github.com/smartcontractkit/chainlink/core/services/keystore/mocks" - - solanaClient "github.com/smartcontractkit/chainlink-solana/pkg/solana/client" - "github.com/smartcontractkit/chainlink-solana/pkg/solana/config" - "github.com/smartcontractkit/chainlink-solana/pkg/solana/db" -) - -func TestTxm_Integration(t *testing.T) { - ctx := testutils.Context(t) - url := solanaClient.SetupLocalSolNode(t) - - // setup key - key, err := solkey.New() - require.NoError(t, err) - pubKey := key.PublicKey() - - // setup load test key - loadTestKey, err := solkey.New() - require.NoError(t, err) - - // setup receiver key - privKeyReceiver, err := solana.NewRandomPrivateKey() - pubKeyReceiver := privKeyReceiver.PublicKey() - - // fund keys - solanaClient.FundTestAccounts(t, []solana.PublicKey{pubKey, loadTestKey.PublicKey()}, url) - - // setup mock keystore - mkey := mocks.NewSolana(t) - mkey.On("Get", key.ID()).Return(key, nil) - mkey.On("Get", loadTestKey.ID()).Return(loadTestKey, nil) - mkey.On("Get", pubKeyReceiver.String()).Return(solkey.Key{}, keystore.KeyNotFoundError{ID: pubKeyReceiver.String(), KeyType: "Solana"}) - - // set up txm - lggr := logger.TestLogger(t) - confirmDuration, err := relayutils.NewDuration(500 * time.Millisecond) - require.NoError(t, err) - cfg := config.NewConfig(db.ChainCfg{ - ConfirmPollPeriod: &confirmDuration, - }, lggr) - client, err := solanaClient.NewClient(url, cfg, 2*time.Second, lggr) - require.NoError(t, err) - getClient := func() (solanaClient.ReaderWriter, error) { - return client, nil - } - txm := soltxm.NewTxm("localnet", getClient, cfg, mkey, lggr) - - // track initial balance - initBal, err := client.Balance(pubKey) - assert.NoError(t, err) - assert.NotEqual(t, uint64(0), initBal) // should be funded - - // start - require.NoError(t, txm.Start(ctx)) - - // already started - assert.Error(t, txm.Start(ctx)) - - createTx := func(signer solana.PublicKey, sender solana.PublicKey, receiver solana.PublicKey, amt uint64) *solana.Transaction { - // create transfer tx - hash, err := client.LatestBlockhash() - assert.NoError(t, err) - tx, err := solana.NewTransaction( - []solana.Instruction{ - system.NewTransferInstruction( - amt, - sender, - receiver, - ).Build(), - }, - hash.Value.Blockhash, - solana.TransactionPayer(signer), - ) - require.NoError(t, err) - return tx - } - - // enqueue txs (must pass to move on to load test) - require.NoError(t, txm.Enqueue("test_success_0", createTx(pubKey, pubKey, pubKeyReceiver, solana.LAMPORTS_PER_SOL))) - require.Error(t, txm.Enqueue("test_invalidSigner", createTx(pubKeyReceiver, pubKey, pubKeyReceiver, solana.LAMPORTS_PER_SOL))) // cannot sign tx before enqueuing - require.NoError(t, txm.Enqueue("test_invalidReceiver", createTx(pubKey, pubKey, solana.PublicKey{}, solana.LAMPORTS_PER_SOL))) - time.Sleep(500 * time.Millisecond) // pause 0.5s for new blockhash - require.NoError(t, txm.Enqueue("test_success_1", createTx(pubKey, pubKey, pubKeyReceiver, solana.LAMPORTS_PER_SOL))) - require.NoError(t, txm.Enqueue("test_txFail", createTx(pubKey, pubKey, pubKeyReceiver, 1000*solana.LAMPORTS_PER_SOL))) - - // load test: try to overload txs, confirm, or simulation - for i := 0; i < 1000; i++ { - assert.NoError(t, txm.Enqueue(fmt.Sprintf("load_%d", i), createTx(loadTestKey.PublicKey(), loadTestKey.PublicKey(), loadTestKey.PublicKey(), uint64(i)))) - time.Sleep(10 * time.Millisecond) // ~100 txs per second (note: have run 5ms delays for ~200tx/s succesfully) - } - - // check to make sure all txs are closed out from inflight list (longest should last MaxConfirmTimeout) - ctx, cancel := context.WithCancel(ctx) - t.Cleanup(cancel) - ticker := time.NewTicker(time.Second) - defer ticker.Stop() -loop: - for { - select { - case <-ctx.Done(): - assert.Equal(t, 0, txm.InflightTxs()) - break loop - case <-ticker.C: - if txm.InflightTxs() == 0 { - cancel() // exit for loop - } - } - } - assert.NoError(t, txm.Close()) - - // check balance changes - senderBal, err := client.Balance(pubKey) - assert.NoError(t, err) - assert.Greater(t, initBal, senderBal) - assert.Greater(t, initBal-senderBal, 2*solana.LAMPORTS_PER_SOL) // balance change = sent + fees - - receiverBal, err := client.Balance(pubKeyReceiver) - assert.NoError(t, err) - assert.Equal(t, 2*solana.LAMPORTS_PER_SOL, receiverBal) -} diff --git a/core/chains/solana/soltxm/utils.go b/core/chains/solana/soltxm/utils.go deleted file mode 100644 index ee185a2c7d0..00000000000 --- a/core/chains/solana/soltxm/utils.go +++ /dev/null @@ -1,71 +0,0 @@ -package soltxm - -import ( - "fmt" - "sort" - - "github.com/gagliardetto/solana-go" - "github.com/gagliardetto/solana-go/rpc" -) - -// tx not found -// < tx processed -// < tx confirmed/finalized + revert -// < tx confirmed/finalized + success -const ( - NotFound = iota - Processed - ConfirmedRevert - ConfirmedSuccess -) - -type statuses struct { - sigs []solana.Signature - res []*rpc.SignatureStatusesResult -} - -func (s statuses) Len() int { - return len(s.res) -} - -func (s statuses) Swap(i, j int) { - s.sigs[i], s.sigs[j] = s.sigs[j], s.sigs[i] - s.res[i], s.res[j] = s.res[j], s.res[i] -} - -func (s statuses) Less(i, j int) bool { - return convertStatus(s.res[i]) > convertStatus(s.res[j]) // returns list with highest first -} - -func SortSignaturesAndResults(sigs []solana.Signature, res []*rpc.SignatureStatusesResult) ([]solana.Signature, []*rpc.SignatureStatusesResult, error) { - if len(sigs) != len(res) { - return []solana.Signature{}, []*rpc.SignatureStatusesResult{}, fmt.Errorf("signatures and results lengths do not match") - } - - s := statuses{ - sigs: sigs, - res: res, - } - sort.Sort(s) - return s.sigs, s.res, nil -} - -func convertStatus(res *rpc.SignatureStatusesResult) uint { - if res == nil { - return NotFound - } - - if res.ConfirmationStatus == rpc.ConfirmationStatusProcessed { - return Processed - } - - if res.ConfirmationStatus == rpc.ConfirmationStatusConfirmed || - res.ConfirmationStatus == rpc.ConfirmationStatusFinalized { - if res.Err != nil { - return ConfirmedRevert - } - return ConfirmedSuccess - } - - return NotFound -} diff --git a/core/chains/solana/soltxm/utils_test.go b/core/chains/solana/soltxm/utils_test.go deleted file mode 100644 index 13eb03c7dbf..00000000000 --- a/core/chains/solana/soltxm/utils_test.go +++ /dev/null @@ -1,40 +0,0 @@ -package soltxm - -import ( - "testing" - - "github.com/gagliardetto/solana-go" - "github.com/gagliardetto/solana-go/rpc" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestSortSignaturesAndResults(t *testing.T) { - sig := []solana.Signature{ - {0}, {1}, {2}, {3}, - } - - statuses := []*rpc.SignatureStatusesResult{ - {ConfirmationStatus: rpc.ConfirmationStatusProcessed}, - {ConfirmationStatus: rpc.ConfirmationStatusConfirmed}, - nil, - {ConfirmationStatus: rpc.ConfirmationStatusConfirmed, Err: "ERROR"}, - } - - _, _, err := SortSignaturesAndResults([]solana.Signature{}, statuses) - require.Error(t, err) - - sig, statuses, err = SortSignaturesAndResults(sig, statuses) - require.NoError(t, err) - - // new expected order [1, 3, 0, 2] - assert.Equal(t, rpc.SignatureStatusesResult{ConfirmationStatus: rpc.ConfirmationStatusConfirmed}, *statuses[0]) - assert.Equal(t, rpc.SignatureStatusesResult{ConfirmationStatus: rpc.ConfirmationStatusConfirmed, Err: "ERROR"}, *statuses[1]) - assert.Equal(t, rpc.SignatureStatusesResult{ConfirmationStatus: rpc.ConfirmationStatusProcessed}, *statuses[2]) - assert.True(t, nil == statuses[3]) - - assert.Equal(t, solana.Signature{1}, sig[0]) - assert.Equal(t, solana.Signature{3}, sig[1]) - assert.Equal(t, solana.Signature{0}, sig[2]) - assert.Equal(t, solana.Signature{2}, sig[3]) -} diff --git a/core/services/keystore/mocks/solana.go b/core/services/keystore/mocks/solana.go index f91b89d287b..ba188af0947 100644 --- a/core/services/keystore/mocks/solana.go +++ b/core/services/keystore/mocks/solana.go @@ -3,8 +3,11 @@ package mocks import ( - solkey "github.com/smartcontractkit/chainlink/core/services/keystore/keys/solkey" + context "context" + mock "github.com/stretchr/testify/mock" + + solkey "github.com/smartcontractkit/chainlink/core/services/keystore/keys/solkey" ) // Solana is an autogenerated mock type for the Solana type @@ -188,6 +191,32 @@ func (_m *Solana) Import(keyJSON []byte, password string) (solkey.Key, error) { return r0, r1 } +// Sign provides a mock function with given fields: ctx, id, msg +func (_m *Solana) Sign(ctx context.Context, id string, msg []byte) ([]byte, error) { + ret := _m.Called(ctx, id, msg) + + var r0 []byte + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string, []byte) ([]byte, error)); ok { + return rf(ctx, id, msg) + } + if rf, ok := ret.Get(0).(func(context.Context, string, []byte) []byte); ok { + r0 = rf(ctx, id, msg) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]byte) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, string, []byte) error); ok { + r1 = rf(ctx, id, msg) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + type mockConstructorTestingTNewSolana interface { mock.TestingT Cleanup(func()) diff --git a/core/services/keystore/solana.go b/core/services/keystore/solana.go index dc9a3381f5d..c351e0cd9a6 100644 --- a/core/services/keystore/solana.go +++ b/core/services/keystore/solana.go @@ -1,6 +1,7 @@ package keystore import ( + "context" "fmt" "github.com/pkg/errors" @@ -19,6 +20,7 @@ type Solana interface { Import(keyJSON []byte, password string) (solkey.Key, error) Export(id string, password string) ([]byte, error) EnsureKey() error + Sign(ctx context.Context, id string, msg []byte) (signature []byte, err error) } type solana struct { @@ -142,6 +144,14 @@ func (ks *solana) EnsureKey() error { return ks.safeAddKey(key) } +func (ks *solana) Sign(_ context.Context, id string, msg []byte) (signature []byte, err error) { + k, err := ks.Get(id) + if err != nil { + return nil, err + } + return k.Sign(msg) +} + var ( ErrNoSolanaKey = errors.New("no solana keys exist") ) diff --git a/core/services/keystore/solana_test.go b/core/services/keystore/solana_test.go index 8ffbe5ce2db..f9107f69525 100644 --- a/core/services/keystore/solana_test.go +++ b/core/services/keystore/solana_test.go @@ -7,6 +7,7 @@ import ( "github.com/stretchr/testify/require" "github.com/smartcontractkit/chainlink/core/internal/cltest" + "github.com/smartcontractkit/chainlink/core/internal/testutils" configtest "github.com/smartcontractkit/chainlink/core/internal/testutils/configtest/v2" "github.com/smartcontractkit/chainlink/core/internal/testutils/pgtest" "github.com/smartcontractkit/chainlink/core/services/keystore" @@ -107,4 +108,26 @@ func Test_SolanaKeyStore_E2E(t *testing.T) { require.NoError(t, err) require.Equal(t, 1, len(keys)) }) + + t.Run("sign tx", func(t *testing.T) { + defer reset() + newKey, err := solkey.New() + require.NoError(t, err) + require.NoError(t, ks.Add(newKey)) + + // sign unknown ID + _, err = ks.Sign(testutils.Context(t), "not-real", nil) + assert.Error(t, err) + + // sign known key + payload := []byte{1} + sig, err := ks.Sign(testutils.Context(t), newKey.ID(), payload) + require.NoError(t, err) + + directSig, err := newKey.Sign(payload) + require.NoError(t, err) + + // signatures should match using keystore sign or key sign + assert.Equal(t, directSig, sig) + }) } diff --git a/go.mod b/go.mod index 06de64ba975..f94319fd9af 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,7 @@ require ( github.com/Depado/ginprom v1.7.4 github.com/Masterminds/semver/v3 v3.2.0 github.com/ava-labs/coreth v0.11.0-rc.4 - github.com/btcsuite/btcd v0.23.1 + github.com/btcsuite/btcd v0.23.4 github.com/danielkov/gin-helmet v0.0.0-20171108135313-1387e224435e github.com/docker/docker v20.10.18+incompatible github.com/docker/go-connections v0.4.0 @@ -56,8 +56,8 @@ require ( github.com/scylladb/go-reflectx v1.0.1 github.com/shirou/gopsutil/v3 v3.22.12 github.com/shopspring/decimal v1.3.1 - github.com/smartcontractkit/chainlink-relay v0.1.7-0.20230221200635-404a44389f85 - github.com/smartcontractkit/chainlink-solana v1.0.3-0.20230221200929-d415eda78bff + github.com/smartcontractkit/chainlink-relay v0.1.7-0.20230309154839-2b6a5b078888 + github.com/smartcontractkit/chainlink-solana v1.0.3-0.20230313192900-6270bc7c445f github.com/smartcontractkit/chainlink-starknet/relayer v0.0.0-20230223033525-5be75fb81118 github.com/smartcontractkit/libocr v0.0.0-20221209172631-568a30f68407 github.com/smartcontractkit/ocr2keepers v0.6.14 @@ -67,7 +67,7 @@ require ( github.com/spf13/cast v1.5.0 github.com/spf13/cobra v1.5.0 github.com/spf13/viper v1.12.0 - github.com/stretchr/testify v1.8.1 + github.com/stretchr/testify v1.8.2 github.com/tendermint/tendermint v0.34.15 github.com/theodesp/go-heaps v0.0.0-20190520121037-88e35354fe0a github.com/tidwall/gjson v1.14.4 @@ -81,7 +81,7 @@ require ( go.uber.org/multierr v1.9.0 go.uber.org/zap v1.24.0 golang.org/x/crypto v0.6.0 - golang.org/x/exp v0.0.0-20230213192124-5e25df0256eb + golang.org/x/exp v0.0.0-20230224173230-c95f2b4c22f2 golang.org/x/sync v0.1.0 golang.org/x/term v0.5.0 golang.org/x/text v0.7.0 diff --git a/go.sum b/go.sum index 16890de19cd..0f6d7f03c31 100644 --- a/go.sum +++ b/go.sum @@ -1298,10 +1298,10 @@ github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0= github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= -github.com/smartcontractkit/chainlink-relay v0.1.7-0.20230221200635-404a44389f85 h1:zNpMPZQUW9hfDMUPc3PRbeDuRyMyT5QfLUyZd8cSYyE= -github.com/smartcontractkit/chainlink-relay v0.1.7-0.20230221200635-404a44389f85/go.mod h1:Uqt8amCr4U4/6n+pvr1OMlNBzSqYSr6oeWAaBsBdPYE= -github.com/smartcontractkit/chainlink-solana v1.0.3-0.20230221200929-d415eda78bff h1:132GK++RRDRTSNCkxjasyoQ4JVDpFIty1Tz3waho+rQ= -github.com/smartcontractkit/chainlink-solana v1.0.3-0.20230221200929-d415eda78bff/go.mod h1:ysqElsllDsTwFipIy1Lc7GqCS3FVOcjXVfZq8/Boh2g= +github.com/smartcontractkit/chainlink-relay v0.1.7-0.20230309154839-2b6a5b078888 h1:EGMHKJWQtjjJM3+FaPJMipvxGVio1P66y5NyqgBGoMk= +github.com/smartcontractkit/chainlink-relay v0.1.7-0.20230309154839-2b6a5b078888/go.mod h1:Uqt8amCr4U4/6n+pvr1OMlNBzSqYSr6oeWAaBsBdPYE= +github.com/smartcontractkit/chainlink-solana v1.0.3-0.20230313192900-6270bc7c445f h1:8u/sTyTOcokP/QQH0IW1ifzwxSk3eT+MntovfirkCgE= +github.com/smartcontractkit/chainlink-solana v1.0.3-0.20230313192900-6270bc7c445f/go.mod h1:cigIrApl6WcvLhaYeevIWc91PyW2NAJRcmbmBMaIZg4= github.com/smartcontractkit/chainlink-starknet/ops v0.0.0-20230214070706-9544d04bb4d8 h1:GRx8L71lgGIeTYIbgsXGQ/JFWKPEnAmJVO42zQ3n69A= github.com/smartcontractkit/chainlink-starknet/relayer v0.0.0-20230223033525-5be75fb81118 h1:Srn9VdZq4xYuFWm96AvzmYTvg8Q7P3Eu7ztdVxtZsrU= github.com/smartcontractkit/chainlink-starknet/relayer v0.0.0-20230223033525-5be75fb81118/go.mod h1:UPMx1+qH6/T+6DMf3x3ReiD2EJuQOuaG5iIUUPEKL9U= @@ -1376,8 +1376,9 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= +github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= github.com/subosito/gotenv v1.3.0 h1:mjC+YW8QpAdXibNi+vNWgzmgBH4+5l5dCXv8cNysBLI= github.com/subosito/gotenv v1.3.0/go.mod h1:YzJjq/33h7nrwdY+iHMhEOEEbW0ovIz0tB6t6PwAXzs= @@ -1582,8 +1583,8 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0 golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= -golang.org/x/exp v0.0.0-20230213192124-5e25df0256eb h1:PaBZQdo+iSDyHT053FjUCgZQ/9uqVwPOcl7KSWhKn6w= -golang.org/x/exp v0.0.0-20230213192124-5e25df0256eb/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= +golang.org/x/exp v0.0.0-20230224173230-c95f2b4c22f2 h1:Jvc7gsqn21cJHCmAWx0LiimpP18LZmUxkT5Mp7EZ1mI= +golang.org/x/exp v0.0.0-20230224173230-c95f2b4c22f2/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= diff --git a/integration-tests/go.mod b/integration-tests/go.mod index f374cd233e0..77932cc60bd 100644 --- a/integration-tests/go.mod +++ b/integration-tests/go.mod @@ -18,7 +18,7 @@ require ( github.com/smartcontractkit/libocr v0.0.0-20221209172631-568a30f68407 github.com/smartcontractkit/ocr2keepers v0.6.14 github.com/smartcontractkit/ocr2vrf v0.0.0-20230221012516-b4187fdffa0c - github.com/stretchr/testify v1.8.1 + github.com/stretchr/testify v1.8.2 github.com/umbracle/ethgo v0.1.3 go.dedis.ch/kyber/v3 v3.0.14 go.uber.org/zap v1.24.0 @@ -44,7 +44,7 @@ require ( github.com/beorn7/perks v1.0.1 // indirect github.com/blang/semver v3.5.1+incompatible // indirect github.com/blendle/zapdriver v1.3.1 // indirect - github.com/btcsuite/btcd v0.23.1 // indirect + github.com/btcsuite/btcd v0.23.4 // indirect github.com/btcsuite/btcd/btcec/v2 v2.3.2 // indirect github.com/c2h5oh/datasize v0.0.0-20200112174442-28bbd4740fee // indirect github.com/cdk8s-team/cdk8s-core-go/cdk8s/v2 v2.7.5 // indirect @@ -250,7 +250,7 @@ require ( github.com/shirou/gopsutil/v3 v3.22.12 // indirect github.com/shopspring/decimal v1.3.1 // indirect github.com/sirupsen/logrus v1.9.0 // indirect - github.com/smartcontractkit/chainlink-relay v0.1.7-0.20230221200635-404a44389f85 // indirect + github.com/smartcontractkit/chainlink-relay v0.1.7-0.20230309154839-2b6a5b078888 // indirect github.com/smartcontractkit/chainlink-starknet/relayer v0.0.0-20230223033525-5be75fb81118 // indirect github.com/smartcontractkit/sqlx v1.3.5-0.20210805004948-4be295aacbeb // indirect github.com/spacemonkeygo/spacelog v0.0.0-20180420211403-2296661a0572 // indirect @@ -288,7 +288,7 @@ require ( go.uber.org/multierr v1.9.0 // indirect go.uber.org/ratelimit v0.2.0 // indirect golang.org/x/crypto v0.6.0 // indirect - golang.org/x/exp v0.0.0-20230213192124-5e25df0256eb // indirect + golang.org/x/exp v0.0.0-20230224173230-c95f2b4c22f2 // indirect golang.org/x/lint v0.0.0-20210508222113-6edffad5e616 // indirect golang.org/x/mod v0.7.0 // indirect golang.org/x/net v0.7.0 // indirect diff --git a/integration-tests/go.sum b/integration-tests/go.sum index eeef79b6229..90b4ace75a5 100644 --- a/integration-tests/go.sum +++ b/integration-tests/go.sum @@ -1781,9 +1781,9 @@ github.com/slack-go/slack v0.12.1 h1:X97b9g2hnITDtNsNe5GkGx6O2/Sz/uC20ejRZN6QxOw github.com/slack-go/slack v0.12.1/go.mod h1:hlGi5oXA+Gt+yWTPP0plCdRKmjsDxecdHxYQdlMQKOw= github.com/smartcontractkit/chainlink-env v0.3.17 h1:LyWRNtfbJGMkOJsTewTWdAZwfZfJSfoPc9nwJNVSL14= github.com/smartcontractkit/chainlink-env v0.3.17/go.mod h1:9c0Czq4a6wZKY20BcoAlK29DnejQIiLo/MwKYtSFnHk= -github.com/smartcontractkit/chainlink-relay v0.1.7-0.20230221200635-404a44389f85 h1:zNpMPZQUW9hfDMUPc3PRbeDuRyMyT5QfLUyZd8cSYyE= -github.com/smartcontractkit/chainlink-relay v0.1.7-0.20230221200635-404a44389f85/go.mod h1:Uqt8amCr4U4/6n+pvr1OMlNBzSqYSr6oeWAaBsBdPYE= -github.com/smartcontractkit/chainlink-solana v1.0.3-0.20230221200929-d415eda78bff h1:132GK++RRDRTSNCkxjasyoQ4JVDpFIty1Tz3waho+rQ= +github.com/smartcontractkit/chainlink-relay v0.1.7-0.20230309154839-2b6a5b078888 h1:EGMHKJWQtjjJM3+FaPJMipvxGVio1P66y5NyqgBGoMk= +github.com/smartcontractkit/chainlink-relay v0.1.7-0.20230309154839-2b6a5b078888/go.mod h1:Uqt8amCr4U4/6n+pvr1OMlNBzSqYSr6oeWAaBsBdPYE= +github.com/smartcontractkit/chainlink-solana v1.0.3-0.20230313192900-6270bc7c445f h1:8u/sTyTOcokP/QQH0IW1ifzwxSk3eT+MntovfirkCgE= github.com/smartcontractkit/chainlink-starknet/relayer v0.0.0-20230223033525-5be75fb81118 h1:Srn9VdZq4xYuFWm96AvzmYTvg8Q7P3Eu7ztdVxtZsrU= github.com/smartcontractkit/chainlink-starknet/relayer v0.0.0-20230223033525-5be75fb81118/go.mod h1:UPMx1+qH6/T+6DMf3x3ReiD2EJuQOuaG5iIUUPEKL9U= github.com/smartcontractkit/chainlink-testing-framework v1.10.9-0.20230301123015-fa17d1a4dc61 h1:gHU9G2SGqY76Xkbsp1l4AtcFh/mFASbo9TIESE4f0So= @@ -1855,8 +1855,9 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= +github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ= github.com/syndtr/goleveldb v1.0.1-0.20220614013038-64ee5596c38a h1:1ur3QoCqvE5fl+nylMaIr9PVV1w343YRDtsy+Rwu7XI= @@ -2095,8 +2096,8 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0 golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= -golang.org/x/exp v0.0.0-20230213192124-5e25df0256eb h1:PaBZQdo+iSDyHT053FjUCgZQ/9uqVwPOcl7KSWhKn6w= -golang.org/x/exp v0.0.0-20230213192124-5e25df0256eb/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= +golang.org/x/exp v0.0.0-20230224173230-c95f2b4c22f2 h1:Jvc7gsqn21cJHCmAWx0LiimpP18LZmUxkT5Mp7EZ1mI= +golang.org/x/exp v0.0.0-20230224173230-c95f2b4c22f2/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=