diff --git a/.tool-versions b/.tool-versions index 3e82cc776..44df8fe5f 100644 --- a/.tool-versions +++ b/.tool-versions @@ -1,9 +1,8 @@ nodejs 18.20.2 yarn 1.22.19 rust 1.59.0 -golang 1.21.7 -golangci-lint 1.55.2 -pulumi 3.40.1 +golang 1.22.5 +golangci-lint 1.60.1 actionlint 1.6.22 shellcheck 0.8.0 helm 3.9.4 diff --git a/pkg/solana/fees/computebudget.go b/pkg/solana/fees/computebudget.go index 832c130e5..b85a125e8 100644 --- a/pkg/solana/fees/computebudget.go +++ b/pkg/solana/fees/computebudget.go @@ -100,7 +100,6 @@ func encode[V constraints.Unsigned](identifier computeBudgetInstruction, val V) func ParseComputeUnitPrice(data []byte) (ComputeUnitPrice, error) { v, err := parse(InstructionSetComputeUnitPrice, data, binary.LittleEndian.Uint64) return ComputeUnitPrice(v), err - } func ParseComputeUnitLimit(data []byte) (ComputeUnitLimit, error) { diff --git a/pkg/solana/fees/computebudget_test.go b/pkg/solana/fees/computebudget_test.go index 82f2bd294..c5cacabd4 100644 --- a/pkg/solana/fees/computebudget_test.go +++ b/pkg/solana/fees/computebudget_test.go @@ -23,7 +23,6 @@ func TestSet(t *testing.T) { return ComputeUnitLimit(v) }, SetComputeUnitLimit, false) }) - } func testSet[V instruction](t *testing.T, builder func(uint) V, setter func(*solana.Transaction, V) error, expectFirstInstruction bool) { diff --git a/pkg/solana/relay.go b/pkg/solana/relay.go index 9121b185f..ec7aec9f5 100644 --- a/pkg/solana/relay.go +++ b/pkg/solana/relay.go @@ -23,7 +23,7 @@ import ( var _ TxManager = (*txm.Txm)(nil) type TxManager interface { - Enqueue(accountID string, msg *solana.Transaction) error + Enqueue(accountID string, msg *solana.Transaction, txCfgs ...txm.SetTxConfig) error } var _ relaytypes.Relayer = &Relayer{} //nolint:staticcheck diff --git a/pkg/solana/txm/txm.go b/pkg/solana/txm/txm.go index a5700bd8b..50d153be3 100644 --- a/pkg/solana/txm/txm.go +++ b/pkg/solana/txm/txm.go @@ -50,9 +50,21 @@ type Txm struct { fee fees.Estimator } +type TxConfig struct { + Timeout time.Duration // transaction broadcast timeout + + // compute unit price config + FeeBumpPeriod time.Duration // how often to bump fee + BaseComputeUnitPrice uint64 // starting price + ComputeUnitPriceMin uint64 // min price + ComputeUnitPriceMax uint64 // max price + + ComputeUnitLimit uint32 // compute unit limit +} + type pendingTx struct { tx *solanaGo.Transaction - timeout time.Duration + cfg TxConfig signature solanaGo.Signature id uuid.UUID } @@ -112,7 +124,7 @@ func (txm *Txm) run() { select { case msg := <-txm.chSend: // process tx (pass tx copy) - tx, id, sig, err := txm.sendWithRetry(ctx, *msg.tx, msg.timeout) + tx, id, sig, err := txm.sendWithRetry(ctx, *msg.tx, msg.cfg) if err != nil { txm.lggr.Errorw("failed to send transaction", "error", err) txm.client.Reset() // clear client if tx fails immediately (potentially bad RPC) @@ -136,7 +148,7 @@ func (txm *Txm) run() { } } -func (txm *Txm) sendWithRetry(chanCtx context.Context, baseTx solanaGo.Transaction, timeout time.Duration) (solanaGo.Transaction, uuid.UUID, solanaGo.Signature, error) { +func (txm *Txm) sendWithRetry(chanCtx context.Context, baseTx solanaGo.Transaction, txcfg TxConfig) (solanaGo.Transaction, uuid.UUID, solanaGo.Signature, error) { // fetch client client, clientErr := txm.client.Get() if clientErr != nil { @@ -148,19 +160,23 @@ func (txm *Txm) sendWithRetry(chanCtx context.Context, baseTx solanaGo.Transacti // https://github.com/gagliardetto/solana-go/blob/main/transaction.go#L252 key := baseTx.Message.AccountKeys[0].String() - // only calculate base price once + // base compute unit price should only be calculated once // prevent underlying base changing when bumping (could occur with RPC based estimation) - basePrice := txm.fee.BaseComputeUnitPrice() getFee := func(count uint) fees.ComputeUnitPrice { fee := fees.CalculateFee( - basePrice, - txm.cfg.ComputeUnitPriceMax(), - txm.cfg.ComputeUnitPriceMin(), + txcfg.BaseComputeUnitPrice, + txcfg.ComputeUnitPriceMax, + txcfg.ComputeUnitPriceMin, count, ) return fees.ComputeUnitPrice(fee) } + // add compute unit limit instruction - static for the transaction + if computeUnitLimitErr := fees.SetComputeUnitLimit(&baseTx, fees.ComputeUnitLimit(txcfg.ComputeUnitLimit)); computeUnitLimitErr != nil { + return solanaGo.Transaction{}, uuid.Nil, solanaGo.Signature{}, fmt.Errorf("failed to add compute unit limit instruction: %w", computeUnitLimitErr) + } + buildTx := func(base solanaGo.Transaction, retryCount uint) (solanaGo.Transaction, error) { newTx := base // make copy @@ -192,7 +208,7 @@ func (txm *Txm) sendWithRetry(chanCtx context.Context, baseTx solanaGo.Transacti } // create timeout context - ctx, cancel := context.WithTimeout(chanCtx, timeout) + ctx, cancel := context.WithTimeout(chanCtx, txcfg.Timeout) // send initial tx (do not retry and exit early if fails) sig, initSendErr := client.SendTx(ctx, &initTx) @@ -238,7 +254,7 @@ func (txm *Txm) sendWithRetry(chanCtx context.Context, baseTx solanaGo.Transacti case <-tick: var shouldBump bool // bump if period > 0 and past time - if txm.cfg.FeeBumpPeriod() != 0 && time.Since(bumpTime) > txm.cfg.FeeBumpPeriod() { + if txcfg.FeeBumpPeriod != 0 && time.Since(bumpTime) > txcfg.FeeBumpPeriod { bumpCount++ bumpTime = time.Now() shouldBump = true @@ -503,7 +519,11 @@ func (txm *Txm) simulate(ctx context.Context) { } // Enqueue enqueue a msg destined for the solana chain. -func (txm *Txm) Enqueue(accountID string, tx *solanaGo.Transaction) error { +func (txm *Txm) Enqueue(accountID string, tx *solanaGo.Transaction, txCfgs ...SetTxConfig) error { + if err := txm.Ready(); err != nil { + return fmt.Errorf("error in soltxm.Enqueue: %w", err) + } + // validate nil pointer if tx == nil { return errors.New("error in soltxm.Enqueue: tx is nil pointer") @@ -521,9 +541,15 @@ func (txm *Txm) Enqueue(accountID string, tx *solanaGo.Transaction) error { return fmt.Errorf("error in soltxm.Enqueue.GetKey: %w", err) } + // apply changes to default config + cfg := txm.defaultTxConfig() + for _, v := range txCfgs { + v(&cfg) + } + msg := pendingTx{ - tx: tx, - timeout: txm.cfg.TxRetryTimeout(), + tx: tx, + cfg: cfg, } select { @@ -556,7 +582,18 @@ func (txm *Txm) Healthy() error { // Ready service is ready func (txm *Txm) Ready() error { - return nil + return txm.starter.Ready() } func (txm *Txm) HealthReport() map[string]error { return map[string]error{txm.Name(): txm.Healthy()} } + +func (txm *Txm) defaultTxConfig() TxConfig { + return TxConfig{ + Timeout: txm.cfg.TxRetryTimeout(), + FeeBumpPeriod: txm.cfg.FeeBumpPeriod(), + BaseComputeUnitPrice: txm.fee.BaseComputeUnitPrice(), + ComputeUnitPriceMin: txm.cfg.ComputeUnitPriceMin(), + ComputeUnitPriceMax: txm.cfg.ComputeUnitPriceMax(), + ComputeUnitLimit: txm.cfg.ComputeUnitLimitDefault(), + } +} diff --git a/pkg/solana/txm/txm_internal_test.go b/pkg/solana/txm/txm_internal_test.go index 3f370898e..0b3f62bfc 100644 --- a/pkg/solana/txm/txm_internal_test.go +++ b/pkg/solana/txm/txm_internal_test.go @@ -71,8 +71,9 @@ func getTx(t *testing.T, val uint64, keystore SimpleKeystore, price fees.Compute return &base, func(price fees.ComputeUnitPrice) *solana.Transaction { tx := base - // add fee + // add fee parameters require.NoError(t, fees.SetComputeUnitPrice(&tx, price)) + require.NoError(t, fees.SetComputeUnitLimit(&tx, 200_000)) // default // sign tx txMsg, err := tx.Message.MarshalBinary() @@ -646,6 +647,7 @@ func TestTxm_Enqueue(t *testing.T) { lggr := logger.Test(t) cfg := config.NewDefault() mc := mocks.NewReaderWriter(t) + ctx := tests.Context(t) // mock solana keystore mkey := keyMocks.NewSimpleKeystore(t) @@ -685,6 +687,9 @@ func TestTxm_Enqueue(t *testing.T) { return mc, nil }, cfg, mkey, lggr) + require.ErrorContains(t, txm.Enqueue("txmUnstarted", &solana.Transaction{}), "not started") + require.NoError(t, txm.Start(ctx)) + txs := []struct { name string tx *solana.Transaction diff --git a/pkg/solana/txm/txm_race_test.go b/pkg/solana/txm/txm_race_test.go index 2a49eb58b..8c70dd45a 100644 --- a/pkg/solana/txm/txm_race_test.go +++ b/pkg/solana/txm/txm_race_test.go @@ -51,6 +51,8 @@ func TestTxm_SendWithRetry_Race(t *testing.T) { cfg.On("ComputeUnitPriceMax").Return(uint64(10)) cfg.On("ComputeUnitPriceMin").Return(uint64(0)) cfg.On("FeeBumpPeriod").Return(txRetryDuration / 6) + cfg.On("TxRetryTimeout").Return(txRetryDuration) + cfg.On("ComputeUnitLimitDefault").Return(uint32(0)) // keystore mock ks.On("Sign", mock.Anything, mock.Anything, mock.Anything).Return([]byte{}, nil) @@ -69,7 +71,7 @@ func TestTxm_SendWithRetry_Race(t *testing.T) { _, _, _, err := txm.sendWithRetry( tests.Context(t), tx, - txRetryDuration, + txm.defaultTxConfig(), ) require.NoError(t, err) @@ -205,12 +207,14 @@ func TestTxm_SendWithRetry_Race(t *testing.T) { // client mock - first tx is always successful tx0 := NewTestTx() require.NoError(t, fees.SetComputeUnitPrice(&tx0, 0)) + require.NoError(t, fees.SetComputeUnitLimit(&tx0, 0)) tx0.Signatures = make([]solanaGo.Signature, 1) client.On("SendTx", mock.Anything, &tx0).Return(solanaGo.Signature{1}, nil) // init bump tx fails, rebroadcast is successful tx1 := NewTestTx() require.NoError(t, fees.SetComputeUnitPrice(&tx1, 1)) + require.NoError(t, fees.SetComputeUnitLimit(&tx1, 0)) tx1.Signatures = make([]solanaGo.Signature, 1) client.On("SendTx", mock.Anything, &tx1).Return(solanaGo.Signature{}, fmt.Errorf("BUMP FAILED")).Once() client.On("SendTx", mock.Anything, &tx1).Return(solanaGo.Signature{2}, nil) @@ -218,6 +222,7 @@ func TestTxm_SendWithRetry_Race(t *testing.T) { // init bump tx success, rebroadcast fails tx2 := NewTestTx() require.NoError(t, fees.SetComputeUnitPrice(&tx2, 2)) + require.NoError(t, fees.SetComputeUnitLimit(&tx2, 0)) tx2.Signatures = make([]solanaGo.Signature, 1) client.On("SendTx", mock.Anything, &tx2).Return(solanaGo.Signature{3}, nil).Once() client.On("SendTx", mock.Anything, &tx2).Return(solanaGo.Signature{}, fmt.Errorf("REBROADCAST FAILED")) @@ -225,6 +230,7 @@ func TestTxm_SendWithRetry_Race(t *testing.T) { // always successful tx3 := NewTestTx() require.NoError(t, fees.SetComputeUnitPrice(&tx3, 4)) + require.NoError(t, fees.SetComputeUnitLimit(&tx3, 0)) tx3.Signatures = make([]solanaGo.Signature, 1) client.On("SendTx", mock.Anything, &tx3).Return(solanaGo.Signature{4}, nil) diff --git a/pkg/solana/txm/utils.go b/pkg/solana/txm/utils.go index a1f437233..5f955c1f3 100644 --- a/pkg/solana/txm/utils.go +++ b/pkg/solana/txm/utils.go @@ -5,6 +5,7 @@ import ( "fmt" "sort" "sync" + "time" "github.com/gagliardetto/solana-go" "github.com/gagliardetto/solana-go/rpc" @@ -145,3 +146,36 @@ func (s *signatureList) Wait(index int) { wg.Wait() } + +type SetTxConfig func(*TxConfig) + +func SetTimeout(t time.Duration) SetTxConfig { + return func(cfg *TxConfig) { + cfg.Timeout = t + } +} +func SetFeeBumpPeriod(t time.Duration) SetTxConfig { + return func(cfg *TxConfig) { + cfg.FeeBumpPeriod = t + } +} +func SetBaseComputeUnitPrice(v uint64) SetTxConfig { + return func(cfg *TxConfig) { + cfg.BaseComputeUnitPrice = v + } +} +func SetComputeUnitPriceMin(v uint64) SetTxConfig { + return func(cfg *TxConfig) { + cfg.ComputeUnitPriceMin = v + } +} +func SetComputeUnitPriceMax(v uint64) SetTxConfig { + return func(cfg *TxConfig) { + cfg.ComputeUnitPriceMax = v + } +} +func SetComputeUnitLimit(v uint32) SetTxConfig { + return func(cfg *TxConfig) { + cfg.ComputeUnitLimit = v + } +} diff --git a/pkg/solana/txm/utils_test.go b/pkg/solana/txm/utils_test.go index fd3536bc2..0530495d7 100644 --- a/pkg/solana/txm/utils_test.go +++ b/pkg/solana/txm/utils_test.go @@ -3,6 +3,7 @@ package txm import ( "sync" "testing" + "time" "github.com/gagliardetto/solana-go" "github.com/gagliardetto/solana-go/rpc" @@ -73,3 +74,25 @@ func TestSignatureList_AllocateWaitSet(t *testing.T) { assert.NoError(t, sigs.Set(ind1, solana.Signature{1})) wg.Wait() } + +func TestSetTxConfig(t *testing.T) { + cfg := TxConfig{} + + for _, v := range []SetTxConfig{ + SetTimeout(1 * time.Second), + SetFeeBumpPeriod(2 * time.Second), + SetBaseComputeUnitPrice(3), + SetComputeUnitPriceMin(4), + SetComputeUnitPriceMax(5), + SetComputeUnitLimit(6), + } { + v(&cfg) + } + + assert.Equal(t, 1*time.Second, cfg.Timeout) + assert.Equal(t, 2*time.Second, cfg.FeeBumpPeriod) + assert.Equal(t, uint64(3), cfg.BaseComputeUnitPrice) + assert.Equal(t, uint64(4), cfg.ComputeUnitPriceMin) + assert.Equal(t, uint64(5), cfg.ComputeUnitPriceMax) + assert.Equal(t, uint32(6), cfg.ComputeUnitLimit) +}