Skip to content

Commit

Permalink
assemble transaction with configurable parameters
Browse files Browse the repository at this point in the history
  • Loading branch information
aalu1418 committed Sep 25, 2024
1 parent 3745640 commit 8007501
Show file tree
Hide file tree
Showing 9 changed files with 124 additions and 22 deletions.
5 changes: 2 additions & 3 deletions .tool-versions
Original file line number Diff line number Diff line change
@@ -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
Expand Down
1 change: 0 additions & 1 deletion pkg/solana/fees/computebudget.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
1 change: 0 additions & 1 deletion pkg/solana/fees/computebudget_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
2 changes: 1 addition & 1 deletion pkg/solana/relay.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
65 changes: 51 additions & 14 deletions pkg/solana/txm/txm.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down Expand Up @@ -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)
Expand All @@ -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 {
Expand All @@ -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

Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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")
Expand All @@ -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 {
Expand Down Expand Up @@ -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(),
}
}
7 changes: 6 additions & 1 deletion pkg/solana/txm/txm_internal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down
8 changes: 7 additions & 1 deletion pkg/solana/txm/txm_race_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand All @@ -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)

Expand Down Expand Up @@ -205,26 +207,30 @@ 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)

// 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"))

// 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)

Expand Down
34 changes: 34 additions & 0 deletions pkg/solana/txm/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"fmt"
"sort"
"sync"
"time"

"github.com/gagliardetto/solana-go"
"github.com/gagliardetto/solana-go/rpc"
Expand Down Expand Up @@ -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
}
}
23 changes: 23 additions & 0 deletions pkg/solana/txm/utils_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package txm
import (
"sync"
"testing"
"time"

"github.com/gagliardetto/solana-go"
"github.com/gagliardetto/solana-go/rpc"
Expand Down Expand Up @@ -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)
}

0 comments on commit 8007501

Please sign in to comment.