diff --git a/pkg/solana/client/client.go b/pkg/solana/client/client.go index f9f6715b0..a015fdc1f 100644 --- a/pkg/solana/client/client.go +++ b/pkg/solana/client/client.go @@ -36,8 +36,11 @@ type Reader interface { ChainID(ctx context.Context) (mn.StringID, error) GetFeeForMessage(ctx context.Context, msg string) (uint64, error) GetLatestBlock(ctx context.Context) (*rpc.GetBlockResult, error) + GetTransaction(ctx context.Context, txHash solana.Signature, opts *rpc.GetTransactionOpts) (*rpc.GetTransactionResult, error) + GetBlocks(ctx context.Context, startSlot uint64, endSlot *uint64) (rpc.BlocksResult, error) GetBlocksWithLimit(ctx context.Context, startSlot uint64, limit uint64) (*rpc.BlocksResult, error) GetBlock(ctx context.Context, slot uint64) (*rpc.GetBlockResult, error) + GetSignaturesForAddressWithOpts(ctx context.Context, addr solana.PublicKey, opts *rpc.GetSignaturesForAddressOpts) ([]*rpc.TransactionSignature, error) } // AccountReader is an interface that allows users to pass either the solana rpc client or the relay client @@ -121,6 +124,43 @@ func (c *Client) SlotHeightWithCommitment(ctx context.Context, commitment rpc.Co return v.(uint64), err } +func (c *Client) GetSignaturesForAddressWithOpts(ctx context.Context, addr solana.PublicKey, opts *rpc.GetSignaturesForAddressOpts) ([]*rpc.TransactionSignature, error) { + done := c.latency("signatures_for_address") + defer done() + + ctx, cancel := context.WithTimeout(ctx, c.contextDuration) + defer cancel() + if opts == nil { + opts = &rpc.GetSignaturesForAddressOpts{} + } + if opts.Commitment == "" { + opts.Commitment = c.commitment + } + return c.rpc.GetSignaturesForAddressWithOpts(ctx, addr, opts) +} + +func (c *Client) GetTransaction(ctx context.Context, txHash solana.Signature, opts *rpc.GetTransactionOpts) (*rpc.GetTransactionResult, error) { + done := c.latency("transaction") + defer done() + + ctx, cancel := context.WithTimeout(ctx, c.contextDuration) + defer cancel() + + if opts == nil { + opts = &rpc.GetTransactionOpts{ + Encoding: solana.EncodingBase64, + } + } + if opts.Commitment == "" { + opts.Commitment = c.commitment + } + + v, err, _ := c.requestGroup.Do("GetTransaction", func() (interface{}, error) { + return c.rpc.GetTransaction(ctx, txHash, opts) + }) + return v.(*rpc.GetTransactionResult), err +} + func (c *Client) GetAccountInfoWithOpts(ctx context.Context, addr solana.PublicKey, opts *rpc.GetAccountInfoOpts) (*rpc.GetAccountInfoResult, error) { done := c.latency("account_info") defer done() @@ -131,6 +171,19 @@ func (c *Client) GetAccountInfoWithOpts(ctx context.Context, addr solana.PublicK return c.rpc.GetAccountInfoWithOpts(ctx, addr, opts) } +func (c *Client) GetBlocks(ctx context.Context, startSlot uint64, endSlot *uint64) (out rpc.BlocksResult, err error) { + done := c.latency("blocks") + defer done() + + ctx, cancel := context.WithTimeout(ctx, c.contextDuration) + defer cancel() + + v, err, _ := c.requestGroup.Do("GetBlocks", func() (interface{}, error) { + return c.rpc.GetBlocks(ctx, startSlot, endSlot, c.commitment) + }) + return v.(rpc.BlocksResult), err +} + func (c *Client) LatestBlockhash(ctx context.Context) (*rpc.GetLatestBlockhashResult, error) { done := c.latency("latest_blockhash") defer done() diff --git a/pkg/solana/client/client_test.go b/pkg/solana/client/client_test.go index ceb94035e..20b67ac8d 100644 --- a/pkg/solana/client/client_test.go +++ b/pkg/solana/client/client_test.go @@ -242,6 +242,52 @@ func TestClient_Writer_Integration(t *testing.T) { assert.Nil(t, statuses[0].Err) assert.NotNil(t, statuses[1].Err) + + getTxResult, err := c.GetTransaction(ctx, sigSuccess, nil) + assert.NoError(t, err) + assert.NotNil(t, getTxResult) + + sigs, err := c.GetSignaturesForAddressWithOpts(ctx, pubKey, nil) + assert.NoError(t, err) + requiredSigs := map[solana.Signature]bool{ + sigSuccess: false, + sigFail: false, + } + for _, sig := range sigs { + if _, required := requiredSigs[sig.Signature]; required { + requiredSigs[sig.Signature] = true + } + } + require.True(t, requiredSigs[sigSuccess] && requiredSigs[sigFail]) +} + +func TestClient_GetBlocks(t *testing.T) { + url := SetupLocalSolNode(t) + privKey, err := solana.NewRandomPrivateKey() + require.NoError(t, err) + pubKey := privKey.PublicKey() + FundTestAccounts(t, []solana.PublicKey{pubKey}, url) + + requestTimeout := 5 * time.Second + lggr := logger.Test(t) + cfg := config.NewDefault() + + ctx := tests.Context(t) + c, err := NewClient(url, cfg, requestTimeout, lggr) + require.NoError(t, err) + + // Verify we can retrieve blocks + startSlot := uint64(1) + endSlot := uint64(6) + require.Eventually(t, + func() bool { + blocks, err := c.GetBlocks(ctx, startSlot, &endSlot) + if err != nil { + return false + } + return len(blocks) == 5 + }, + requestTimeout, 500*time.Millisecond) } func TestClient_SendTxDuplicates_Integration(t *testing.T) { diff --git a/pkg/solana/client/mocks/reader_writer.go b/pkg/solana/client/mocks/reader_writer.go index 86285fdf5..c64a4a9ad 100644 --- a/pkg/solana/client/mocks/reader_writer.go +++ b/pkg/solana/client/mocks/reader_writer.go @@ -257,6 +257,66 @@ func (_c *ReaderWriter_GetBlock_Call) RunAndReturn(run func(context.Context, uin return _c } +// GetBlocks provides a mock function with given fields: ctx, startSlot, endSlot +func (_m *ReaderWriter) GetBlocks(ctx context.Context, startSlot uint64, endSlot *uint64) (rpc.BlocksResult, error) { + ret := _m.Called(ctx, startSlot, endSlot) + + if len(ret) == 0 { + panic("no return value specified for GetBlocks") + } + + var r0 rpc.BlocksResult + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, uint64, *uint64) (rpc.BlocksResult, error)); ok { + return rf(ctx, startSlot, endSlot) + } + if rf, ok := ret.Get(0).(func(context.Context, uint64, *uint64) rpc.BlocksResult); ok { + r0 = rf(ctx, startSlot, endSlot) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(rpc.BlocksResult) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, uint64, *uint64) error); ok { + r1 = rf(ctx, startSlot, endSlot) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// ReaderWriter_GetBlocks_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetBlocks' +type ReaderWriter_GetBlocks_Call struct { + *mock.Call +} + +// GetBlocks is a helper method to define mock.On call +// - ctx context.Context +// - startSlot uint64 +// - endSlot *uint64 +func (_e *ReaderWriter_Expecter) GetBlocks(ctx interface{}, startSlot interface{}, endSlot interface{}) *ReaderWriter_GetBlocks_Call { + return &ReaderWriter_GetBlocks_Call{Call: _e.mock.On("GetBlocks", ctx, startSlot, endSlot)} +} + +func (_c *ReaderWriter_GetBlocks_Call) Run(run func(ctx context.Context, startSlot uint64, endSlot *uint64)) *ReaderWriter_GetBlocks_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(uint64), args[2].(*uint64)) + }) + return _c +} + +func (_c *ReaderWriter_GetBlocks_Call) Return(_a0 rpc.BlocksResult, _a1 error) *ReaderWriter_GetBlocks_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *ReaderWriter_GetBlocks_Call) RunAndReturn(run func(context.Context, uint64, *uint64) (rpc.BlocksResult, error)) *ReaderWriter_GetBlocks_Call { + _c.Call.Return(run) + return _c +} + // GetBlocksWithLimit provides a mock function with given fields: ctx, startSlot, limit func (_m *ReaderWriter) GetBlocksWithLimit(ctx context.Context, startSlot uint64, limit uint64) (*rpc.BlocksResult, error) { ret := _m.Called(ctx, startSlot, limit) @@ -432,6 +492,126 @@ func (_c *ReaderWriter_GetLatestBlock_Call) RunAndReturn(run func(context.Contex return _c } +// GetSignaturesForAddressWithOpts provides a mock function with given fields: ctx, addr, opts +func (_m *ReaderWriter) GetSignaturesForAddressWithOpts(ctx context.Context, addr solana.PublicKey, opts *rpc.GetSignaturesForAddressOpts) ([]*rpc.TransactionSignature, error) { + ret := _m.Called(ctx, addr, opts) + + if len(ret) == 0 { + panic("no return value specified for GetSignaturesForAddressWithOpts") + } + + var r0 []*rpc.TransactionSignature + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, solana.PublicKey, *rpc.GetSignaturesForAddressOpts) ([]*rpc.TransactionSignature, error)); ok { + return rf(ctx, addr, opts) + } + if rf, ok := ret.Get(0).(func(context.Context, solana.PublicKey, *rpc.GetSignaturesForAddressOpts) []*rpc.TransactionSignature); ok { + r0 = rf(ctx, addr, opts) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]*rpc.TransactionSignature) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, solana.PublicKey, *rpc.GetSignaturesForAddressOpts) error); ok { + r1 = rf(ctx, addr, opts) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// ReaderWriter_GetSignaturesForAddressWithOpts_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetSignaturesForAddressWithOpts' +type ReaderWriter_GetSignaturesForAddressWithOpts_Call struct { + *mock.Call +} + +// GetSignaturesForAddressWithOpts is a helper method to define mock.On call +// - ctx context.Context +// - addr solana.PublicKey +// - opts *rpc.GetSignaturesForAddressOpts +func (_e *ReaderWriter_Expecter) GetSignaturesForAddressWithOpts(ctx interface{}, addr interface{}, opts interface{}) *ReaderWriter_GetSignaturesForAddressWithOpts_Call { + return &ReaderWriter_GetSignaturesForAddressWithOpts_Call{Call: _e.mock.On("GetSignaturesForAddressWithOpts", ctx, addr, opts)} +} + +func (_c *ReaderWriter_GetSignaturesForAddressWithOpts_Call) Run(run func(ctx context.Context, addr solana.PublicKey, opts *rpc.GetSignaturesForAddressOpts)) *ReaderWriter_GetSignaturesForAddressWithOpts_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(solana.PublicKey), args[2].(*rpc.GetSignaturesForAddressOpts)) + }) + return _c +} + +func (_c *ReaderWriter_GetSignaturesForAddressWithOpts_Call) Return(_a0 []*rpc.TransactionSignature, _a1 error) *ReaderWriter_GetSignaturesForAddressWithOpts_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *ReaderWriter_GetSignaturesForAddressWithOpts_Call) RunAndReturn(run func(context.Context, solana.PublicKey, *rpc.GetSignaturesForAddressOpts) ([]*rpc.TransactionSignature, error)) *ReaderWriter_GetSignaturesForAddressWithOpts_Call { + _c.Call.Return(run) + return _c +} + +// GetTransaction provides a mock function with given fields: ctx, txHash, opts +func (_m *ReaderWriter) GetTransaction(ctx context.Context, txHash solana.Signature, opts *rpc.GetTransactionOpts) (*rpc.GetTransactionResult, error) { + ret := _m.Called(ctx, txHash, opts) + + if len(ret) == 0 { + panic("no return value specified for GetTransaction") + } + + var r0 *rpc.GetTransactionResult + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, solana.Signature, *rpc.GetTransactionOpts) (*rpc.GetTransactionResult, error)); ok { + return rf(ctx, txHash, opts) + } + if rf, ok := ret.Get(0).(func(context.Context, solana.Signature, *rpc.GetTransactionOpts) *rpc.GetTransactionResult); ok { + r0 = rf(ctx, txHash, opts) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*rpc.GetTransactionResult) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, solana.Signature, *rpc.GetTransactionOpts) error); ok { + r1 = rf(ctx, txHash, opts) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// ReaderWriter_GetTransaction_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetTransaction' +type ReaderWriter_GetTransaction_Call struct { + *mock.Call +} + +// GetTransaction is a helper method to define mock.On call +// - ctx context.Context +// - txHash solana.Signature +// - opts *rpc.GetTransactionOpts +func (_e *ReaderWriter_Expecter) GetTransaction(ctx interface{}, txHash interface{}, opts interface{}) *ReaderWriter_GetTransaction_Call { + return &ReaderWriter_GetTransaction_Call{Call: _e.mock.On("GetTransaction", ctx, txHash, opts)} +} + +func (_c *ReaderWriter_GetTransaction_Call) Run(run func(ctx context.Context, txHash solana.Signature, opts *rpc.GetTransactionOpts)) *ReaderWriter_GetTransaction_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(solana.Signature), args[2].(*rpc.GetTransactionOpts)) + }) + return _c +} + +func (_c *ReaderWriter_GetTransaction_Call) Return(_a0 *rpc.GetTransactionResult, _a1 error) *ReaderWriter_GetTransaction_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *ReaderWriter_GetTransaction_Call) RunAndReturn(run func(context.Context, solana.Signature, *rpc.GetTransactionOpts) (*rpc.GetTransactionResult, error)) *ReaderWriter_GetTransaction_Call { + _c.Call.Return(run) + return _c +} + // LatestBlockhash provides a mock function with given fields: ctx func (_m *ReaderWriter) LatestBlockhash(ctx context.Context) (*rpc.GetLatestBlockhashResult, error) { ret := _m.Called(ctx)