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/contracts/tests/ocr2.spec.ts b/contracts/tests/ocr2.spec.ts index 8543b0371..4bc7aaaf5 100644 --- a/contracts/tests/ocr2.spec.ts +++ b/contracts/tests/ocr2.spec.ts @@ -249,6 +249,11 @@ describe("ocr2", () => { ]), }) ); + tx.add( + ComputeBudgetProgram.setComputeUnitLimit({ + units: 200_000, // use default limit + }) + ); try { return await provider.sendAndConfirm(tx, [transmitter]); @@ -914,7 +919,7 @@ describe("ocr2", () => { it("Transmit a bunch of rounds to check ringbuffer wraparound", async () => { for (let i = 2; i <= rounds; i++) { - let transmitTx = await transmit(feed.publicKey, i, i, new BN(i)); + let transmitTx = await transmit(feed.publicKey, i, i, new BN(i), i); await provider.connection.confirmTransaction(transmitTx, "confirmed"); let t = await provider.connection.getTransaction(transmitTx, { diff --git a/contracts/yarn.lock b/contracts/yarn.lock index 753f52453..123bb62da 100644 --- a/contracts/yarn.lock +++ b/contracts/yarn.lock @@ -2,7 +2,7 @@ # yarn lockfile v1 -"@babel/runtime@^7.17.2", "@babel/runtime@^7.23.4", "@babel/runtime@^7.24.8": +"@babel/runtime@^7.17.2", "@babel/runtime@^7.23.4": version "7.25.0" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.25.0.tgz#3af9a91c1b739c569d5d80cc917280919c544ecb" integrity sha512-7dRy4DwXwtzBrPbZflqxnvfxLF8kdZXPkhymtDeFoFqE6ldzjQFgYTtYIFARcLEYDrqfBfYcZt1WqFxRoyC9Rw== @@ -16,6 +16,13 @@ dependencies: regenerator-runtime "^0.14.0" +"@babel/runtime@^7.24.8", "@babel/runtime@^7.25.0": + version "7.25.6" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.25.6.tgz#9afc3289f7184d8d7f98b099884c26317b9264d2" + integrity sha512-VBj9MYyDb9tuLq7yzqjgzt6Q+IBQLrGZfdjOekyEirZPHxXWoTSGUTMrpsfi58Up73d13NfYLv8HT9vmznjzhQ== + dependencies: + regenerator-runtime "^0.14.0" + "@chainlink/solana-sdk@../ts": version "0.2.2" dependencies: @@ -50,7 +57,7 @@ bn.js "^5.1.2" buffer-layout "^1.2.0" -"@noble/curves@^1.2.0", "@noble/curves@^1.4.2": +"@noble/curves@^1.2.0": version "1.5.0" resolved "https://registry.yarnpkg.com/@noble/curves/-/curves-1.5.0.tgz#7a9b9b507065d516e6dce275a1e31db8d2a100dd" integrity sha512-J5EKamIHnKPyClwVrzmaf5wSdQXgdHcPZIZLu3bwnbeCx8/7NPK5q2ZBWF+5FvYGByjiQQsJYX6jfgB2wDPn3A== @@ -64,11 +71,23 @@ dependencies: "@noble/hashes" "1.4.0" -"@noble/hashes@1.4.0", "@noble/hashes@^1.3.1", "@noble/hashes@^1.3.3", "@noble/hashes@^1.4.0": +"@noble/curves@^1.4.2": + version "1.6.0" + resolved "https://registry.yarnpkg.com/@noble/curves/-/curves-1.6.0.tgz#be5296ebcd5a1730fccea4786d420f87abfeb40b" + integrity sha512-TlaHRXDehJuRNR9TfZDNQ45mMEd5dwUwmicsafcIX4SsNiqnCHKjE/1alYPd/lDRVhxdhUAlv8uEhMCI5zjIJQ== + dependencies: + "@noble/hashes" "1.5.0" + +"@noble/hashes@1.4.0", "@noble/hashes@^1.3.1", "@noble/hashes@^1.3.3": version "1.4.0" resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.4.0.tgz#45814aa329f30e4fe0ba49426f49dfccdd066426" integrity sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg== +"@noble/hashes@1.5.0", "@noble/hashes@^1.4.0": + version "1.5.0" + resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.5.0.tgz#abadc5ca20332db2b1b2aa3e496e9af1213570b0" + integrity sha512-1j6kQFb7QRru7eKN3ZDvRcP13rugwdxZqCjbiAVZfIJwgj2A65UmT4TgARXGlXgnRkORLTDTrO19ZErt7+QXgA== + "@solana/buffer-layout-utils@^0.2.0": version "0.2.0" resolved "https://registry.yarnpkg.com/@solana/buffer-layout-utils/-/buffer-layout-utils-0.2.0.tgz#b45a6cab3293a2eb7597cceb474f229889d875ca" @@ -172,12 +191,12 @@ rpc-websockets "^7.5.1" superstruct "^0.14.2" -"@solana/web3.js@^1.50.1", "@solana/web3.js@^1.68.0": - version "1.95.2" - resolved "https://registry.yarnpkg.com/@solana/web3.js/-/web3.js-1.95.2.tgz#6f8a0362fa75886a21550dbec49aad54481463a6" - integrity sha512-SjlHp0G4qhuhkQQc+YXdGkI8EerCqwxvgytMgBpzMUQTafrkNant3e7pgilBGgjy/iM40ICvWBLgASTPMrQU7w== +"@solana/web3.js@^1.50.1": + version "1.95.3" + resolved "https://registry.yarnpkg.com/@solana/web3.js/-/web3.js-1.95.3.tgz#70b5f4d76823f56b5af6403da51125fffeb65ff3" + integrity sha512-O6rPUN0w2fkNqx/Z3QJMB9L225Ex10PRDH8bTaIUPZXMPV0QP8ZpPvjQnXK+upUczlRgzHzd6SjKIha1p+I6og== dependencies: - "@babel/runtime" "^7.24.8" + "@babel/runtime" "^7.25.0" "@noble/curves" "^1.4.2" "@noble/hashes" "^1.4.0" "@solana/buffer-layout" "^4.0.1" @@ -214,10 +233,31 @@ rpc-websockets "^8.0.1" superstruct "^1.0.4" +"@solana/web3.js@^1.68.0": + version "1.95.2" + resolved "https://registry.yarnpkg.com/@solana/web3.js/-/web3.js-1.95.2.tgz#6f8a0362fa75886a21550dbec49aad54481463a6" + integrity sha512-SjlHp0G4qhuhkQQc+YXdGkI8EerCqwxvgytMgBpzMUQTafrkNant3e7pgilBGgjy/iM40ICvWBLgASTPMrQU7w== + dependencies: + "@babel/runtime" "^7.24.8" + "@noble/curves" "^1.4.2" + "@noble/hashes" "^1.4.0" + "@solana/buffer-layout" "^4.0.1" + agentkeepalive "^4.5.0" + bigint-buffer "^1.1.5" + bn.js "^5.2.1" + borsh "^0.7.0" + bs58 "^4.0.1" + buffer "6.0.3" + fast-stable-stringify "^1.0.0" + jayson "^4.1.1" + node-fetch "^2.7.0" + rpc-websockets "^9.0.2" + superstruct "^2.0.2" + "@swc/helpers@^0.5.11": - version "0.5.12" - resolved "https://registry.yarnpkg.com/@swc/helpers/-/helpers-0.5.12.tgz#37aaca95284019eb5d2207101249435659709f4b" - integrity sha512-KMZNXiGibsW9kvZAO1Pam2JPTDBm+KSHMMHWdsyI/1DbIZjT2A6Gy3hblVXUMEDvUAKq+e0vL0X0o54owWji7g== + version "0.5.13" + resolved "https://registry.yarnpkg.com/@swc/helpers/-/helpers-0.5.13.tgz#33e63ff3cd0cade557672bd7888a39ce7d115a8c" + integrity sha512-UoKGxQ3r5kYI9dALKJapMmuK+1zWM/H17Z1+iwnNmzcJRnfFuevZs375TA5rW31pu4BS4NoSy1fRsexDXfWn5w== dependencies: tslib "^2.4.0" @@ -244,9 +284,9 @@ integrity sha512-Z61JK7DKDtdKTWwLeElSEBcWGRLY8g95ic5FoQqI9CMx0ns/Ghep3B4DfcEimiKMvtamNVULVNKEsiwV3aQmXw== "@types/node@*": - version "22.4.0" - resolved "https://registry.yarnpkg.com/@types/node/-/node-22.4.0.tgz#c295fe1d6f5f58916cc61dbef8cf65b5b9b71de9" - integrity sha512-49AbMDwYUz7EXxKU/r7mXOsxwFr4BYbvB7tWYxVuLdb2ibd30ijjXINSMAHiEEZk5PCRBmW1gUeisn2VMKt3cQ== + version "22.6.0" + resolved "https://registry.yarnpkg.com/@types/node/-/node-22.6.0.tgz#b604c9a628760221905c1b272fd6aee661f45042" + integrity sha512-QyR8d5bmq+eR72TwQDfujwShHMcIrWIYsaQFtXRE58MHPTEKUNxjxvl0yS0qPMds5xbSDWtp7ZpvGFtd7dfMdQ== dependencies: undici-types "~6.19.2" @@ -933,7 +973,7 @@ isomorphic-ws@^4.0.1: resolved "https://registry.yarnpkg.com/isomorphic-ws/-/isomorphic-ws-4.0.1.tgz#55fd4cd6c5e6491e76dc125938dd863f5cd4f2dc" integrity sha512-BhBvN2MBpWTaSHdWRb/bwdZJ1WaehQ2L1KngkCkfLUGF0mAWAT1sQUQacEmQ0jXkFw/czDXPNQSL5u2/Krsz1w== -jayson@^4.1.0, jayson@^4.1.1: +jayson@^4.1.0: version "4.1.1" resolved "https://registry.yarnpkg.com/jayson/-/jayson-4.1.1.tgz#282ff13d3cea09776db684b7eeca98c47b2fa99a" integrity sha512-5ZWm4Q/0DHPyeMfAsrwViwUS2DMVsQgWh8bEEIVTkfb3DzHZ2L3G5WUnF+AKmGjjM9r1uAv73SaqC1/U4RL45w== @@ -951,6 +991,24 @@ jayson@^4.1.0, jayson@^4.1.1: uuid "^8.3.2" ws "^7.5.10" +jayson@^4.1.1: + version "4.1.2" + resolved "https://registry.yarnpkg.com/jayson/-/jayson-4.1.2.tgz#443c26a8658703e0b2e881117b09395d88b6982e" + integrity sha512-5nzMWDHy6f+koZOuYsArh2AXs73NfWYVlFyJJuCedr93GpY+Ku8qq10ropSXVfHK+H0T6paA88ww+/dV+1fBNA== + dependencies: + "@types/connect" "^3.4.33" + "@types/node" "^12.12.54" + "@types/ws" "^7.4.4" + JSONStream "^1.3.5" + commander "^2.20.3" + delay "^5.0.0" + es6-promisify "^5.0.0" + eyes "^0.1.8" + isomorphic-ws "^4.0.1" + json-stringify-safe "^5.0.1" + uuid "^8.3.2" + ws "^7.5.10" + js-yaml@4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602" @@ -1134,9 +1192,9 @@ node-gyp-build@^4.2.0: integrity sha512-iWjXZvmboq0ja1pUGULQBexmxq8CV4xBhX7VDOTbL7ZR4FOowwY/VOtRxBN/yKxmdGoIp4j5ysNT4u3S2pDQ3Q== node-gyp-build@^4.3.0: - version "4.8.1" - resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.8.1.tgz#976d3ad905e71b76086f4f0b0d3637fe79b6cda5" - integrity sha512-OSs33Z9yWr148JZcbZd5WiAXhh/n9z8TxQcdMhIOlpN9AhWpLfvVFO73+m77bBABQMaY9XSvIa+qk0jlI7Gcaw== + version "4.8.2" + resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.8.2.tgz#4f802b71c1ab2ca16af830e6c1ea7dd1ad9496fa" + integrity sha512-IRUxE4BVsHWXkV/SFOut4qTlagw2aM8T5/vnTsmrHJvVoKueJHRc/JaFND7QDDc61kLYUJ6qlZM3sqTSyx2dTw== normalize-path@^3.0.0, normalize-path@~3.0.0: version "3.0.0" @@ -1486,11 +1544,16 @@ tsconfig-paths@^3.5.0: minimist "^1.2.6" strip-bom "^3.0.0" -tslib@^2.0.3, tslib@^2.4.0: +tslib@^2.0.3: version "2.6.3" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.3.tgz#0438f810ad7a9edcde7a241c3d80db693c8cbfe0" integrity sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ== +tslib@^2.4.0: + version "2.7.0" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.7.0.tgz#d9b40c5c40ab59e8738f297df3087bf1a2690c01" + integrity sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA== + type-detect@^4.0.0, type-detect@^4.0.8: version "4.0.8" resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.8.tgz#7646fb5f18871cfbb7749e69bd39a6388eb7450c" @@ -1502,9 +1565,9 @@ typescript@^4.5.4: integrity sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g== undici-types@~6.19.2: - version "6.19.6" - resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.19.6.tgz#e218c3df0987f4c0e0008ca00d6b6472d9b89b36" - integrity sha512-e/vggGopEfTKSvj4ihnOLTsqhrKRN3LeO6qSN/GxohhuRv8qH9bNQ4B8W7e/vFL+0XTnmHPB4/kegunZGA4Org== + version "6.19.8" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.19.8.tgz#35111c9d1437ab83a7cdc0abae2f26d88eda0a02" + integrity sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw== utf-8-validate@^5.0.2: version "5.0.10" diff --git a/pkg/monitoring/types/txdetails.go b/pkg/monitoring/types/txdetails.go index f5149677a..73c5cabd6 100644 --- a/pkg/monitoring/types/txdetails.go +++ b/pkg/monitoring/types/txdetails.go @@ -144,7 +144,7 @@ func ParseTx(tx *solanaGo.Transaction, programAddr solanaGo.PublicKey) (TxDetail } // find compute budget program instruction - if tx.Message.AccountKeys[instruction.ProgramIDIndex] == solanaGo.MustPublicKeyFromBase58(fees.ComputeBudgetProgram) { + if tx.Message.AccountKeys[instruction.ProgramIDIndex] == fees.ComputeBudgetProgram { // parsing compute unit price var err error txDetails.ComputeUnitPrice, err = fees.ParseComputeUnitPrice(instruction.Data) diff --git a/pkg/solana/chain.go b/pkg/solana/chain.go index 5749e3f34..85f71ee92 100644 --- a/pkg/solana/chain.go +++ b/pkg/solana/chain.go @@ -561,8 +561,15 @@ func (c *chain) sendTx(ctx context.Context, from, to string, amount *big.Int, ba } } - txm := c.TxManager() - err = txm.Enqueue("", tx) + chainTxm := c.TxManager() + err = chainTxm.Enqueue("", tx, + txm.SetComputeUnitLimit(500), // reduce from default 200K limit - should only take 450 compute units + // no fee bumping and no additional fee - makes validating balance accurate + txm.SetComputeUnitPriceMax(0), + txm.SetComputeUnitPriceMin(0), + txm.SetBaseComputeUnitPrice(0), + txm.SetFeeBumpPeriod(0), + ) if err != nil { return fmt.Errorf("transaction failed: %w", err) } diff --git a/pkg/solana/chain_test.go b/pkg/solana/chain_test.go index 6fb966740..4097e38dd 100644 --- a/pkg/solana/chain_test.go +++ b/pkg/solana/chain_test.go @@ -1,18 +1,24 @@ package solana import ( + "context" "errors" "fmt" "io" + "math/big" "net/http" "net/http/httptest" "strings" "sync" "testing" + "github.com/gagliardetto/solana-go" + "github.com/gagliardetto/solana-go/rpc" "github.com/google/uuid" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" + "go.uber.org/zap/zapcore" "github.com/smartcontractkit/chainlink-common/pkg/config" "github.com/smartcontractkit/chainlink-common/pkg/logger" @@ -20,6 +26,8 @@ import ( "github.com/smartcontractkit/chainlink-solana/pkg/solana/client" solcfg "github.com/smartcontractkit/chainlink-solana/pkg/solana/config" + "github.com/smartcontractkit/chainlink-solana/pkg/solana/fees" + "github.com/smartcontractkit/chainlink-solana/pkg/solana/txm/mocks" ) const TestSolanaGenesisHashTemplate = `{"jsonrpc":"2.0","result":"%s","id":1}` @@ -238,3 +246,68 @@ func TestSolanaChain_VerifiedClient_ParallelClients(t *testing.T) { func ptr[T any](t T) *T { return &t } + +func TestChain_Transact(t *testing.T) { + ctx := tests.Context(t) + url := client.SetupLocalSolNode(t) + lgr, logs := logger.TestObserved(t, zapcore.DebugLevel) + + // transaction parameters + sender, err := solana.NewRandomPrivateKey() + require.NoError(t, err) + receiver, err := solana.NewRandomPrivateKey() + require.NoError(t, err) + amount := big.NewInt(100_000_000_000 - 5_000) // total balance - tx fee + client.FundTestAccounts(t, solana.PublicKeySlice{sender.PublicKey()}, url) + + // configuration + cfg := solcfg.NewDefault() + cfg.Nodes = append(cfg.Nodes, &solcfg.Node{ + Name: ptr("localnet-" + t.Name()), + URL: config.MustParseURL(url), + SendOnly: false, + }) + + // mocked keystore + mkey := mocks.NewSimpleKeystore(t) + mkey.On("Sign", mock.Anything, sender.PublicKey().String(), mock.Anything).Return(func(_ context.Context, _ string, data []byte) []byte { + sig, _ := sender.Sign(data) + return sig[:] + }, nil) + + c, err := newChain("localnet", cfg, mkey, lgr) + require.NoError(t, err) + require.NoError(t, c.txm.Start(ctx)) + + require.NoError(t, c.Transact(ctx, sender.PublicKey().String(), receiver.PublicKey().String(), amount, true)) + tests.AssertLogEventually(t, logs, "tx state: confirmed") + tests.AssertLogEventually(t, logs, "stopped tx retry") + require.NoError(t, c.txm.Close()) + + filteredLogs := logs.FilterMessage("tx state: confirmed").All() + require.Len(t, filteredLogs, 1) + sig, ok := filteredLogs[0].ContextMap()["signature"] + require.True(t, ok) + + // inspect transaction + solClient := rpc.New(url) + res, err := solClient.GetTransaction(ctx, solana.MustSignatureFromBase58(sig.(string)), &rpc.GetTransactionOpts{Commitment: "confirmed"}) + require.NoError(t, err) + require.Nil(t, res.Meta.Err) // no error + + // validate balances change as expected + require.Equal(t, amount.Uint64()+5_000, res.Meta.PreBalances[0]) + require.Zero(t, res.Meta.PostBalances[0]) + require.Zero(t, res.Meta.PreBalances[1]) + require.Equal(t, amount.Uint64(), res.Meta.PostBalances[1]) + + tx, err := res.Transaction.GetTransaction() + require.NoError(t, err) + require.Len(t, tx.Message.Instructions, 3) + price, err := fees.ParseComputeUnitPrice(tx.Message.Instructions[0].Data) + require.NoError(t, err) + assert.Equal(t, fees.ComputeUnitPrice(0), price) + limit, err := fees.ParseComputeUnitLimit(tx.Message.Instructions[2].Data) + require.NoError(t, err) + assert.Equal(t, fees.ComputeUnitLimit(500), limit) +} diff --git a/pkg/solana/config/config.go b/pkg/solana/config/config.go index cabfe3365..ef4a1cda3 100644 --- a/pkg/solana/config/config.go +++ b/pkg/solana/config/config.go @@ -10,25 +10,26 @@ import ( ) // Global solana defaults. -var defaultConfigSet = configSet{ - BalancePollPeriod: 5 * time.Second, // poll period for balance monitoring - ConfirmPollPeriod: 500 * time.Millisecond, // polling for tx confirmation - OCR2CachePollPeriod: time.Second, // cache polling rate - OCR2CacheTTL: time.Minute, // stale cache deadline - TxTimeout: time.Minute, // timeout for send tx method in client - TxRetryTimeout: 10 * time.Second, // duration for tx rebroadcasting to RPC node - TxConfirmTimeout: 30 * time.Second, // duration before discarding tx as unconfirmed - SkipPreflight: true, // to enable or disable preflight checks - Commitment: rpc.CommitmentConfirmed, - MaxRetries: new(uint), // max number of retries (default = *new(uint) = 0). when config.MaxRetries < 0, interpreted as MaxRetries = nil and rpc node will do a reasonable number of retries +var defaultConfigSet = Chain{ + BalancePollPeriod: config.MustNewDuration(5 * time.Second), // poll period for balance monitoring + ConfirmPollPeriod: config.MustNewDuration(500 * time.Millisecond), // polling for tx confirmation + OCR2CachePollPeriod: config.MustNewDuration(time.Second), // cache polling rate + OCR2CacheTTL: config.MustNewDuration(time.Minute), // stale cache deadline + TxTimeout: config.MustNewDuration(time.Minute), // timeout for send tx method in client + TxRetryTimeout: config.MustNewDuration(10 * time.Second), // duration for tx rebroadcasting to RPC node + TxConfirmTimeout: config.MustNewDuration(30 * time.Second), // duration before discarding tx as unconfirmed + SkipPreflight: ptr(true), // to enable or disable preflight checks + Commitment: ptr(string(rpc.CommitmentConfirmed)), + MaxRetries: ptr(int64(0)), // max number of retries (default = 0). when config.MaxRetries < 0), interpreted as MaxRetries = nil and rpc node will do a reasonable number of retries // fee estimator - FeeEstimatorMode: "fixed", - ComputeUnitPriceMax: 1_000, - ComputeUnitPriceMin: 0, - ComputeUnitPriceDefault: 0, - FeeBumpPeriod: 3 * time.Second, // set to 0 to disable fee bumping - BlockHistoryPollPeriod: 5 * time.Second, + FeeEstimatorMode: ptr("fixed"), + ComputeUnitPriceMax: ptr(uint64(1_000)), + ComputeUnitPriceMin: ptr(uint64(0)), + ComputeUnitPriceDefault: ptr(uint64(0)), + FeeBumpPeriod: config.MustNewDuration(3 * time.Second), // set to 0 to disable fee bumping + BlockHistoryPollPeriod: config.MustNewDuration(5 * time.Second), + ComputeUnitLimitDefault: ptr(uint32(200_000)), // set to 0 to disable adding compute unit limit } //go:generate mockery --name Config --output ./mocks/ --case=underscore --filename config.go @@ -51,27 +52,7 @@ type Config interface { ComputeUnitPriceDefault() uint64 FeeBumpPeriod() time.Duration BlockHistoryPollPeriod() time.Duration -} - -// opt: remove -type configSet struct { - BalancePollPeriod time.Duration - ConfirmPollPeriod time.Duration - OCR2CachePollPeriod time.Duration - OCR2CacheTTL time.Duration - TxTimeout time.Duration - TxRetryTimeout time.Duration - TxConfirmTimeout time.Duration - SkipPreflight bool - Commitment rpc.CommitmentType - MaxRetries *uint - - FeeEstimatorMode string - ComputeUnitPriceMax uint64 - ComputeUnitPriceMin uint64 - ComputeUnitPriceDefault uint64 - FeeBumpPeriod time.Duration - BlockHistoryPollPeriod time.Duration + ComputeUnitLimitDefault() uint32 } type Chain struct { @@ -91,57 +72,60 @@ type Chain struct { ComputeUnitPriceDefault *uint64 FeeBumpPeriod *config.Duration BlockHistoryPollPeriod *config.Duration + ComputeUnitLimitDefault *uint32 } func (c *Chain) SetDefaults() { if c.BalancePollPeriod == nil { - c.BalancePollPeriod = config.MustNewDuration(defaultConfigSet.BalancePollPeriod) + c.BalancePollPeriod = defaultConfigSet.BalancePollPeriod } if c.ConfirmPollPeriod == nil { - c.ConfirmPollPeriod = config.MustNewDuration(defaultConfigSet.ConfirmPollPeriod) + c.ConfirmPollPeriod = defaultConfigSet.ConfirmPollPeriod } if c.OCR2CachePollPeriod == nil { - c.OCR2CachePollPeriod = config.MustNewDuration(defaultConfigSet.OCR2CachePollPeriod) + c.OCR2CachePollPeriod = defaultConfigSet.OCR2CachePollPeriod } if c.OCR2CacheTTL == nil { - c.OCR2CacheTTL = config.MustNewDuration(defaultConfigSet.OCR2CacheTTL) + c.OCR2CacheTTL = defaultConfigSet.OCR2CacheTTL } if c.TxTimeout == nil { - c.TxTimeout = config.MustNewDuration(defaultConfigSet.TxTimeout) + c.TxTimeout = defaultConfigSet.TxTimeout } if c.TxRetryTimeout == nil { - c.TxRetryTimeout = config.MustNewDuration(defaultConfigSet.TxRetryTimeout) + c.TxRetryTimeout = defaultConfigSet.TxRetryTimeout } if c.TxConfirmTimeout == nil { - c.TxConfirmTimeout = config.MustNewDuration(defaultConfigSet.TxConfirmTimeout) + c.TxConfirmTimeout = defaultConfigSet.TxConfirmTimeout } if c.SkipPreflight == nil { - c.SkipPreflight = &defaultConfigSet.SkipPreflight + c.SkipPreflight = defaultConfigSet.SkipPreflight } if c.Commitment == nil { - c.Commitment = (*string)(&defaultConfigSet.Commitment) + c.Commitment = defaultConfigSet.Commitment } - if c.MaxRetries == nil && defaultConfigSet.MaxRetries != nil { - i := int64(*defaultConfigSet.MaxRetries) //nolint:gosec // reasonable default value does not cause overflow - c.MaxRetries = &i + if c.MaxRetries == nil { + c.MaxRetries = defaultConfigSet.MaxRetries } if c.FeeEstimatorMode == nil { - c.FeeEstimatorMode = &defaultConfigSet.FeeEstimatorMode + c.FeeEstimatorMode = defaultConfigSet.FeeEstimatorMode } if c.ComputeUnitPriceMax == nil { - c.ComputeUnitPriceMax = &defaultConfigSet.ComputeUnitPriceMax + c.ComputeUnitPriceMax = defaultConfigSet.ComputeUnitPriceMax } if c.ComputeUnitPriceMin == nil { - c.ComputeUnitPriceMin = &defaultConfigSet.ComputeUnitPriceMin + c.ComputeUnitPriceMin = defaultConfigSet.ComputeUnitPriceMin } if c.ComputeUnitPriceDefault == nil { - c.ComputeUnitPriceDefault = &defaultConfigSet.ComputeUnitPriceDefault + c.ComputeUnitPriceDefault = defaultConfigSet.ComputeUnitPriceDefault } if c.FeeBumpPeriod == nil { - c.FeeBumpPeriod = config.MustNewDuration(defaultConfigSet.FeeBumpPeriod) + c.FeeBumpPeriod = defaultConfigSet.FeeBumpPeriod } if c.BlockHistoryPollPeriod == nil { - c.BlockHistoryPollPeriod = config.MustNewDuration(defaultConfigSet.BlockHistoryPollPeriod) + c.BlockHistoryPollPeriod = defaultConfigSet.BlockHistoryPollPeriod + } + if c.ComputeUnitLimitDefault == nil { + c.ComputeUnitLimitDefault = defaultConfigSet.ComputeUnitLimitDefault } } @@ -162,3 +146,7 @@ func (n *Node) ValidateConfig() (err error) { } return } + +func ptr[T any](t T) *T { + return &t +} diff --git a/pkg/solana/config/mocks/config.go b/pkg/solana/config/mocks/config.go index 712850f70..56b1cca98 100644 --- a/pkg/solana/config/mocks/config.go +++ b/pkg/solana/config/mocks/config.go @@ -68,6 +68,24 @@ func (_m *Config) Commitment() rpc.CommitmentType { return r0 } +// ComputeUnitLimitDefault provides a mock function with given fields: +func (_m *Config) ComputeUnitLimitDefault() uint32 { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for ComputeUnitLimitDefault") + } + + var r0 uint32 + if rf, ok := ret.Get(0).(func() uint32); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(uint32) + } + + return r0 +} + // ComputeUnitPriceDefault provides a mock function with given fields: func (_m *Config) ComputeUnitPriceDefault() uint64 { ret := _m.Called() diff --git a/pkg/solana/config/toml.go b/pkg/solana/config/toml.go index fcceb0603..056fcc210 100644 --- a/pkg/solana/config/toml.go +++ b/pkg/solana/config/toml.go @@ -277,6 +277,10 @@ func (c *TOMLConfig) BlockHistoryPollPeriod() time.Duration { return c.Chain.BlockHistoryPollPeriod.Duration() } +func (c *TOMLConfig) ComputeUnitLimitDefault() uint32 { + return *c.Chain.ComputeUnitLimitDefault +} + func (c *TOMLConfig) ListNodes() Nodes { return c.Nodes } @@ -285,6 +289,11 @@ func (c *TOMLConfig) MultiNodeConfig() *MultiNode { return &c.MultiNode } +func (c *TOMLConfig) SetDefaults() { + c.Chain.SetDefaults() + c.MultiNode.SetDefaults() +} + func NewDefault() *TOMLConfig { cfg := &TOMLConfig{} cfg.Chain.SetDefaults() diff --git a/pkg/solana/fees/computebudget.go b/pkg/solana/fees/computebudget.go index 6738d4fb4..aa21c1105 100644 --- a/pkg/solana/fees/computebudget.go +++ b/pkg/solana/fees/computebudget.go @@ -6,12 +6,13 @@ import ( "fmt" "github.com/gagliardetto/solana-go" + "golang.org/x/exp/constraints" ) // https://github.com/solana-labs/solana/blob/60858d043ca612334de300805d93ea3014e8ab37/sdk/src/compute_budget.rs#L25 const ( // deprecated: will not support for building instruction - InstructionRequestUnitsDeprecated uint8 = iota + InstructionRequestUnitsDeprecated computeBudgetInstruction = 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 @@ -30,29 +31,61 @@ const ( InstructionSetComputeUnitPrice ) -const ( - ComputeBudgetProgram = "ComputeBudget111111111111111111111111111111" +var ( + ComputeBudgetProgram = solana.MustPublicKeyFromBase58("ComputeBudget111111111111111111111111111111") ) +type computeBudgetInstruction uint8 + +func (ins computeBudgetInstruction) String() (out string) { + out = "INVALID" + switch ins { + case InstructionRequestUnitsDeprecated: + out = "RequestUnitsDeprecated" + case InstructionRequestHeapFrame: + out = "RequestHeapFrame" + case InstructionSetComputeUnitLimit: + out = "SetComputeUnitLimit" + case InstructionSetComputeUnitPrice: + out = "SetComputeUnitPrice" + } + return out +} + +// instruction is an internal interface for encoding instruction data +type instruction interface { + Data() ([]byte, error) + Selector() computeBudgetInstruction +} + // 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(ComputeBudgetProgram) +// simple encoding into program expected format +func (val ComputeUnitPrice) Data() ([]byte, error) { + return encode(InstructionSetComputeUnitPrice, val) } -// No accounts needed -func (val ComputeUnitPrice) Accounts() (accounts []*solana.AccountMeta) { - return accounts +func (val ComputeUnitPrice) Selector() computeBudgetInstruction { + return InstructionSetComputeUnitPrice } -// simple encoding into program expected format -func (val ComputeUnitPrice) Data() ([]byte, error) { +type ComputeUnitLimit uint32 + +func (val ComputeUnitLimit) Data() ([]byte, error) { + return encode(InstructionSetComputeUnitLimit, val) +} + +func (val ComputeUnitLimit) Selector() computeBudgetInstruction { + return InstructionSetComputeUnitLimit +} + +// encode combines the identifier and little encoded value into a byte array +func encode[V constraints.Unsigned](identifier computeBudgetInstruction, val V) ([]byte, error) { buf := new(bytes.Buffer) // encode method identifier - if err := buf.WriteByte(InstructionSetComputeUnitPrice); err != nil { + if err := buf.WriteByte(uint8(identifier)); err != nil { return []byte{}, err } @@ -65,26 +98,47 @@ func (val ComputeUnitPrice) Data() ([]byte, error) { } func ParseComputeUnitPrice(data []byte) (ComputeUnitPrice, error) { - if len(data) != (1 + 8) { // instruction byte + uint64 + v, err := parse(InstructionSetComputeUnitPrice, data, binary.LittleEndian.Uint64) + return ComputeUnitPrice(v), err +} + +func ParseComputeUnitLimit(data []byte) (ComputeUnitLimit, error) { + v, err := parse(InstructionSetComputeUnitLimit, data, binary.LittleEndian.Uint32) + return ComputeUnitLimit(v), err +} + +// parse implements tx data parsing for the provided instruction type and specified decoder +func parse[V constraints.Unsigned](ins computeBudgetInstruction, data []byte, decoder func([]byte) V) (V, error) { + if len(data) != (1 + binary.Size(V(0))) { // instruction byte + uintXXX length return 0, fmt.Errorf("invalid length: %d", len(data)) } - if data[0] != InstructionSetComputeUnitPrice { - return 0, fmt.Errorf("not SetComputeUnitPrice identifier: %d", data[0]) + // validate instruction identifier + if data[0] != uint8(ins) { + return 0, fmt.Errorf("not %s identifier: %d", ins, data[0]) } - // guarantees length 8 - return ComputeUnitPrice(binary.LittleEndian.Uint64(data[1:])), nil + // guarantees length to fit the binary decoder + return decoder(data[1:]), nil } // modifies passed in tx to set compute unit price -func SetComputeUnitPrice(tx *solana.Transaction, price ComputeUnitPrice) error { +func SetComputeUnitPrice(tx *solana.Transaction, value ComputeUnitPrice) error { + return set(tx, value, true) // data feeds expects SetComputeUnitPrice instruction to be right before report instruction +} + +func SetComputeUnitLimit(tx *solana.Transaction, value ComputeUnitLimit) error { + return set(tx, value, false) // appends instruction to the end +} + +// set adds or modifies instructions for the compute budget program +func set(tx *solana.Transaction, baseData instruction, appendToFront bool) 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 int for i, a := range tx.Message.AccountKeys { - if a.Equals(price.ProgramID()) { + if a.Equals(ComputeBudgetProgram) { exists = true programIdx = i break @@ -92,7 +146,7 @@ func SetComputeUnitPrice(tx *solana.Transaction, price ComputeUnitPrice) error { } // if it doesn't exist, add to account keys if !exists { - tx.Message.AccountKeys = append(tx.Message.AccountKeys, price.ProgramID()) + tx.Message.AccountKeys = append(tx.Message.AccountKeys, ComputeBudgetProgram) programIdx = len(tx.Message.AccountKeys) - 1 // last index of account keys // https://github.com/gagliardetto/solana-go/blob/618f56666078f8131a384ab27afd918d248c08b7/transaction.go#L293 @@ -100,7 +154,7 @@ func SetComputeUnitPrice(tx *solana.Transaction, price ComputeUnitPrice) error { } // get instruction data - data, err := price.Data() + data, err := baseData.Data() if err != nil { return err } @@ -117,7 +171,7 @@ func SetComputeUnitPrice(tx *solana.Transaction, price ComputeUnitPrice) error { for i := range tx.Message.Instructions { if int(tx.Message.Instructions[i].ProgramIDIndex) == programIdx && len(tx.Message.Instructions[i].Data) > 0 && - tx.Message.Instructions[i].Data[0] == InstructionSetComputeUnitPrice { + tx.Message.Instructions[i].Data[0] == uint8(baseData.Selector()) { found = true instructionIdx = i break @@ -127,8 +181,11 @@ func SetComputeUnitPrice(tx *solana.Transaction, price ComputeUnitPrice) error { 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...) + if appendToFront { + tx.Message.Instructions = append([]solana.CompiledInstruction{instruction}, tx.Message.Instructions...) + } else { + tx.Message.Instructions = append(tx.Message.Instructions, instruction) + } } return nil diff --git a/pkg/solana/fees/computebudget_test.go b/pkg/solana/fees/computebudget_test.go index c80550297..c5cacabd4 100644 --- a/pkg/solana/fees/computebudget_test.go +++ b/pkg/solana/fees/computebudget_test.go @@ -1,6 +1,7 @@ package fees import ( + "fmt" "testing" "github.com/gagliardetto/solana-go" @@ -9,11 +10,35 @@ import ( "github.com/stretchr/testify/require" ) -func TestSetComputeUnitPrice(t *testing.T) { +func TestSet(t *testing.T) { + t.Run("ComputeUnitPrice", func(t *testing.T) { + t.Parallel() + testSet(t, func(v uint) ComputeUnitPrice { + return ComputeUnitPrice(v) + }, SetComputeUnitPrice, true) + }) + t.Run("ComputeUnitLimit", func(t *testing.T) { + t.Parallel() + testSet(t, func(v uint) ComputeUnitLimit { + return ComputeUnitLimit(v) + }, SetComputeUnitLimit, false) + }) +} + +func testSet[V instruction](t *testing.T, builder func(uint) V, setter func(*solana.Transaction, V) error, expectFirstInstruction bool) { key, err := solana.NewRandomPrivateKey() require.NoError(t, err) + getIndex := func(count int) int { + index := count - 1 + if expectFirstInstruction { + index = 0 + } + return index + } + t.Run("noAccount_nofee", func(t *testing.T) { + t.Parallel() // build base tx (no fee) tx, err := solana.NewTransaction([]solana.Instruction{ system.NewTransferInstruction( @@ -26,19 +51,21 @@ func TestSetComputeUnitPrice(t *testing.T) { instructionCount := len(tx.Message.Instructions) // add fee - require.NoError(t, SetComputeUnitPrice(tx, 1)) + require.NoError(t, setter(tx, builder(1))) // evaluate currentCount := len(tx.Message.Instructions) assert.Greater(t, currentCount, instructionCount) assert.Equal(t, 2, currentCount) - assert.Equal(t, ComputeBudgetProgram, tx.Message.AccountKeys[tx.Message.Instructions[0].ProgramIDIndex].String()) - data, err := ComputeUnitPrice(1).Data() + i := getIndex(currentCount) + assert.Equal(t, ComputeBudgetProgram, tx.Message.AccountKeys[tx.Message.Instructions[i].ProgramIDIndex]) + data, err := builder(1).Data() assert.NoError(t, err) - assert.Equal(t, data, []byte(tx.Message.Instructions[0].Data)) + assert.Equal(t, data, []byte(tx.Message.Instructions[i].Data)) }) t.Run("accountExists_noFee", func(t *testing.T) { + t.Parallel() // build base tx (no fee) tx, err := solana.NewTransaction([]solana.Instruction{ system.NewTransferInstruction( @@ -49,25 +76,27 @@ func TestSetComputeUnitPrice(t *testing.T) { }, solana.Hash{}) require.NoError(t, err) accountCount := len(tx.Message.AccountKeys) - tx.Message.AccountKeys = append(tx.Message.AccountKeys, ComputeUnitPrice(0).ProgramID()) + tx.Message.AccountKeys = append(tx.Message.AccountKeys, ComputeBudgetProgram) accountCount++ // add fee - require.NoError(t, SetComputeUnitPrice(tx, 1)) + require.NoError(t, setter(tx, builder(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, ComputeBudgetProgram, tx.Message.AccountKeys[tx.Message.Instructions[0].ProgramIDIndex].String()) - data, err := ComputeUnitPrice(1).Data() + i := getIndex(len(tx.Message.Instructions)) + assert.Equal(t, ComputeBudgetProgram, tx.Message.AccountKeys[tx.Message.Instructions[i].ProgramIDIndex]) + data, err := builder(1).Data() assert.NoError(t, err) - assert.Equal(t, data, []byte(tx.Message.Instructions[0].Data)) + assert.Equal(t, data, []byte(tx.Message.Instructions[i].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) { + t.Run("exists_unknownOrder", func(t *testing.T) { + t.Parallel() // build base tx (no fee) tx, err := solana.NewTransaction([]solana.Instruction{ system.NewTransferInstruction( @@ -80,42 +109,66 @@ func TestSetComputeUnitPrice(t *testing.T) { transferInstruction := tx.Message.Instructions[0] // add fee - require.NoError(t, SetComputeUnitPrice(tx, 0)) + require.NoError(t, setter(tx, builder(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] + + // after swap + computeIndex := 0 + transferIndex := 1 + if expectFirstInstruction { + computeIndex = 1 + transferIndex = 0 + } + + require.Equal(t, transferInstruction, tx.Message.Instructions[transferIndex]) + oldComputeInstruction := tx.Message.Instructions[computeIndex] 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]) + require.NoError(t, setter(tx, builder(100))) + require.Equal(t, transferInstruction, tx.Message.Instructions[transferIndex]) // transfer should not have been touched + assert.NotEqual(t, oldComputeInstruction, tx.Message.Instructions[computeIndex]) 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() + data, err := builder(100).Data() assert.NoError(t, err) - assert.Equal(t, data, []byte(tx.Message.Instructions[1].Data)) + assert.Equal(t, data, []byte(tx.Message.Instructions[computeIndex].Data)) + }) +} + +func TestParse(t *testing.T) { + t.Run("ComputeUnitPrice", func(t *testing.T) { + t.Parallel() + testParse(t, func(v uint) ComputeUnitPrice { + return ComputeUnitPrice(v) + }, ParseComputeUnitPrice) + }) + t.Run("ComputeUnitLimit", func(t *testing.T) { + t.Parallel() + testParse(t, func(v uint) ComputeUnitLimit { + return ComputeUnitLimit(v) + }, ParseComputeUnitLimit) }) } -func TestParseComputeUnitPrice(t *testing.T) { - data, err := ComputeUnitPrice(100).Data() +func testParse[V instruction](t *testing.T, builder func(uint) V, parser func([]byte) (V, error)) { + data, err := builder(100).Data() assert.NoError(t, err) - v, err := ParseComputeUnitPrice(data) + v, err := parser(data) assert.NoError(t, err) - assert.Equal(t, ComputeUnitPrice(100), v) + assert.Equal(t, builder(100), v) - _, err = ParseComputeUnitPrice([]byte{}) + _, err = parser([]byte{}) assert.ErrorContains(t, err, "invalid length") tooLong := [10]byte{} - _, err = ParseComputeUnitPrice(tooLong[:]) + _, err = parser(tooLong[:]) assert.ErrorContains(t, err, "invalid length") invalidData := data - invalidData[0] = InstructionRequestHeapFrame - _, err = ParseComputeUnitPrice(invalidData) - assert.ErrorContains(t, err, "not SetComputeUnitPrice identifier") + invalidData[0] = uint8(InstructionRequestHeapFrame) + _, err = parser(invalidData) + assert.ErrorContains(t, err, fmt.Sprintf("not %s identifier", builder(0).Selector())) } diff --git a/pkg/solana/fees/utils.go b/pkg/solana/fees/utils.go index 6b99829d8..652fc5039 100644 --- a/pkg/solana/fees/utils.go +++ b/pkg/solana/fees/utils.go @@ -71,7 +71,7 @@ func ParseBlock(res *rpc.GetBlockResult) (out BlockData, err error) { var price ComputeUnitPrice // default 0 for _, instruction := range baseTx.Message.Instructions { // find instructions for compute budget program - if baseTx.Message.AccountKeys[instruction.ProgramIDIndex] == solana.MustPublicKeyFromBase58(ComputeBudgetProgram) { + if baseTx.Message.AccountKeys[instruction.ProgramIDIndex] == ComputeBudgetProgram { parsed, parseErr := ParseComputeUnitPrice(instruction.Data) // if compute unit price found, break instruction loop // only one compute unit price tx is allowed 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/mocks/simple_keystore.go b/pkg/solana/txm/mocks/simple_keystore.go index 86c9d3f17..1c0bd6562 100644 --- a/pkg/solana/txm/mocks/simple_keystore.go +++ b/pkg/solana/txm/mocks/simple_keystore.go @@ -13,6 +13,36 @@ type SimpleKeystore struct { mock.Mock } +// Accounts provides a mock function with given fields: ctx +func (_m *SimpleKeystore) Accounts(ctx context.Context) ([]string, error) { + ret := _m.Called(ctx) + + if len(ret) == 0 { + panic("no return value specified for Accounts") + } + + var r0 []string + var r1 error + if rf, ok := ret.Get(0).(func(context.Context) ([]string, error)); ok { + return rf(ctx) + } + if rf, ok := ret.Get(0).(func(context.Context) []string); ok { + r0 = rf(ctx) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]string) + } + } + + if rf, ok := ret.Get(1).(func(context.Context) error); ok { + r1 = rf(ctx) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // Sign provides a mock function with given fields: ctx, account, data func (_m *SimpleKeystore) Sign(ctx context.Context, account string, data []byte) ([]byte, error) { ret := _m.Called(ctx, account, data) diff --git a/pkg/solana/txm/txm.go b/pkg/solana/txm/txm.go index dd4a31c9b..87861fd83 100644 --- a/pkg/solana/txm/txm.go +++ b/pkg/solana/txm/txm.go @@ -13,6 +13,7 @@ import ( "github.com/google/uuid" "github.com/smartcontractkit/chainlink-common/pkg/logger" + "github.com/smartcontractkit/chainlink-common/pkg/loop" "github.com/smartcontractkit/chainlink-common/pkg/services" "github.com/smartcontractkit/chainlink-common/pkg/utils" @@ -32,8 +33,11 @@ var _ services.Service = (*Txm)(nil) //go:generate mockery --name SimpleKeystore --output ./mocks/ --case=underscore --filename simple_keystore.go type SimpleKeystore interface { Sign(ctx context.Context, account string, data []byte) (signature []byte, err error) + Accounts(ctx context.Context) (accounts []string, err error) } +var _ loop.Keystore = (SimpleKeystore)(nil) + // Txm manages transactions for the solana blockchain. // simple implementation with no persistently stored txs type Txm struct { @@ -50,9 +54,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 +128,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 +152,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 +164,26 @@ 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 int) fees.ComputeUnitPrice { fee := fees.CalculateFee( - basePrice, - txm.cfg.ComputeUnitPriceMax(), - txm.cfg.ComputeUnitPriceMin(), + txcfg.BaseComputeUnitPrice, + txcfg.ComputeUnitPriceMax, + txcfg.ComputeUnitPriceMin, uint(count), //nolint:gosec // reasonable number of bumps should never cause overflow ) return fees.ComputeUnitPrice(fee) } + // add compute unit limit instruction - static for the transaction + // skip if compute unit limit = 0 (otherwise would always fail) + if txcfg.ComputeUnitLimit != 0 { + 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 int) (solanaGo.Transaction, error) { newTx := base // make copy @@ -192,7 +215,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 +261,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 +526,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 +548,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 +589,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..adfa273f4 100644 --- a/pkg/solana/txm/txm_internal_test.go +++ b/pkg/solana/txm/txm_internal_test.go @@ -50,7 +50,7 @@ func (p soltxmProm) getInflight() float64 { } // create placeholder transaction and returns func for signed tx with fee -func getTx(t *testing.T, val uint64, keystore SimpleKeystore, price fees.ComputeUnitPrice) (*solana.Transaction, func(fees.ComputeUnitPrice) *solana.Transaction) { +func getTx(t *testing.T, val uint64, keystore SimpleKeystore, price fees.ComputeUnitPrice) (*solana.Transaction, func(fees.ComputeUnitPrice, bool) *solana.Transaction) { pubkey := solana.PublicKey{} // create transfer tx @@ -69,10 +69,13 @@ func getTx(t *testing.T, val uint64, keystore SimpleKeystore, price fees.Compute base := *tx // tx to send to txm, txm will add fee & sign - return &base, func(price fees.ComputeUnitPrice) *solana.Transaction { + return &base, func(price fees.ComputeUnitPrice, addLimit bool) *solana.Transaction { tx := base - // add fee + // add fee parameters require.NoError(t, fees.SetComputeUnitPrice(&tx, price)) + if addLimit { + require.NoError(t, fees.SetComputeUnitLimit(&tx, 200_000)) // default + } // sign tx txMsg, err := tx.Message.MarshalBinary() @@ -166,12 +169,12 @@ func TestTxm(t *testing.T) { sendCount := 0 var countRW sync.RWMutex - mc.On("SendTx", mock.Anything, signed(0)).Run(func(mock.Arguments) { + mc.On("SendTx", mock.Anything, signed(0, true)).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() + mc.On("SimulateTx", mock.Anything, signed(0, true), mock.Anything).Return(&rpc.SimulateTransactionResult{}, nil).Once() // handle signature status calls count := 0 @@ -219,7 +222,7 @@ func TestTxm(t *testing.T) { 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) { + mc.On("SendTx", mock.Anything, signed(0, true)).Run(func(mock.Arguments) { wg.Done() }).Return(solana.Signature{}, errors.New("FAIL")).Once() @@ -243,8 +246,8 @@ func TestTxm(t *testing.T) { 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) { + mc.On("SendTx", mock.Anything, signed(0, true)).Return(sig, nil) + mc.On("SimulateTx", mock.Anything, signed(0, true), mock.Anything).Run(func(mock.Arguments) { wg.Done() }).Return(&rpc.SimulateTransactionResult{ Err: "FAIL", @@ -273,12 +276,12 @@ func TestTxm(t *testing.T) { 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) { + mc.On("SendTx", mock.Anything, signed(0, true)).Return(sig, nil) + mc.On("SendTx", mock.Anything, signed(1, true)).Return(retry0, nil) + mc.On("SendTx", mock.Anything, signed(2, true)).Return(retry1, nil) + mc.On("SendTx", mock.Anything, signed(3, true)).Return(retry2, nil).Maybe() + mc.On("SendTx", mock.Anything, signed(4, true)).Return(retry3, nil).Maybe() + mc.On("SimulateTx", mock.Anything, signed(0, true), mock.Anything).Run(func(mock.Arguments) { wg.Done() }).Return(&rpc.SimulateTransactionResult{}, errors.New("FAIL")).Once() // all signature statuses are nil, handled automatically @@ -311,8 +314,8 @@ func TestTxm(t *testing.T) { 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) { + mc.On("SendTx", mock.Anything, signed(0, true)).Return(sig, nil) + mc.On("SimulateTx", mock.Anything, signed(0, true), mock.Anything).Run(func(mock.Arguments) { wg.Done() }).Return(&rpc.SimulateTransactionResult{ Err: tempErr, @@ -341,8 +344,8 @@ func TestTxm(t *testing.T) { 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) { + mc.On("SendTx", mock.Anything, signed(0, true)).Return(sig, nil) + mc.On("SimulateTx", mock.Anything, signed(0, true), mock.Anything).Run(func(mock.Arguments) { wg.Done() }).Return(&rpc.SimulateTransactionResult{ Err: "BlockhashNotFound", @@ -383,8 +386,8 @@ func TestTxm(t *testing.T) { 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) { + mc.On("SendTx", mock.Anything, signed(0, true)).Return(sig, nil) + mc.On("SimulateTx", mock.Anything, signed(0, true), mock.Anything).Run(func(mock.Arguments) { wg.Done() }).Return(&rpc.SimulateTransactionResult{ Err: "AlreadyProcessed", @@ -424,12 +427,12 @@ func TestTxm(t *testing.T) { 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) { + mc.On("SendTx", mock.Anything, signed(0, true)).Return(sig, nil) + mc.On("SendTx", mock.Anything, signed(1, true)).Return(retry0, nil) + mc.On("SendTx", mock.Anything, signed(2, true)).Return(retry1, nil) + mc.On("SendTx", mock.Anything, signed(3, true)).Return(retry2, nil).Maybe() + mc.On("SendTx", mock.Anything, signed(4, true)).Return(retry3, nil).Maybe() + mc.On("SimulateTx", mock.Anything, signed(0, true), mock.Anything).Run(func(mock.Arguments) { wg.Done() }).Return(&rpc.SimulateTransactionResult{}, nil).Once() @@ -465,12 +468,12 @@ func TestTxm(t *testing.T) { 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) { + mc.On("SendTx", mock.Anything, signed(0, true)).Return(sig, nil) + mc.On("SendTx", mock.Anything, signed(1, true)).Return(retry0, nil) + mc.On("SendTx", mock.Anything, signed(2, true)).Return(retry1, nil) + mc.On("SendTx", mock.Anything, signed(3, true)).Return(retry2, nil).Maybe() + mc.On("SendTx", mock.Anything, signed(4, true)).Return(retry3, nil).Maybe() + mc.On("SimulateTx", mock.Anything, signed(0, true), mock.Anything).Run(func(mock.Arguments) { wg.Done() }).Return(&rpc.SimulateTransactionResult{}, nil).Once() @@ -509,8 +512,8 @@ func TestTxm(t *testing.T) { 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) { + mc.On("SendTx", mock.Anything, signed(0, true)).Return(sig, nil) + mc.On("SimulateTx", mock.Anything, signed(0, true), mock.Anything).Run(func(mock.Arguments) { wg.Done() }).Return(&rpc.SimulateTransactionResult{}, nil).Once() @@ -547,12 +550,12 @@ func TestTxm(t *testing.T) { 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) { + mc.On("SendTx", mock.Anything, signed(0, true)).Return(sig, nil) + mc.On("SendTx", mock.Anything, signed(1, true)).Return(retry0, nil) + mc.On("SendTx", mock.Anything, signed(2, true)).Return(retry1, nil) + mc.On("SendTx", mock.Anything, signed(3, true)).Return(retry2, nil).Maybe() + mc.On("SendTx", mock.Anything, signed(4, true)).Return(retry3, nil).Maybe() + mc.On("SimulateTx", mock.Anything, signed(0, true), mock.Anything).Run(func(mock.Arguments) { wg.Done() }).Return(&rpc.SimulateTransactionResult{}, nil).Once() @@ -584,21 +587,16 @@ func TestTxm(t *testing.T) { sig := getSig() tx, signed := getTx(t, 11, mkey, 0) - // disable fee bumping defaultFeeBumpPeriod := cfg.FeeBumpPeriod() - cfg.Chain.FeeBumpPeriod = relayconfig.MustNewDuration(0) - defer func() { - cfg.Chain.FeeBumpPeriod = relayconfig.MustNewDuration(defaultFeeBumpPeriod) // reset - }() sendCount := 0 var countRW sync.RWMutex - mc.On("SendTx", mock.Anything, signed(0)).Run(func(mock.Arguments) { + mc.On("SendTx", mock.Anything, signed(0, true)).Run(func(mock.Arguments) { countRW.Lock() sendCount++ countRW.Unlock() }).Return(sig, nil) // only sends one transaction type (no bumping) - mc.On("SimulateTx", mock.Anything, signed(0), mock.Anything).Return(&rpc.SimulateTransactionResult{}, nil).Once() + mc.On("SimulateTx", mock.Anything, signed(0, true), mock.Anything).Return(&rpc.SimulateTransactionResult{}, nil).Once() // handle signature status calls var wg sync.WaitGroup @@ -618,8 +616,8 @@ func TestTxm(t *testing.T) { return } - // send tx - assert.NoError(t, txm.Enqueue(t.Name(), tx)) + // send tx - with disabled fee bumping + assert.NoError(t, txm.Enqueue(t.Name(), tx, SetFeeBumpPeriod(0))) wg.Wait() // no transactions stored inflight txs list @@ -637,6 +635,41 @@ func TestTxm(t *testing.T) { prom.success++ prom.assertEqual(t) }) + + // compute unit limit disabled + t.Run("computeUnitLimitDisabled", func(t *testing.T) { + sig := getSig() + tx, signed := getTx(t, 12, mkey, 0) + + // should only match transaction without compute unit limit + assert.Len(t, signed(0, false).Message.Instructions, 2) + mc.On("SendTx", mock.Anything, signed(0, false)).Return(sig, nil) // only sends one transaction type (no bumping) + mc.On("SimulateTx", mock.Anything, signed(0, false), mock.Anything).Return(&rpc.SimulateTransactionResult{}, nil).Once() + + // handle signature status calls + var wg sync.WaitGroup + wg.Add(1) + statuses[sig] = func() *rpc.SignatureStatusesResult { + defer wg.Done() + return &rpc.SignatureStatusesResult{ + ConfirmationStatus: rpc.ConfirmationStatusConfirmed, + } + } + + // send tx - with disabled fee bumping and disabled compute unit limit + assert.NoError(t, txm.Enqueue(t.Name(), tx, SetFeeBumpPeriod(0), SetComputeUnitLimit(0))) + 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) + }) }) } } @@ -646,6 +679,8 @@ func TestTxm_Enqueue(t *testing.T) { lggr := logger.Test(t) cfg := config.NewDefault() mc := mocks.NewReaderWriter(t) + mc.On("SendTx", mock.Anything, mock.Anything).Return(solana.Signature{}, nil).Maybe() + ctx := tests.Context(t) // mock solana keystore mkey := keyMocks.NewSimpleKeystore(t) @@ -685,6 +720,10 @@ 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)) + t.Cleanup(func() { require.NoError(t, txm.Close()) }) + 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..aa0a6de6a 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(200_000)) // default value, cannot not use 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, 200_000)) 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, 200_000)) 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, 200_000)) 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, 200_000)) 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) +}