From e97ee34c4a7f95f4c774a1c56310abbc37e74db7 Mon Sep 17 00:00:00 2001 From: Domino Valdano <2644901+reductionista@users.noreply.github.com> Date: Mon, 9 Dec 2024 21:38:08 -0800 Subject: [PATCH 01/43] Add client & collector to LogPoller struct Also: - Add GetBlocksWithOpts to Reader - Update loader.RPClient to be consistent with client.Reader interface --- pkg/solana/client/client.go | 10 ++++++++++ pkg/solana/logpoller/loader.go | 8 ++++---- pkg/solana/logpoller/loader_test.go | 2 +- pkg/solana/logpoller/log_poller.go | 21 +++++++++++++++++---- 4 files changed, 32 insertions(+), 9 deletions(-) diff --git a/pkg/solana/client/client.go b/pkg/solana/client/client.go index 5eaa37b89..5fa529f12 100644 --- a/pkg/solana/client/client.go +++ b/pkg/solana/client/client.go @@ -41,6 +41,7 @@ type Reader interface { 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) + GetBlockWithOpts(context.Context, uint64, *rpc.GetBlockOpts) (*rpc.GetBlockResult, error) GetBlock(ctx context.Context, slot uint64) (*rpc.GetBlockResult, error) GetSignaturesForAddressWithOpts(ctx context.Context, addr solana.PublicKey, opts *rpc.GetSignaturesForAddressOpts) ([]*rpc.TransactionSignature, error) } @@ -346,6 +347,15 @@ func (c *Client) GetLatestBlockHeight(ctx context.Context) (uint64, error) { return v.(uint64), err } +func (c *Client) GetBlockWithOpts(ctx context.Context, slot uint64, opts *rpc.GetBlockOpts) (*rpc.GetBlockResult, error) { + // get block based on slot with custom options set + done := c.latency("get_block_with_opts") + defer done() + ctx, cancel := context.WithTimeout(ctx, c.txTimeout) + defer cancel() + return c.rpc.GetBlockWithOpts(ctx, slot, opts) +} + func (c *Client) GetBlock(ctx context.Context, slot uint64) (*rpc.GetBlockResult, error) { // get block based on slot done := c.latency("get_block") diff --git a/pkg/solana/logpoller/loader.go b/pkg/solana/logpoller/loader.go index d714f08ad..39a985a98 100644 --- a/pkg/solana/logpoller/loader.go +++ b/pkg/solana/logpoller/loader.go @@ -27,8 +27,8 @@ type ProgramEventProcessor interface { } type RPCClient interface { - GetLatestBlockhash(ctx context.Context, commitment rpc.CommitmentType) (out *rpc.GetLatestBlockhashResult, err error) - GetBlocks(ctx context.Context, startSlot uint64, endSlot *uint64, commitment rpc.CommitmentType) (out rpc.BlocksResult, err error) + LatestBlockhash(ctx context.Context) (out *rpc.GetLatestBlockhashResult, err error) + GetBlocks(ctx context.Context, startSlot uint64, endSlot *uint64) (out rpc.BlocksResult, err error) GetBlockWithOpts(context.Context, uint64, *rpc.GetBlockOpts) (*rpc.GetBlockResult, error) GetSignaturesForAddressWithOpts(context.Context, solana.PublicKey, *rpc.GetSignaturesForAddressOpts) ([]*rpc.TransactionSignature, error) } @@ -170,7 +170,7 @@ func (c *EncodedLogCollector) runSlotPolling(ctx context.Context) { ctxB, cancel := context.WithTimeout(ctx, c.rpcTimeLimit) // not to be run as a job, but as a blocking call - result, err := c.client.GetLatestBlockhash(ctxB, rpc.CommitmentFinalized) + result, err := c.client.LatestBlockhash(ctxB) if err != nil { c.lggr.Error("failed to get latest blockhash", "err", err) cancel() @@ -276,7 +276,7 @@ func (c *EncodedLogCollector) loadSlotBlocksRange(ctx context.Context, start, en rpcCtx, cancel := context.WithTimeout(ctx, c.rpcTimeLimit) defer cancel() - if result, err = c.client.GetBlocks(rpcCtx, start, &end, rpc.CommitmentFinalized); err != nil { + if result, err = c.client.GetBlocks(rpcCtx, start, &end); err != nil { return err } diff --git a/pkg/solana/logpoller/loader_test.go b/pkg/solana/logpoller/loader_test.go index e3cbb7700..bc24bb95d 100644 --- a/pkg/solana/logpoller/loader_test.go +++ b/pkg/solana/logpoller/loader_test.go @@ -19,7 +19,7 @@ import ( "github.com/smartcontractkit/chainlink-common/pkg/utils/tests" "github.com/smartcontractkit/chainlink-solana/pkg/solana/logpoller" - mocks "github.com/smartcontractkit/chainlink-solana/pkg/solana/logpoller/mocks" + "github.com/smartcontractkit/chainlink-solana/pkg/solana/logpoller/mocks" ) var ( diff --git a/pkg/solana/logpoller/log_poller.go b/pkg/solana/logpoller/log_poller.go index 4c386693e..bffbd57ee 100644 --- a/pkg/solana/logpoller/log_poller.go +++ b/pkg/solana/logpoller/log_poller.go @@ -7,6 +7,8 @@ import ( "github.com/smartcontractkit/chainlink-common/pkg/logger" "github.com/smartcontractkit/chainlink-common/pkg/services" + + "github.com/smartcontractkit/chainlink-solana/pkg/solana/client" ) var ( @@ -24,17 +26,21 @@ type ORM interface { type LogPoller struct { services.Service eng *services.Engine - - lggr logger.SugaredLogger - orm ORM + services.StateMachine + lggr logger.SugaredLogger + orm ORM + client client.Reader + collector *EncodedLogCollector filters *filters + events []ProgramEvent } -func New(lggr logger.SugaredLogger, orm ORM) *LogPoller { +func New(lggr logger.SugaredLogger, orm ORM, cl client.Reader) *LogPoller { lggr = logger.Sugared(logger.Named(lggr, "LogPoller")) lp := &LogPoller{ orm: orm, + client: cl, lggr: lggr, filters: newFilters(lggr, orm), } @@ -44,6 +50,8 @@ func New(lggr logger.SugaredLogger, orm ORM) *LogPoller { Start: lp.start, }.NewServiceEngine(lggr) lp.lggr = lp.eng.SugaredLogger + lp.collector = NewEncodedLogCollector(lp.client, lp, lp.lggr) + return lp } @@ -53,6 +61,11 @@ func (lp *LogPoller) start(context.Context) error { return nil } +func (lp LogPoller) Process(event ProgramEvent) error { + // process stream of events coming from event loader + return nil +} + // RegisterFilter - refer to filters.RegisterFilter for details. func (lp *LogPoller) RegisterFilter(ctx context.Context, filter Filter) error { ctx, cancel := lp.eng.Ctx(ctx) From b75a0e112bcb1031e72bb53b2807f1345c8b8ccf Mon Sep 17 00:00:00 2001 From: Domino Valdano <2644901+reductionista@users.noreply.github.com> Date: Wed, 11 Dec 2024 11:55:24 -0800 Subject: [PATCH 02/43] Re-generate RPClient & ReaderWriter mocks --- pkg/solana/client/mocks/reader_writer.go | 60 +++++++++++ pkg/solana/logpoller/mocks/rpc_client.go | 126 +++++++++++------------ 2 files changed, 122 insertions(+), 64 deletions(-) diff --git a/pkg/solana/client/mocks/reader_writer.go b/pkg/solana/client/mocks/reader_writer.go index 7c72ca183..0e7b1cbc2 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 } +// GetBlockWithOpts provides a mock function with given fields: _a0, _a1, _a2 +func (_m *ReaderWriter) GetBlockWithOpts(_a0 context.Context, _a1 uint64, _a2 *rpc.GetBlockOpts) (*rpc.GetBlockResult, error) { + ret := _m.Called(_a0, _a1, _a2) + + if len(ret) == 0 { + panic("no return value specified for GetBlockWithOpts") + } + + var r0 *rpc.GetBlockResult + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, uint64, *rpc.GetBlockOpts) (*rpc.GetBlockResult, error)); ok { + return rf(_a0, _a1, _a2) + } + if rf, ok := ret.Get(0).(func(context.Context, uint64, *rpc.GetBlockOpts) *rpc.GetBlockResult); ok { + r0 = rf(_a0, _a1, _a2) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*rpc.GetBlockResult) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, uint64, *rpc.GetBlockOpts) error); ok { + r1 = rf(_a0, _a1, _a2) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// ReaderWriter_GetBlockWithOpts_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetBlockWithOpts' +type ReaderWriter_GetBlockWithOpts_Call struct { + *mock.Call +} + +// GetBlockWithOpts is a helper method to define mock.On call +// - _a0 context.Context +// - _a1 uint64 +// - _a2 *rpc.GetBlockOpts +func (_e *ReaderWriter_Expecter) GetBlockWithOpts(_a0 interface{}, _a1 interface{}, _a2 interface{}) *ReaderWriter_GetBlockWithOpts_Call { + return &ReaderWriter_GetBlockWithOpts_Call{Call: _e.mock.On("GetBlockWithOpts", _a0, _a1, _a2)} +} + +func (_c *ReaderWriter_GetBlockWithOpts_Call) Run(run func(_a0 context.Context, _a1 uint64, _a2 *rpc.GetBlockOpts)) *ReaderWriter_GetBlockWithOpts_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(uint64), args[2].(*rpc.GetBlockOpts)) + }) + return _c +} + +func (_c *ReaderWriter_GetBlockWithOpts_Call) Return(_a0 *rpc.GetBlockResult, _a1 error) *ReaderWriter_GetBlockWithOpts_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *ReaderWriter_GetBlockWithOpts_Call) RunAndReturn(run func(context.Context, uint64, *rpc.GetBlockOpts) (*rpc.GetBlockResult, error)) *ReaderWriter_GetBlockWithOpts_Call { + _c.Call.Return(run) + 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) diff --git a/pkg/solana/logpoller/mocks/rpc_client.go b/pkg/solana/logpoller/mocks/rpc_client.go index 851eba9ec..1d112f399 100644 --- a/pkg/solana/logpoller/mocks/rpc_client.go +++ b/pkg/solana/logpoller/mocks/rpc_client.go @@ -85,9 +85,9 @@ func (_c *RPCClient_GetBlockWithOpts_Call) RunAndReturn(run func(context.Context return _c } -// GetBlocks provides a mock function with given fields: ctx, startSlot, endSlot, commitment -func (_m *RPCClient) GetBlocks(ctx context.Context, startSlot uint64, endSlot *uint64, commitment rpc.CommitmentType) (rpc.BlocksResult, error) { - ret := _m.Called(ctx, startSlot, endSlot, commitment) +// GetBlocks provides a mock function with given fields: ctx, startSlot, endSlot +func (_m *RPCClient) 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") @@ -95,19 +95,19 @@ func (_m *RPCClient) GetBlocks(ctx context.Context, startSlot uint64, endSlot *u var r0 rpc.BlocksResult var r1 error - if rf, ok := ret.Get(0).(func(context.Context, uint64, *uint64, rpc.CommitmentType) (rpc.BlocksResult, error)); ok { - return rf(ctx, startSlot, endSlot, commitment) + 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.CommitmentType) rpc.BlocksResult); ok { - r0 = rf(ctx, startSlot, endSlot, commitment) + 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, rpc.CommitmentType) error); ok { - r1 = rf(ctx, startSlot, endSlot, commitment) + if rf, ok := ret.Get(1).(func(context.Context, uint64, *uint64) error); ok { + r1 = rf(ctx, startSlot, endSlot) } else { r1 = ret.Error(1) } @@ -124,14 +124,13 @@ type RPCClient_GetBlocks_Call struct { // - ctx context.Context // - startSlot uint64 // - endSlot *uint64 -// - commitment rpc.CommitmentType -func (_e *RPCClient_Expecter) GetBlocks(ctx interface{}, startSlot interface{}, endSlot interface{}, commitment interface{}) *RPCClient_GetBlocks_Call { - return &RPCClient_GetBlocks_Call{Call: _e.mock.On("GetBlocks", ctx, startSlot, endSlot, commitment)} +func (_e *RPCClient_Expecter) GetBlocks(ctx interface{}, startSlot interface{}, endSlot interface{}) *RPCClient_GetBlocks_Call { + return &RPCClient_GetBlocks_Call{Call: _e.mock.On("GetBlocks", ctx, startSlot, endSlot)} } -func (_c *RPCClient_GetBlocks_Call) Run(run func(ctx context.Context, startSlot uint64, endSlot *uint64, commitment rpc.CommitmentType)) *RPCClient_GetBlocks_Call { +func (_c *RPCClient_GetBlocks_Call) Run(run func(ctx context.Context, startSlot uint64, endSlot *uint64)) *RPCClient_GetBlocks_Call { _c.Call.Run(func(args mock.Arguments) { - run(args[0].(context.Context), args[1].(uint64), args[2].(*uint64), args[3].(rpc.CommitmentType)) + run(args[0].(context.Context), args[1].(uint64), args[2].(*uint64)) }) return _c } @@ -141,34 +140,34 @@ func (_c *RPCClient_GetBlocks_Call) Return(out rpc.BlocksResult, err error) *RPC return _c } -func (_c *RPCClient_GetBlocks_Call) RunAndReturn(run func(context.Context, uint64, *uint64, rpc.CommitmentType) (rpc.BlocksResult, error)) *RPCClient_GetBlocks_Call { +func (_c *RPCClient_GetBlocks_Call) RunAndReturn(run func(context.Context, uint64, *uint64) (rpc.BlocksResult, error)) *RPCClient_GetBlocks_Call { _c.Call.Return(run) return _c } -// GetLatestBlockhash provides a mock function with given fields: ctx, commitment -func (_m *RPCClient) GetLatestBlockhash(ctx context.Context, commitment rpc.CommitmentType) (*rpc.GetLatestBlockhashResult, error) { - ret := _m.Called(ctx, commitment) +// GetSignaturesForAddressWithOpts provides a mock function with given fields: _a0, _a1, _a2 +func (_m *RPCClient) GetSignaturesForAddressWithOpts(_a0 context.Context, _a1 solana.PublicKey, _a2 *rpc.GetSignaturesForAddressOpts) ([]*rpc.TransactionSignature, error) { + ret := _m.Called(_a0, _a1, _a2) if len(ret) == 0 { - panic("no return value specified for GetLatestBlockhash") + panic("no return value specified for GetSignaturesForAddressWithOpts") } - var r0 *rpc.GetLatestBlockhashResult + var r0 []*rpc.TransactionSignature var r1 error - if rf, ok := ret.Get(0).(func(context.Context, rpc.CommitmentType) (*rpc.GetLatestBlockhashResult, error)); ok { - return rf(ctx, commitment) + if rf, ok := ret.Get(0).(func(context.Context, solana.PublicKey, *rpc.GetSignaturesForAddressOpts) ([]*rpc.TransactionSignature, error)); ok { + return rf(_a0, _a1, _a2) } - if rf, ok := ret.Get(0).(func(context.Context, rpc.CommitmentType) *rpc.GetLatestBlockhashResult); ok { - r0 = rf(ctx, commitment) + if rf, ok := ret.Get(0).(func(context.Context, solana.PublicKey, *rpc.GetSignaturesForAddressOpts) []*rpc.TransactionSignature); ok { + r0 = rf(_a0, _a1, _a2) } else { if ret.Get(0) != nil { - r0 = ret.Get(0).(*rpc.GetLatestBlockhashResult) + r0 = ret.Get(0).([]*rpc.TransactionSignature) } } - if rf, ok := ret.Get(1).(func(context.Context, rpc.CommitmentType) error); ok { - r1 = rf(ctx, commitment) + if rf, ok := ret.Get(1).(func(context.Context, solana.PublicKey, *rpc.GetSignaturesForAddressOpts) error); ok { + r1 = rf(_a0, _a1, _a2) } else { r1 = ret.Error(1) } @@ -176,58 +175,59 @@ func (_m *RPCClient) GetLatestBlockhash(ctx context.Context, commitment rpc.Comm return r0, r1 } -// RPCClient_GetLatestBlockhash_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetLatestBlockhash' -type RPCClient_GetLatestBlockhash_Call struct { +// RPCClient_GetSignaturesForAddressWithOpts_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetSignaturesForAddressWithOpts' +type RPCClient_GetSignaturesForAddressWithOpts_Call struct { *mock.Call } -// GetLatestBlockhash is a helper method to define mock.On call -// - ctx context.Context -// - commitment rpc.CommitmentType -func (_e *RPCClient_Expecter) GetLatestBlockhash(ctx interface{}, commitment interface{}) *RPCClient_GetLatestBlockhash_Call { - return &RPCClient_GetLatestBlockhash_Call{Call: _e.mock.On("GetLatestBlockhash", ctx, commitment)} +// GetSignaturesForAddressWithOpts is a helper method to define mock.On call +// - _a0 context.Context +// - _a1 solana.PublicKey +// - _a2 *rpc.GetSignaturesForAddressOpts +func (_e *RPCClient_Expecter) GetSignaturesForAddressWithOpts(_a0 interface{}, _a1 interface{}, _a2 interface{}) *RPCClient_GetSignaturesForAddressWithOpts_Call { + return &RPCClient_GetSignaturesForAddressWithOpts_Call{Call: _e.mock.On("GetSignaturesForAddressWithOpts", _a0, _a1, _a2)} } -func (_c *RPCClient_GetLatestBlockhash_Call) Run(run func(ctx context.Context, commitment rpc.CommitmentType)) *RPCClient_GetLatestBlockhash_Call { +func (_c *RPCClient_GetSignaturesForAddressWithOpts_Call) Run(run func(_a0 context.Context, _a1 solana.PublicKey, _a2 *rpc.GetSignaturesForAddressOpts)) *RPCClient_GetSignaturesForAddressWithOpts_Call { _c.Call.Run(func(args mock.Arguments) { - run(args[0].(context.Context), args[1].(rpc.CommitmentType)) + run(args[0].(context.Context), args[1].(solana.PublicKey), args[2].(*rpc.GetSignaturesForAddressOpts)) }) return _c } -func (_c *RPCClient_GetLatestBlockhash_Call) Return(out *rpc.GetLatestBlockhashResult, err error) *RPCClient_GetLatestBlockhash_Call { - _c.Call.Return(out, err) +func (_c *RPCClient_GetSignaturesForAddressWithOpts_Call) Return(_a0 []*rpc.TransactionSignature, _a1 error) *RPCClient_GetSignaturesForAddressWithOpts_Call { + _c.Call.Return(_a0, _a1) return _c } -func (_c *RPCClient_GetLatestBlockhash_Call) RunAndReturn(run func(context.Context, rpc.CommitmentType) (*rpc.GetLatestBlockhashResult, error)) *RPCClient_GetLatestBlockhash_Call { +func (_c *RPCClient_GetSignaturesForAddressWithOpts_Call) RunAndReturn(run func(context.Context, solana.PublicKey, *rpc.GetSignaturesForAddressOpts) ([]*rpc.TransactionSignature, error)) *RPCClient_GetSignaturesForAddressWithOpts_Call { _c.Call.Return(run) return _c } -// GetSignaturesForAddressWithOpts provides a mock function with given fields: _a0, _a1, _a2 -func (_m *RPCClient) GetSignaturesForAddressWithOpts(_a0 context.Context, _a1 solana.PublicKey, _a2 *rpc.GetSignaturesForAddressOpts) ([]*rpc.TransactionSignature, error) { - ret := _m.Called(_a0, _a1, _a2) +// LatestBlockhash provides a mock function with given fields: ctx +func (_m *RPCClient) LatestBlockhash(ctx context.Context) (*rpc.GetLatestBlockhashResult, error) { + ret := _m.Called(ctx) if len(ret) == 0 { - panic("no return value specified for GetSignaturesForAddressWithOpts") + panic("no return value specified for LatestBlockhash") } - var r0 []*rpc.TransactionSignature + var r0 *rpc.GetLatestBlockhashResult var r1 error - if rf, ok := ret.Get(0).(func(context.Context, solana.PublicKey, *rpc.GetSignaturesForAddressOpts) ([]*rpc.TransactionSignature, error)); ok { - return rf(_a0, _a1, _a2) + if rf, ok := ret.Get(0).(func(context.Context) (*rpc.GetLatestBlockhashResult, error)); ok { + return rf(ctx) } - if rf, ok := ret.Get(0).(func(context.Context, solana.PublicKey, *rpc.GetSignaturesForAddressOpts) []*rpc.TransactionSignature); ok { - r0 = rf(_a0, _a1, _a2) + if rf, ok := ret.Get(0).(func(context.Context) *rpc.GetLatestBlockhashResult); ok { + r0 = rf(ctx) } else { if ret.Get(0) != nil { - r0 = ret.Get(0).([]*rpc.TransactionSignature) + r0 = ret.Get(0).(*rpc.GetLatestBlockhashResult) } } - if rf, ok := ret.Get(1).(func(context.Context, solana.PublicKey, *rpc.GetSignaturesForAddressOpts) error); ok { - r1 = rf(_a0, _a1, _a2) + if rf, ok := ret.Get(1).(func(context.Context) error); ok { + r1 = rf(ctx) } else { r1 = ret.Error(1) } @@ -235,32 +235,30 @@ func (_m *RPCClient) GetSignaturesForAddressWithOpts(_a0 context.Context, _a1 so return r0, r1 } -// RPCClient_GetSignaturesForAddressWithOpts_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetSignaturesForAddressWithOpts' -type RPCClient_GetSignaturesForAddressWithOpts_Call struct { +// RPCClient_LatestBlockhash_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'LatestBlockhash' +type RPCClient_LatestBlockhash_Call struct { *mock.Call } -// GetSignaturesForAddressWithOpts is a helper method to define mock.On call -// - _a0 context.Context -// - _a1 solana.PublicKey -// - _a2 *rpc.GetSignaturesForAddressOpts -func (_e *RPCClient_Expecter) GetSignaturesForAddressWithOpts(_a0 interface{}, _a1 interface{}, _a2 interface{}) *RPCClient_GetSignaturesForAddressWithOpts_Call { - return &RPCClient_GetSignaturesForAddressWithOpts_Call{Call: _e.mock.On("GetSignaturesForAddressWithOpts", _a0, _a1, _a2)} +// LatestBlockhash is a helper method to define mock.On call +// - ctx context.Context +func (_e *RPCClient_Expecter) LatestBlockhash(ctx interface{}) *RPCClient_LatestBlockhash_Call { + return &RPCClient_LatestBlockhash_Call{Call: _e.mock.On("LatestBlockhash", ctx)} } -func (_c *RPCClient_GetSignaturesForAddressWithOpts_Call) Run(run func(_a0 context.Context, _a1 solana.PublicKey, _a2 *rpc.GetSignaturesForAddressOpts)) *RPCClient_GetSignaturesForAddressWithOpts_Call { +func (_c *RPCClient_LatestBlockhash_Call) Run(run func(ctx context.Context)) *RPCClient_LatestBlockhash_Call { _c.Call.Run(func(args mock.Arguments) { - run(args[0].(context.Context), args[1].(solana.PublicKey), args[2].(*rpc.GetSignaturesForAddressOpts)) + run(args[0].(context.Context)) }) return _c } -func (_c *RPCClient_GetSignaturesForAddressWithOpts_Call) Return(_a0 []*rpc.TransactionSignature, _a1 error) *RPCClient_GetSignaturesForAddressWithOpts_Call { - _c.Call.Return(_a0, _a1) +func (_c *RPCClient_LatestBlockhash_Call) Return(out *rpc.GetLatestBlockhashResult, err error) *RPCClient_LatestBlockhash_Call { + _c.Call.Return(out, err) return _c } -func (_c *RPCClient_GetSignaturesForAddressWithOpts_Call) RunAndReturn(run func(context.Context, solana.PublicKey, *rpc.GetSignaturesForAddressOpts) ([]*rpc.TransactionSignature, error)) *RPCClient_GetSignaturesForAddressWithOpts_Call { +func (_c *RPCClient_LatestBlockhash_Call) RunAndReturn(run func(context.Context) (*rpc.GetLatestBlockhashResult, error)) *RPCClient_LatestBlockhash_Call { _c.Call.Return(run) return _c } From 4eefa8ab7cc7c98af7d1bb97589949b5ba0bec69 Mon Sep 17 00:00:00 2001 From: Domino Valdano <2644901+reductionista@users.noreply.github.com> Date: Wed, 11 Dec 2024 11:59:36 -0800 Subject: [PATCH 03/43] Update loader tests for interface --- pkg/solana/logpoller/loader_test.go | 25 +++++++++++-------------- 1 file changed, 11 insertions(+), 14 deletions(-) diff --git a/pkg/solana/logpoller/loader_test.go b/pkg/solana/logpoller/loader_test.go index bc24bb95d..4d3dcd8cc 100644 --- a/pkg/solana/logpoller/loader_test.go +++ b/pkg/solana/logpoller/loader_test.go @@ -63,7 +63,7 @@ func TestEncodedLogCollector_ParseSingleEvent(t *testing.T) { latest.Store(uint64(40)) client.EXPECT(). - GetLatestBlockhash(mock.Anything, rpc.CommitmentFinalized). + LatestBlockhash(mock.Anything). RunAndReturn(latestBlockhashReturnFunc(&latest)) client.EXPECT(). @@ -71,7 +71,6 @@ func TestEncodedLogCollector_ParseSingleEvent(t *testing.T) { mock.Anything, mock.MatchedBy(getBlocksStartValMatcher), mock.MatchedBy(getBlocksEndValMatcher(&latest)), - rpc.CommitmentFinalized, ). RunAndReturn(getBlocksReturnFunc(false)) @@ -139,7 +138,7 @@ func TestEncodedLogCollector_MultipleEventOrdered(t *testing.T) { } client.EXPECT(). - GetLatestBlockhash(mock.Anything, rpc.CommitmentFinalized). + LatestBlockhash(mock.Anything). RunAndReturn(latestBlockhashReturnFunc(&latest)) client.EXPECT(). @@ -147,7 +146,6 @@ func TestEncodedLogCollector_MultipleEventOrdered(t *testing.T) { mock.Anything, mock.MatchedBy(getBlocksStartValMatcher), mock.MatchedBy(getBlocksEndValMatcher(&latest)), - rpc.CommitmentFinalized, ). RunAndReturn(getBlocksReturnFunc(false)) @@ -298,7 +296,7 @@ func TestEncodedLogCollector_BackfillForAddress(t *testing.T) { // GetLatestBlockhash might be called at start-up; make it take some time because the result isn't needed for this test client.EXPECT(). - GetLatestBlockhash(mock.Anything, rpc.CommitmentFinalized). + LatestBlockhash(mock.Anything). RunAndReturn(latestBlockhashReturnFunc(&latest)). After(2 * time.Second). Maybe() @@ -308,7 +306,6 @@ func TestEncodedLogCollector_BackfillForAddress(t *testing.T) { mock.Anything, mock.MatchedBy(getBlocksStartValMatcher), mock.MatchedBy(getBlocksEndValMatcher(&latest)), - rpc.CommitmentFinalized, ). RunAndReturn(getBlocksReturnFunc(true)) @@ -455,7 +452,7 @@ func (p *testBlockProducer) Count() uint64 { return p.count } -func (p *testBlockProducer) GetLatestBlockhash(_ context.Context, _ rpc.CommitmentType) (out *rpc.GetLatestBlockhashResult, err error) { +func (p *testBlockProducer) LatestBlockhash(_ context.Context) (out *rpc.GetLatestBlockhashResult, err error) { p.b.Helper() p.mu.Lock() @@ -474,7 +471,7 @@ func (p *testBlockProducer) GetLatestBlockhash(_ context.Context, _ rpc.Commitme }, nil } -func (p *testBlockProducer) GetBlocks(_ context.Context, startSlot uint64, endSlot *uint64, _ rpc.CommitmentType) (out rpc.BlocksResult, err error) { +func (p *testBlockProducer) GetBlocks(_ context.Context, startSlot uint64, endSlot *uint64) (out rpc.BlocksResult, err error) { p.b.Helper() p.mu.Lock() @@ -486,7 +483,7 @@ func (p *testBlockProducer) GetBlocks(_ context.Context, startSlot uint64, endSl blocks[idx] = startSlot + uint64(idx) } - return rpc.BlocksResult(blocks), nil + return blocks, nil } func (p *testBlockProducer) GetBlockWithOpts(_ context.Context, block uint64, opts *rpc.GetBlockOpts) (*rpc.GetBlockResult, error) { @@ -589,8 +586,8 @@ func (p *testParser) Events() []logpoller.ProgramEvent { return p.events } -func latestBlockhashReturnFunc(latest *atomic.Uint64) func(context.Context, rpc.CommitmentType) (*rpc.GetLatestBlockhashResult, error) { - return func(ctx context.Context, ct rpc.CommitmentType) (*rpc.GetLatestBlockhashResult, error) { +func latestBlockhashReturnFunc(latest *atomic.Uint64) func(context.Context) (*rpc.GetLatestBlockhashResult, error) { + return func(ctx context.Context) (*rpc.GetLatestBlockhashResult, error) { defer func() { latest.Store(latest.Load() + 2) }() @@ -608,8 +605,8 @@ func latestBlockhashReturnFunc(latest *atomic.Uint64) func(context.Context, rpc. } } -func getBlocksReturnFunc(empty bool) func(context.Context, uint64, *uint64, rpc.CommitmentType) (rpc.BlocksResult, error) { - return func(_ context.Context, u1 uint64, u2 *uint64, _ rpc.CommitmentType) (rpc.BlocksResult, error) { +func getBlocksReturnFunc(empty bool) func(context.Context, uint64, *uint64) (rpc.BlocksResult, error) { + return func(_ context.Context, u1 uint64, u2 *uint64) (rpc.BlocksResult, error) { blocks := []uint64{} if !empty { @@ -619,7 +616,7 @@ func getBlocksReturnFunc(empty bool) func(context.Context, uint64, *uint64, rpc. } } - return rpc.BlocksResult(blocks), nil + return blocks, nil } } From 87bbf0a6a95a39e52e95a7d2284657899082b539 Mon Sep 17 00:00:00 2001 From: Domino Valdano <2644901+reductionista@users.noreply.github.com> Date: Wed, 11 Dec 2024 20:04:13 -0800 Subject: [PATCH 04/43] Add ILogpoller interface and call NewLogPoller() from NewChain() Note: this will hopefully be renamed to LogPoller later, once we find a better name for the struct. (logPoller, for consistency with evm?) and the PR's this depends on have been merged, to avoid merge conflicts --- pkg/solana/chain.go | 15 +++++++++++++-- pkg/solana/logpoller/log_poller.go | 20 ++++++++++++++++---- 2 files changed, 29 insertions(+), 6 deletions(-) diff --git a/pkg/solana/chain.go b/pkg/solana/chain.go index d137e75de..a433a298a 100644 --- a/pkg/solana/chain.go +++ b/pkg/solana/chain.go @@ -28,6 +28,7 @@ import ( mn "github.com/smartcontractkit/chainlink-solana/pkg/solana/client/multinode" "github.com/smartcontractkit/chainlink-solana/pkg/solana/config" "github.com/smartcontractkit/chainlink-solana/pkg/solana/internal" + "github.com/smartcontractkit/chainlink-solana/pkg/solana/logpoller" "github.com/smartcontractkit/chainlink-solana/pkg/solana/monitor" "github.com/smartcontractkit/chainlink-solana/pkg/solana/txm" txmutils "github.com/smartcontractkit/chainlink-solana/pkg/solana/txm/utils" @@ -38,6 +39,7 @@ type Chain interface { ID() string Config() config.Config + LogPoller() logpoller.ILogPoller TxManager() TxManager // Reader returns a new Reader from the available list of nodes (if there are multiple, it will randomly select one) Reader() (client.Reader, error) @@ -90,6 +92,7 @@ type chain struct { services.StateMachine id string cfg *config.TOMLConfig + lp logpoller.ILogPoller txm *txm.Txm balanceMonitor services.Service lggr logger.Logger @@ -237,6 +240,7 @@ func newChain(id string, cfg *config.TOMLConfig, ks core.Keystore, lggr logger.L clientCache: map[string]*verifiedCachedClient{}, } + var lc internal.Loader[client.Reader] = utils.NewLazyLoad(func() (client.Reader, error) { return ch.getClient() }) var tc internal.Loader[client.ReaderWriter] = utils.NewLazyLoad(func() (client.ReaderWriter, error) { return ch.getClient() }) var bc internal.Loader[monitor.BalanceClient] = utils.NewLazyLoad(func() (monitor.BalanceClient, error) { return ch.getClient() }) // getClient returns random client or if MultiNodeEnabled RPC picked and controlled by MultiNode @@ -307,10 +311,13 @@ func newChain(id string, cfg *config.TOMLConfig, ks core.Keystore, lggr logger.L return result.Signature(), result.Error() } - tc = internal.NewLoader[client.ReaderWriter](func() (client.ReaderWriter, error) { return ch.multiNode.SelectRPC() }) - bc = internal.NewLoader[monitor.BalanceClient](func() (monitor.BalanceClient, error) { return ch.multiNode.SelectRPC() }) + // TODO: Can we just remove these? They nullify the lazy loaders initialized earlier, don't they? + //lc = internal.NewLoader[client.Reader](func() (client.Reader, error) { return ch.multiNode.SelectRPC() }) + //tc = internal.NewLoader[client.ReaderWriter](func() (client.ReaderWriter, error) { return ch.multiNode.SelectRPC() }) + //bc = internal.NewLoader[monitor.BalanceClient](func() (monitor.BalanceClient, error) { return ch.multiNode.SelectRPC() }) } + ch.lp = logpoller.NewLogPoller(logger.Sugared(logger.Named(lggr, "LogPoller")), logpoller.NewORM(ch.ID(), ds, lggr), lc) ch.txm = txm.NewTxm(ch.id, tc, sendTx, cfg, ks, lggr) ch.balanceMonitor = monitor.NewBalanceMonitor(ch.id, cfg, lggr, ks, bc) return &ch, nil @@ -400,6 +407,10 @@ func (c *chain) Config() config.Config { return c.cfg } +func (c *chain) LogPoller() logpoller.ILogPoller { + return c.lp +} + func (c *chain) TxManager() TxManager { return c.txm } diff --git a/pkg/solana/logpoller/log_poller.go b/pkg/solana/logpoller/log_poller.go index bffbd57ee..d9a9b5c08 100644 --- a/pkg/solana/logpoller/log_poller.go +++ b/pkg/solana/logpoller/log_poller.go @@ -9,6 +9,7 @@ import ( "github.com/smartcontractkit/chainlink-common/pkg/services" "github.com/smartcontractkit/chainlink-solana/pkg/solana/client" + "github.com/smartcontractkit/chainlink-solana/pkg/solana/internal" ) var ( @@ -23,25 +24,31 @@ type ORM interface { MarkFilterBackfilled(ctx context.Context, id int64) (err error) } +type ILogPoller interface { + Start(context.Context) error + Close() error + RegisterFilter(ctx context.Context, filter Filter) error + UnregisterFilter(ctx context.Context, name string) error +} + type LogPoller struct { services.Service eng *services.Engine services.StateMachine lggr logger.SugaredLogger orm ORM - client client.Reader + client internal.Loader[client.Reader] collector *EncodedLogCollector filters *filters events []ProgramEvent } -func New(lggr logger.SugaredLogger, orm ORM, cl client.Reader) *LogPoller { +func New(lggr logger.SugaredLogger, orm ORM, cl internal.Loader[client.Reader]) ILogPoller { lggr = logger.Sugared(logger.Named(lggr, "LogPoller")) lp := &LogPoller{ orm: orm, client: cl, - lggr: lggr, filters: newFilters(lggr, orm), } @@ -50,7 +57,6 @@ func New(lggr logger.SugaredLogger, orm ORM, cl client.Reader) *LogPoller { Start: lp.start, }.NewServiceEngine(lggr) lp.lggr = lp.eng.SugaredLogger - lp.collector = NewEncodedLogCollector(lp.client, lp, lp.lggr) return lp } @@ -58,11 +64,17 @@ func New(lggr logger.SugaredLogger, orm ORM, cl client.Reader) *LogPoller { func (lp *LogPoller) start(context.Context) error { lp.eng.Go(lp.run) lp.eng.Go(lp.backgroundWorkerRun) + cl, err := lp.client.Get() + if err != nil { + return err + } + lp.collector = NewEncodedLogCollector(cl, lp, lp.lggr) return nil } func (lp LogPoller) Process(event ProgramEvent) error { // process stream of events coming from event loader + return nil } From 1674be0de2f783b464950e408a63a1b334341e15 Mon Sep 17 00:00:00 2001 From: Domino Valdano <2644901+reductionista@users.noreply.github.com> Date: Wed, 11 Dec 2024 20:17:44 -0800 Subject: [PATCH 05/43] fix orm_test.go --- pkg/solana/logpoller/orm_test.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pkg/solana/logpoller/orm_test.go b/pkg/solana/logpoller/orm_test.go index 53512d696..272abc10e 100644 --- a/pkg/solana/logpoller/orm_test.go +++ b/pkg/solana/logpoller/orm_test.go @@ -8,13 +8,12 @@ import ( "github.com/gagliardetto/solana-go" "github.com/google/uuid" - "github.com/stretchr/testify/require" _ "github.com/jackc/pgx/v4/stdlib" - "github.com/smartcontractkit/chainlink-common/pkg/logger" "github.com/smartcontractkit/chainlink-common/pkg/sqlutil/pg" "github.com/smartcontractkit/chainlink-common/pkg/utils/tests" + "github.com/stretchr/testify/require" ) // NOTE: at the moment it's not possible to run all db tests at once. This issue will be addressed separately From f16c435192222a4031263661375a3eaf8718d4c7 Mon Sep 17 00:00:00 2001 From: Domino Valdano <2644901+reductionista@users.noreply.github.com> Date: Wed, 11 Dec 2024 20:36:59 -0800 Subject: [PATCH 06/43] Fix lints in log_poller.go & types.go --- pkg/solana/logpoller/filters_test.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pkg/solana/logpoller/filters_test.go b/pkg/solana/logpoller/filters_test.go index 9f8058703..0f39b60b4 100644 --- a/pkg/solana/logpoller/filters_test.go +++ b/pkg/solana/logpoller/filters_test.go @@ -132,7 +132,8 @@ func TestFilters_RegisterFilter(t *testing.T) { filter := Filter{Name: filterName} err := fs.RegisterFilter(tests.Context(t), filter) require.Error(t, err) - // can readd after db issue is resolved + + // can read after db issue is resolved orm.On("InsertFilter", mock.Anything, mock.Anything).Return(int64(1), nil).Once() err = fs.RegisterFilter(tests.Context(t), filter) require.NoError(t, err) From a236d9af860bdb10f596a81c49a8e237cdbf8c7b Mon Sep 17 00:00:00 2001 From: Domino Valdano <2644901+reductionista@users.noreply.github.com> Date: Wed, 11 Dec 2024 20:37:53 -0800 Subject: [PATCH 07/43] Fix lints in log_poller.go --- pkg/solana/logpoller/log_poller.go | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/pkg/solana/logpoller/log_poller.go b/pkg/solana/logpoller/log_poller.go index d9a9b5c08..6d45652b8 100644 --- a/pkg/solana/logpoller/log_poller.go +++ b/pkg/solana/logpoller/log_poller.go @@ -72,9 +72,9 @@ func (lp *LogPoller) start(context.Context) error { return nil } -func (lp LogPoller) Process(event ProgramEvent) error { +func (lp *LogPoller) Process(event ProgramEvent) error { // process stream of events coming from event loader - + lp.events = append(lp.events, event) return nil } @@ -107,9 +107,8 @@ func (lp *LogPoller) loadFilters(ctx context.Context) error { lp.lggr.Errorw("Failed loading filters in init logpoller loop, retrying later", "err", err) continue } - - return nil } + // unreachable } func (lp *LogPoller) run(ctx context.Context) { From cab34e0a3759273569a6c530d426ca5854f1870c Mon Sep 17 00:00:00 2001 From: Domino Valdano <2644901+reductionista@users.noreply.github.com> Date: Mon, 16 Dec 2024 18:09:10 -0800 Subject: [PATCH 08/43] Add BlockTime & Program to Event data passed to Process() --- pkg/solana/logpoller/job.go | 14 +++++++++++--- pkg/solana/logpoller/log_data_parser.go | 7 +++++-- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/pkg/solana/logpoller/job.go b/pkg/solana/logpoller/job.go index 165c0b5fe..8e24d4165 100644 --- a/pkg/solana/logpoller/job.go +++ b/pkg/solana/logpoller/job.go @@ -36,6 +36,7 @@ type eventDetail struct { slotNumber uint64 blockHeight uint64 blockHash solana.Hash + blockTime solana.UnixTimeSeconds trxIdx int trxSig solana.Signature } @@ -114,12 +115,18 @@ func (j *getTransactionsFromBlockJob) Run(ctx context.Context) error { blockHash: block.Blockhash, } - if block.BlockHeight != nil { - detail.blockHeight = *block.BlockHeight + if block.BlockHeight == nil { + return fmt.Errorf("block at slot %d returned from rpc is missing block number", j.slotNumber) + } + detail.blockHeight = *block.BlockHeight + + if block.BlockTime == nil { + return fmt.Errorf("received block %d from rpc with missing block time", block.BlockHeight) + detail.blockTime = *block.BlockTime } if len(block.Transactions) != len(blockSigsOnly.Signatures) { - return fmt.Errorf("block %d has %d transactions but %d signatures", j.slotNumber, len(block.Transactions), len(blockSigsOnly.Signatures)) + return fmt.Errorf("block %d has %d transactions but %d signatures", block.BlockHeight, len(block.Transactions), len(blockSigsOnly.Signatures)) } j.parser.ExpectTxs(j.slotNumber, len(block.Transactions)) @@ -143,6 +150,7 @@ func messagesToEvents(messages []string, parser ProgramEventProcessor, detail ev event.SlotNumber = detail.slotNumber event.BlockHeight = detail.blockHeight event.BlockHash = detail.blockHash + event.BlockTime = detail.blockTime event.TransactionHash = detail.trxSig event.TransactionIndex = detail.trxIdx event.TransactionLogIndex = logIdx diff --git a/pkg/solana/logpoller/log_data_parser.go b/pkg/solana/logpoller/log_data_parser.go index 4080a09e2..2549c40bc 100644 --- a/pkg/solana/logpoller/log_data_parser.go +++ b/pkg/solana/logpoller/log_data_parser.go @@ -19,6 +19,7 @@ type BlockData struct { SlotNumber uint64 BlockHeight uint64 BlockHash solana.Hash + BlockTime solana.UnixTimeSeconds TransactionHash solana.Signature TransactionIndex int TransactionLogIndex uint @@ -31,6 +32,7 @@ type ProgramLog struct { } type ProgramEvent struct { + Program string BlockData Prefix string Data string @@ -78,8 +80,9 @@ func parseProgramLogs(logs []string) []ProgramOutput { if len(dataMatches) > 1 { instLogs[lastLogIdx].Events = append(instLogs[lastLogIdx].Events, ProgramEvent{ - Prefix: prefixBuilder(depth), - Data: dataMatches[1], + Program: instLogs[lastLogIdx].Program, + Prefix: prefixBuilder(depth), + Data: dataMatches[1], }) } } else if strings.HasPrefix(log, "Log truncated") { From b0e96b37a78a1a3359a5c3f3d98b3ed5e33e56c2 Mon Sep 17 00:00:00 2001 From: Domino Valdano <2644901+reductionista@users.noreply.github.com> Date: Mon, 16 Dec 2024 12:07:43 -0800 Subject: [PATCH 09/43] WIP --- pkg/solana/codec/solana_test.go | 7 + pkg/solana/logpoller/filters.go | 104 +++++++++- pkg/solana/logpoller/log_poller.go | 86 +++++++- pkg/solana/logpoller/models.go | 4 +- pkg/solana/logpoller/utils/anchor.go | 290 +++++++++++++++++++++++++++ 5 files changed, 478 insertions(+), 13 deletions(-) create mode 100644 pkg/solana/logpoller/utils/anchor.go diff --git a/pkg/solana/codec/solana_test.go b/pkg/solana/codec/solana_test.go index 4dd116691..57e921fb7 100644 --- a/pkg/solana/codec/solana_test.go +++ b/pkg/solana/codec/solana_test.go @@ -143,6 +143,13 @@ func TestNewIDLCodec_CircularDependency(t *testing.T) { assert.ErrorIs(t, err, types.ErrInvalidConfig) } +func TestNewIDLInstructionCodec(t *testing.T) { + t.Parallel() + + var idl codec.IDL + +} + func newTestIDLAndCodec(t *testing.T, account bool) (string, codec.IDL, types.RemoteCodec) { t.Helper() diff --git a/pkg/solana/logpoller/filters.go b/pkg/solana/logpoller/filters.go index 4a1496371..14754b406 100644 --- a/pkg/solana/logpoller/filters.go +++ b/pkg/solana/logpoller/filters.go @@ -2,6 +2,7 @@ package logpoller import ( "context" + "encoding/base64" "errors" "fmt" "iter" @@ -9,20 +10,29 @@ import ( "sync" "sync/atomic" + "github.com/gagliardetto/solana-go" "github.com/smartcontractkit/chainlink-common/pkg/logger" + "github.com/smartcontractkit/chainlink-common/pkg/types" + + "github.com/smartcontractkit/chainlink-solana/pkg/solana/codec" + "github.com/smartcontractkit/chainlink-solana/pkg/solana/config" + "github.com/smartcontractkit/chainlink-solana/pkg/solana/logpoller/utils" ) type filters struct { orm ORM lggr logger.SugaredLogger - filtersByID map[int64]*Filter - filtersByName map[string]int64 - filtersByAddress map[PublicKey]map[EventSignature]map[int64]struct{} - filtersToBackfill map[int64]struct{} - filtersToDelete map[int64]Filter - filtersMutex sync.RWMutex - loadedFilters atomic.Bool + filtersByID map[int64]*Filter + filtersByName map[string]int64 + filtersByAddress map[PublicKey]map[EventSignature]map[int64]struct{} + filtersToBackfill map[int64]struct{} + filtersToDelete map[int64]Filter + filtersMutex sync.RWMutex + loadedFilters atomic.Bool + eventCodecs map[int64]types.RemoteCodec + knownPrograms map[string]uint // fast lookup to see if a base58-encoded ProgramID matches any registered filters + knownDiscriminators map[string]uint // fast lookup by first 10 characters (60-bits) of a base64-encoded discriminator } func newFilters(lggr logger.SugaredLogger, orm ORM) *filters { @@ -76,6 +86,13 @@ func (fl *filters) RegisterFilter(ctx context.Context, filter Filter) error { return fmt.Errorf("failed to load filters: %w", err) } + eventCodec, err := codec.NewIDLEventCodec(filter.EventIDL, config.BuilderForEncoding(config.EncodingTypeBorsh)) + if err != nil { + return fmt.Errorf("invalid event IDL for filter %s: %w", filter.Name, err) + } + + filter.EventSig = utils.Discriminator("event", filter.EventName) + fl.filtersMutex.Lock() defer fl.filtersMutex.Unlock() @@ -118,6 +135,23 @@ func (fl *filters) RegisterFilter(ctx context.Context, filter Filter) error { if !filter.IsBackfilled { fl.filtersToBackfill[filter.ID] = struct{}{} } + + fl.eventCodecs[filter.ID] = eventCodec + + programID := filter.Address.ToSolana().String() + if _, ok := fl.knownPrograms[programID]; !ok { + fl.knownPrograms[programID] = 1 + } else { + fl.knownPrograms[programID]++ + } + + discriminator := base64.StdEncoding.EncodeToString(filter.EventSig[:])[:10] + if _, ok := fl.knownPrograms[programID]; !ok { + fl.knownDiscriminators[discriminator] = 1 + } else { + fl.knownDiscriminators[discriminator]++ + } + return nil } @@ -180,6 +214,26 @@ func (fl *filters) removeFilterFromIndexes(filter Filter) { if len(filtersForAddress) == 0 { delete(fl.filtersByAddress, filter.Address) } + + programID := filter.Address.ToSolana().String() + if refcount, ok := fl.knownPrograms[programID]; ok { + refcount-- + if refcount > 0 { + fl.knownPrograms[programID] = refcount + } else { + delete(fl.knownPrograms, programID) + } + } + + discriminator := base64.StdEncoding.EncodeToString(filter.EventSig[:])[:10] + if refcount, ok := fl.knownDiscriminators[discriminator]; ok { + refcount-- + if refcount > 0 { + fl.knownDiscriminators[discriminator] = refcount + } else { + delete(fl.knownDiscriminators, discriminator) + } + } } // MatchingFilters - returns iterator to go through all matching filters. @@ -210,6 +264,42 @@ func (fl *filters) MatchingFilters(addr PublicKey, eventSignature EventSignature } } +func (fl *filters) EventCodec(ID int64) types.RemoteCodec { + return fl.eventCodecs[ID] +} + +// MatchchingFiltersForEncodedEvent - similar to MatchingFilters but accepts a raw encoded event. Under normal operation, +// this will be called on every new event that happens on the blockchain, so it's important it returns immediately if it +// doesn't match any registered filters. +func (fl *filters) MatchingFiltersForEncodedEvent(event ProgramEvent) iter.Seq[Filter] { + if _, ok := fl.knownPrograms[event.Program]; !ok { + return nil + } + + // The first 64-bits of the event data is the event sig. Because it's base64 encoded, this corresponds to + // the first 10 characters plus 4 bits of the 11th character. We can quickly rule it out as not matching any known + // discriminators if the first 10 characters don't match. If it passes that initial test, we base64-decode the + // first 11 characters, and use the first 8 bytes of that as the event sig to call MatchingFilters. The address + // also needs to be base58-decoded to pass to MatchingFilters + if _, ok := fl.knownDiscriminators[event.Data[:10]]; !ok { + return nil + } + + addr, err := solana.PublicKeyFromBase58(event.Program) + if err != nil { + fl.lggr.Errorw("failed to parse Program ID for event", "EventProgram", event) + return nil + } + decoded, err := base64.StdEncoding.DecodeString(event.Data[:11]) + if err != nil { + fl.lggr.Errorw("failed to decode event data", "EventProgram", event) + return nil + } + eventSig := EventSignature(decoded[:8]) + + return fl.MatchingFilters(PublicKey(addr), eventSig) +} + // GetFiltersToBackfill - returns copy of backfill queue // Requires LoadFilters to be called at least once. func (fl *filters) GetFiltersToBackfill() []Filter { diff --git a/pkg/solana/logpoller/log_poller.go b/pkg/solana/logpoller/log_poller.go index 6d45652b8..9c05503d8 100644 --- a/pkg/solana/logpoller/log_poller.go +++ b/pkg/solana/logpoller/log_poller.go @@ -2,11 +2,17 @@ package logpoller import ( "context" + "encoding/base64" "errors" + "fmt" + "math" + "reflect" "time" "github.com/smartcontractkit/chainlink-common/pkg/logger" "github.com/smartcontractkit/chainlink-common/pkg/services" + commontypes "github.com/smartcontractkit/chainlink-common/pkg/types" + "github.com/smartcontractkit/chainlink-common/pkg/utils" "github.com/smartcontractkit/chainlink-solana/pkg/solana/client" "github.com/smartcontractkit/chainlink-solana/pkg/solana/internal" @@ -17,11 +23,13 @@ var ( ) type ORM interface { + ChainID() string InsertFilter(ctx context.Context, filter Filter) (id int64, err error) SelectFilters(ctx context.Context) ([]Filter, error) DeleteFilters(ctx context.Context, filters map[int64]Filter) error MarkFilterDeleted(ctx context.Context, id int64) (err error) MarkFilterBackfilled(ctx context.Context, id int64) (err error) + InsertLogs(context.Context, []Log) (err error) } type ILogPoller interface { @@ -40,8 +48,12 @@ type LogPoller struct { client internal.Loader[client.Reader] collector *EncodedLogCollector - filters *filters - events []ProgramEvent + filters *filters + discriminatorLookup map[string]string + events []ProgramEvent + codec commontypes.RemoteCodec + + chStop services.StopChan } func New(lggr logger.SugaredLogger, orm ORM, cl internal.Loader[client.Reader]) ILogPoller { @@ -72,12 +84,76 @@ func (lp *LogPoller) start(context.Context) error { return nil } -func (lp *LogPoller) Process(event ProgramEvent) error { - // process stream of events coming from event loader - lp.events = append(lp.events, event) +func makeLogIndex(txIndex int, txLogIndex uint) int64 { + if txIndex < 0 || txIndex > math.MaxUint32 || txLogIndex > math.MaxUint32 { + panic(fmt.Sprintf("txIndex or txLogIndex out of range: txIndex=%d, txLogIndex=%d", txIndex, txLogIndex)) + } + return int64(math.MaxUint32*uint32(txIndex) + uint32(txLogIndex)) +} + +// Process - process stream of events coming from log ingester +func (lp *LogPoller) Process(programEvent ProgramEvent) (err error) { + ctx, cancel := utils.ContextFromChan(lp.chStop) + defer cancel() + + blockData := programEvent.BlockData + + var logs []Log + for filter := range lp.filters.MatchingFiltersForEncodedEvent(programEvent) { + log := Log{ + FilterID: filter.ID, + ChainID: lp.orm.ChainID(), + LogIndex: makeLogIndex(blockData.TransactionIndex, blockData.TransactionLogIndex), + BlockHash: Hash(blockData.BlockHash), + BlockNumber: int64(blockData.BlockHeight), + BlockTimestamp: blockData.BlockTime.Time(), // TODO: is this a timezone safe conversion? + Address: filter.Address, + EventSig: filter.EventSig, + TxHash: Signature(blockData.TransactionHash), + } + + log.Data, err = base64.StdEncoding.DecodeString(programEvent.Data) + if err != nil { + return err + } + + var event any + err = lp.filters.EventCodec(filter.ID).Decode(ctx, log.Data, &event, filter.EventName) + if err != nil { + return err + } + + err = lp.ExtractSubkeys(reflect.TypeOf(event), filter.SubkeyPaths) + if err != nil { + return err + } + + // TODO: fill in, and keep track of SequenceNumber for each filter. (Initialize from db on LoadFilters, then increment each time?) + + logs = append(logs, log) + } + + lp.orm.InsertLogs(ctx, logs) return nil } +func (lp *LogPoller) ExtractSubkeys(t reflect.Type, paths SubkeyPaths) error { + s := reflect.TypeOf(event) + if s.Kind() != reflect.Struct { + return fmt.Errorf("event type must be struct, got %v. event=%v", t, event) + } + + for _, path := range paths[0] { + field, err := s.FieldByName(path) + for depth := 0; depth < len(paths); depth++ { + for _, path := range paths[depth] { + field, err = field.Type.FieldByName(path) + } + } + } + +} + // RegisterFilter - refer to filters.RegisterFilter for details. func (lp *LogPoller) RegisterFilter(ctx context.Context, filter Filter) error { ctx, cancel := lp.eng.Ctx(ctx) diff --git a/pkg/solana/logpoller/models.go b/pkg/solana/logpoller/models.go index 0be5b4874..3249e86cf 100644 --- a/pkg/solana/logpoller/models.go +++ b/pkg/solana/logpoller/models.go @@ -4,6 +4,8 @@ import ( "time" "github.com/lib/pq" + + "github.com/smartcontractkit/chainlink-solana/pkg/solana/codec" ) type Filter struct { @@ -13,7 +15,7 @@ type Filter struct { EventName string EventSig EventSignature StartingBlock int64 - EventIDL string + EventIDL codec.IDL SubkeyPaths SubkeyPaths Retention time.Duration MaxLogsKept int64 diff --git a/pkg/solana/logpoller/utils/anchor.go b/pkg/solana/logpoller/utils/anchor.go new file mode 100644 index 000000000..4d11bcc83 --- /dev/null +++ b/pkg/solana/logpoller/utils/anchor.go @@ -0,0 +1,290 @@ +package utils + +import ( + "bytes" + "context" + "crypto/rand" + "crypto/sha256" + "encoding/base64" + "encoding/binary" + "fmt" + "regexp" + "strconv" + "strings" + "testing" + "time" + + bin "github.com/gagliardetto/binary" + "github.com/gagliardetto/solana-go" + "github.com/gagliardetto/solana-go/rpc" + + "github.com/stretchr/testify/require" + + "github.com/smartcontractkit/chainlink-solana/pkg/solana/logpoller" +) + +var ZeroAddress = [32]byte{} + +func MakeRandom32ByteArray() [32]byte { + a := make([]byte, 32) + if _, err := rand.Read(a); err != nil { + panic(err) // should never panic but check in case + } + return [32]byte(a) +} + +func Uint64ToLE(chain uint64) []byte { + chainLE := make([]byte, 8) + binary.LittleEndian.PutUint64(chainLE, chain) + return chainLE +} + +func To28BytesLE(value uint64) [28]byte { + le := make([]byte, 28) + binary.LittleEndian.PutUint64(le, value) + return [28]byte(le) +} + +func Map[T, V any](ts []T, fn func(T) V) []V { + result := make([]V, len(ts)) + for i, t := range ts { + result[i] = fn(t) + } + return result +} + +func Discriminator(namespace, name string) logpoller.EventSignature { + h := sha256.New() + h.Write([]byte(fmt.Sprintf("%s:%s", namespace, name))) + return logpoller.EventSignature(h.Sum(nil)[:8]) +} + +func FundAccounts(ctx context.Context, accounts []solana.PrivateKey, solanaGoClient *rpc.Client, t *testing.T) { + sigs := []solana.Signature{} + for _, v := range accounts { + sig, err := solanaGoClient.RequestAirdrop(ctx, v.PublicKey(), 1000*solana.LAMPORTS_PER_SOL, rpc.CommitmentFinalized) + require.NoError(t, err) + sigs = append(sigs, sig) + } + + // wait for confirmation so later transactions don't fail + remaining := len(sigs) + count := 0 + for remaining > 0 { + count++ + statusRes, sigErr := solanaGoClient.GetSignatureStatuses(ctx, true, sigs...) + require.NoError(t, sigErr) + require.NotNil(t, statusRes) + require.NotNil(t, statusRes.Value) + + unconfirmedTxCount := 0 + for _, res := range statusRes.Value { + if res == nil || res.ConfirmationStatus == rpc.ConfirmationStatusProcessed || res.ConfirmationStatus == rpc.ConfirmationStatusConfirmed { + unconfirmedTxCount++ + } + } + remaining = unconfirmedTxCount + + time.Sleep(500 * time.Millisecond) + if count > 60 { + require.NoError(t, fmt.Errorf("unable to find transaction within timeout")) + } + } +} + +func IsEvent(event string, data []byte) bool { + if len(data) < 8 { + return false + } + d := Discriminator("event", event) + return bytes.Equal(d, data[:8]) +} + +func ParseEvent(logs []string, event string, obj interface{}, print ...bool) error { + for _, v := range logs { + if strings.Contains(v, "Program data:") { + encodedData := strings.TrimSpace(strings.TrimPrefix(v, "Program data:")) + data, err := base64.StdEncoding.DecodeString(encodedData) + if err != nil { + return err + } + if IsEvent(event, data) { + if err := bin.UnmarshalBorsh(obj, data); err != nil { + return err + } + + if len(print) > 0 && print[0] { + fmt.Printf("%s: %+v\n", event, obj) + } + return nil + } + } + } + return fmt.Errorf("%s: event not found", event) +} + +func ParseMultipleEvents[T any](logs []string, event string, print bool) ([]T, error) { + var results []T + for _, v := range logs { + if strings.Contains(v, "Program data:") { + encodedData := strings.TrimSpace(strings.TrimPrefix(v, "Program data:")) + data, err := base64.StdEncoding.DecodeString(encodedData) + if err != nil { + return nil, err + } + if IsEvent(event, data) { + var obj T + if err := bin.UnmarshalBorsh(&obj, data); err != nil { + return nil, err + } + + if print { + fmt.Printf("%s: %+v\n", event, obj) + } + + results = append(results, obj) + } + } + } + if len(results) == 0 { + return nil, fmt.Errorf("%s: event not found", event) + } + + return results, nil +} + +type AnchorInstruction struct { + Name string + ProgramID string + Logs []string + ComputeUnits int + InnerCalls []*AnchorInstruction +} + +// Parses the log messages from an Anchor program and returns a list of AnchorInstructions. +func ParseLogMessages(logMessages []string) []*AnchorInstruction { + var instructions []*AnchorInstruction + var stack []*AnchorInstruction + var currentInstruction *AnchorInstruction + + programInvokeRegex := regexp.MustCompile(`Program (\w+) invoke`) + programSuccessRegex := regexp.MustCompile(`Program (\w+) success`) + computeUnitsRegex := regexp.MustCompile(`Program (\w+) consumed (\d+) of \d+ compute units`) + + for _, line := range logMessages { + line = strings.TrimSpace(line) + + // Program invocation - push to stack + if match := programInvokeRegex.FindStringSubmatch(line); len(match) > 1 { + newInstruction := &AnchorInstruction{ + ProgramID: match[1], + Name: "", + Logs: []string{}, + ComputeUnits: 0, + InnerCalls: []*AnchorInstruction{}, + } + + if len(stack) == 0 { + instructions = append(instructions, newInstruction) + } else { + stack[len(stack)-1].InnerCalls = append(stack[len(stack)-1].InnerCalls, newInstruction) + } + + stack = append(stack, newInstruction) + currentInstruction = newInstruction + continue + } + + // Program success - pop from stack + if match := programSuccessRegex.FindStringSubmatch(line); len(match) > 1 { + if len(stack) > 0 { + stack = stack[:len(stack)-1] // pop + if len(stack) > 0 { + currentInstruction = stack[len(stack)-1] + } else { + currentInstruction = nil + } + } + continue + } + + // Instruction name + if strings.Contains(line, "Instruction:") { + if currentInstruction != nil { + currentInstruction.Name = strings.TrimSpace(strings.Split(line, "Instruction:")[1]) + } + continue + } + + // Program logs + if strings.HasPrefix(line, "Program log:") { + if currentInstruction != nil { + logMessage := strings.TrimSpace(strings.TrimPrefix(line, "Program log:")) + currentInstruction.Logs = append(currentInstruction.Logs, logMessage) + } + continue + } + + // Compute units + if match := computeUnitsRegex.FindStringSubmatch(line); len(match) > 1 { + programID := match[1] + computeUnits, _ := strconv.Atoi(match[2]) + + // Find the instruction in the stack that matches this program ID + for i := len(stack) - 1; i >= 0; i-- { + if stack[i].ProgramID == programID { + stack[i].ComputeUnits = computeUnits + break + } + } + } + } + + return instructions +} + +// Pretty prints the given Anchor instructions. +// Example usage: +// parsed := utils.ParseLogMessages(result.Meta.LogMessages) +// output := utils.PrintInstructions(parsed) +// t.Logf("Parsed Instructions: %s", output) +func PrintInstructions(instructions []*AnchorInstruction) string { + var output strings.Builder + + var printInstruction func(*AnchorInstruction, int, string) + printInstruction = func(instruction *AnchorInstruction, index int, indent string) { + output.WriteString(fmt.Sprintf("%sInstruction %d: %s\n", indent, index, instruction.Name)) + output.WriteString(fmt.Sprintf("%s Program ID: %s\n", indent, instruction.ProgramID)) + output.WriteString(fmt.Sprintf("%s Compute Units: %d\n", indent, instruction.ComputeUnits)) + output.WriteString(fmt.Sprintf("%s Logs:\n", indent)) + for _, log := range instruction.Logs { + output.WriteString(fmt.Sprintf("%s %s\n", indent, log)) + } + if len(instruction.InnerCalls) > 0 { + output.WriteString(fmt.Sprintf("%s Inner Calls:\n", indent)) + for i, innerCall := range instruction.InnerCalls { + printInstruction(innerCall, i+1, indent+" ") + } + } + } + + for i, instruction := range instructions { + printInstruction(instruction, i+1, "") + } + + return output.String() +} + +func GetBlockTime(ctx context.Context, client *rpc.Client, commitment rpc.CommitmentType) (*solana.UnixTimeSeconds, error) { + block, err := client.GetBlockHeight(ctx, commitment) + if err != nil { + return nil, fmt.Errorf("failed to get block height: %w", err) + } + + blockTime, err := client.GetBlockTime(ctx, block) + if err != nil { + return nil, fmt.Errorf("failed to get block time: %w", err) + } + + return blockTime, nil +} From f9f6e3f15a7063ceeddf5924d378a77ead7e0cf5 Mon Sep 17 00:00:00 2001 From: Domino Valdano <2644901+reductionista@users.noreply.github.com> Date: Thu, 19 Dec 2024 19:16:57 -0800 Subject: [PATCH 10/43] Use EventTypeProvider interface to represent a codec function for retrieving event types from idl --- pkg/solana/chain.go | 3 +- pkg/solana/logpoller/filters.go | 15 ------ pkg/solana/logpoller/log_poller.go | 62 ++++++++++-------------- pkg/solana/logpoller/models.go | 7 ++- pkg/solana/logpoller/orm.go | 6 ++- pkg/solana/logpoller/query.go | 4 +- pkg/solana/logpoller/types.go | 71 ++++++++++++++++++++-------- pkg/solana/logpoller/utils/anchor.go | 10 ++-- 8 files changed, 92 insertions(+), 86 deletions(-) diff --git a/pkg/solana/chain.go b/pkg/solana/chain.go index a433a298a..e8c321d0d 100644 --- a/pkg/solana/chain.go +++ b/pkg/solana/chain.go @@ -317,7 +317,8 @@ func newChain(id string, cfg *config.TOMLConfig, ks core.Keystore, lggr logger.L //bc = internal.NewLoader[monitor.BalanceClient](func() (monitor.BalanceClient, error) { return ch.multiNode.SelectRPC() }) } - ch.lp = logpoller.NewLogPoller(logger.Sugared(logger.Named(lggr, "LogPoller")), logpoller.NewORM(ch.ID(), ds, lggr), lc) + // TODO: import typeProvider function from codec package and pass to constructor + ch.lp = logpoller.New(logger.Sugared(logger.Named(lggr, "LogPoller")), logpoller.NewORM(ch.ID(), ds, lggr), lc, nil) ch.txm = txm.NewTxm(ch.id, tc, sendTx, cfg, ks, lggr) ch.balanceMonitor = monitor.NewBalanceMonitor(ch.id, cfg, lggr, ks, bc) return &ch, nil diff --git a/pkg/solana/logpoller/filters.go b/pkg/solana/logpoller/filters.go index 14754b406..1458abe39 100644 --- a/pkg/solana/logpoller/filters.go +++ b/pkg/solana/logpoller/filters.go @@ -12,10 +12,7 @@ import ( "github.com/gagliardetto/solana-go" "github.com/smartcontractkit/chainlink-common/pkg/logger" - "github.com/smartcontractkit/chainlink-common/pkg/types" - "github.com/smartcontractkit/chainlink-solana/pkg/solana/codec" - "github.com/smartcontractkit/chainlink-solana/pkg/solana/config" "github.com/smartcontractkit/chainlink-solana/pkg/solana/logpoller/utils" ) @@ -30,7 +27,6 @@ type filters struct { filtersToDelete map[int64]Filter filtersMutex sync.RWMutex loadedFilters atomic.Bool - eventCodecs map[int64]types.RemoteCodec knownPrograms map[string]uint // fast lookup to see if a base58-encoded ProgramID matches any registered filters knownDiscriminators map[string]uint // fast lookup by first 10 characters (60-bits) of a base64-encoded discriminator } @@ -86,11 +82,6 @@ func (fl *filters) RegisterFilter(ctx context.Context, filter Filter) error { return fmt.Errorf("failed to load filters: %w", err) } - eventCodec, err := codec.NewIDLEventCodec(filter.EventIDL, config.BuilderForEncoding(config.EncodingTypeBorsh)) - if err != nil { - return fmt.Errorf("invalid event IDL for filter %s: %w", filter.Name, err) - } - filter.EventSig = utils.Discriminator("event", filter.EventName) fl.filtersMutex.Lock() @@ -136,8 +127,6 @@ func (fl *filters) RegisterFilter(ctx context.Context, filter Filter) error { fl.filtersToBackfill[filter.ID] = struct{}{} } - fl.eventCodecs[filter.ID] = eventCodec - programID := filter.Address.ToSolana().String() if _, ok := fl.knownPrograms[programID]; !ok { fl.knownPrograms[programID] = 1 @@ -264,10 +253,6 @@ func (fl *filters) MatchingFilters(addr PublicKey, eventSignature EventSignature } } -func (fl *filters) EventCodec(ID int64) types.RemoteCodec { - return fl.eventCodecs[ID] -} - // MatchchingFiltersForEncodedEvent - similar to MatchingFilters but accepts a raw encoded event. Under normal operation, // this will be called on every new event that happens on the blockchain, so it's important it returns immediately if it // doesn't match any registered filters. diff --git a/pkg/solana/logpoller/log_poller.go b/pkg/solana/logpoller/log_poller.go index 9c05503d8..fd9c8ab57 100644 --- a/pkg/solana/logpoller/log_poller.go +++ b/pkg/solana/logpoller/log_poller.go @@ -6,12 +6,11 @@ import ( "errors" "fmt" "math" - "reflect" "time" + bin "github.com/gagliardetto/binary" "github.com/smartcontractkit/chainlink-common/pkg/logger" "github.com/smartcontractkit/chainlink-common/pkg/services" - commontypes "github.com/smartcontractkit/chainlink-common/pkg/types" "github.com/smartcontractkit/chainlink-common/pkg/utils" "github.com/smartcontractkit/chainlink-solana/pkg/solana/client" @@ -19,7 +18,8 @@ import ( ) var ( - ErrFilterNameConflict = errors.New("filter with such name already exists") + ErrFilterNameConflict = errors.New("filter with such name already exists") + ErrMissingEventTypeProvider = errors.New("cannot start LogPoller without EventTypeProvider") ) type ORM interface { @@ -43,6 +43,7 @@ type LogPoller struct { services.Service eng *services.Engine services.StateMachine + lggr logger.SugaredLogger orm ORM client internal.Loader[client.Reader] @@ -51,17 +52,16 @@ type LogPoller struct { filters *filters discriminatorLookup map[string]string events []ProgramEvent - codec commontypes.RemoteCodec - - chStop services.StopChan + typeProvider EventTypeProvider } -func New(lggr logger.SugaredLogger, orm ORM, cl internal.Loader[client.Reader]) ILogPoller { +func New(lggr logger.SugaredLogger, orm ORM, cl internal.Loader[client.Reader], typeProvider EventTypeProvider) ILogPoller { lggr = logger.Sugared(logger.Named(lggr, "LogPoller")) lp := &LogPoller{ - orm: orm, - client: cl, - filters: newFilters(lggr, orm), + orm: orm, + client: cl, + filters: newFilters(lggr, orm), + typeProvider: typeProvider, } lp.Service, lp.eng = services.Config{ @@ -74,13 +74,17 @@ func New(lggr logger.SugaredLogger, orm ORM, cl internal.Loader[client.Reader]) } func (lp *LogPoller) start(context.Context) error { - lp.eng.Go(lp.run) - lp.eng.Go(lp.backgroundWorkerRun) + if lp.typeProvider == nil { + return ErrMissingEventTypeProvider + } cl, err := lp.client.Get() if err != nil { return err } lp.collector = NewEncodedLogCollector(cl, lp, lp.lggr) + + lp.eng.Go(lp.run) + lp.eng.Go(lp.backgroundWorkerRun) return nil } @@ -93,7 +97,7 @@ func makeLogIndex(txIndex int, txLogIndex uint) int64 { // Process - process stream of events coming from log ingester func (lp *LogPoller) Process(programEvent ProgramEvent) (err error) { - ctx, cancel := utils.ContextFromChan(lp.chStop) + ctx, cancel := utils.ContextFromChan(lp.eng.StopChan) defer cancel() blockData := programEvent.BlockData @@ -117,15 +121,14 @@ func (lp *LogPoller) Process(programEvent ProgramEvent) (err error) { return err } - var event any - err = lp.filters.EventCodec(filter.ID).Decode(ctx, log.Data, &event, filter.EventName) - if err != nil { - return err - } + for _, path := range filter.SubkeyPaths { - err = lp.ExtractSubkeys(reflect.TypeOf(event), filter.SubkeyPaths) - if err != nil { - return err + var event any + event, err = lp.typeProvider.CreateType(filter.EventIdl.IdlEvent, filter.EventIdl.IdlTypeDefSlice, path) + bin.UnmarshalBorsh(&event, log.Data) + if err != nil { + return err + } } // TODO: fill in, and keep track of SequenceNumber for each filter. (Initialize from db on LoadFilters, then increment each time?) @@ -137,23 +140,6 @@ func (lp *LogPoller) Process(programEvent ProgramEvent) (err error) { return nil } -func (lp *LogPoller) ExtractSubkeys(t reflect.Type, paths SubkeyPaths) error { - s := reflect.TypeOf(event) - if s.Kind() != reflect.Struct { - return fmt.Errorf("event type must be struct, got %v. event=%v", t, event) - } - - for _, path := range paths[0] { - field, err := s.FieldByName(path) - for depth := 0; depth < len(paths); depth++ { - for _, path := range paths[depth] { - field, err = field.Type.FieldByName(path) - } - } - } - -} - // RegisterFilter - refer to filters.RegisterFilter for details. func (lp *LogPoller) RegisterFilter(ctx context.Context, filter Filter) error { ctx, cancel := lp.eng.Ctx(ctx) diff --git a/pkg/solana/logpoller/models.go b/pkg/solana/logpoller/models.go index 3249e86cf..b1a1db3ac 100644 --- a/pkg/solana/logpoller/models.go +++ b/pkg/solana/logpoller/models.go @@ -4,8 +4,6 @@ import ( "time" "github.com/lib/pq" - - "github.com/smartcontractkit/chainlink-solana/pkg/solana/codec" ) type Filter struct { @@ -15,7 +13,7 @@ type Filter struct { EventName string EventSig EventSignature StartingBlock int64 - EventIDL codec.IDL + EventIdl EventIdl SubkeyPaths SubkeyPaths Retention time.Duration MaxLogsKept int64 @@ -24,7 +22,8 @@ type Filter struct { } func (f Filter) MatchSameLogs(other Filter) bool { - return f.Address == other.Address && f.EventSig == other.EventSig && f.EventIDL == other.EventIDL && f.SubkeyPaths.Equal(other.SubkeyPaths) + return f.Address == other.Address && f.EventSig == other.EventSig && + f.EventIdl.Equal(other.EventIdl) && f.SubkeyPaths.Equal(other.SubkeyPaths) } type Log struct { diff --git a/pkg/solana/logpoller/orm.go b/pkg/solana/logpoller/orm.go index 2239ed608..38ba77da2 100644 --- a/pkg/solana/logpoller/orm.go +++ b/pkg/solana/logpoller/orm.go @@ -27,6 +27,10 @@ func NewORM(chainID string, ds sqlutil.DataSource, lggr logger.Logger) *DSORM { } } +func (o *DSORM) ChainID() string { + return o.chainID +} + func (o *DSORM) Transact(ctx context.Context, fn func(*DSORM) error) (err error) { return sqlutil.Transact(ctx, o.new, o.ds, nil, fn) } @@ -48,7 +52,7 @@ func (o *DSORM) InsertFilter(ctx context.Context, filter Filter) (id int64, err withEventName(filter.EventName). withEventSig(filter.EventSig). withStartingBlock(filter.StartingBlock). - withEventIDL(filter.EventIDL). + withEventIDL(filter.EventIdl). withSubkeyPaths(filter.SubkeyPaths). withIsBackfilled(filter.IsBackfilled). toArgs() diff --git a/pkg/solana/logpoller/query.go b/pkg/solana/logpoller/query.go index ba310c6b5..4c7844183 100644 --- a/pkg/solana/logpoller/query.go +++ b/pkg/solana/logpoller/query.go @@ -86,8 +86,8 @@ func (q *queryArgs) withStartingBlock(startingBlock int64) *queryArgs { } // withEventIDL sets the EventIDL field in queryArgs. -func (q *queryArgs) withEventIDL(eventIDL string) *queryArgs { - return q.withField("event_idl", eventIDL) +func (q *queryArgs) withEventIDL(eventIdl EventIdl) *queryArgs { + return q.withField("event_idl", eventIdl) } // withSubkeyPaths sets the SubkeyPaths field in queryArgs. diff --git a/pkg/solana/logpoller/types.go b/pkg/solana/logpoller/types.go index 143c28898..ba3812a3e 100644 --- a/pkg/solana/logpoller/types.go +++ b/pkg/solana/logpoller/types.go @@ -4,9 +4,12 @@ import ( "database/sql/driver" "encoding/json" "fmt" + "reflect" "slices" "github.com/gagliardetto/solana-go" + + "github.com/smartcontractkit/chainlink-solana/pkg/solana/codec" ) type PublicKey solana.PublicKey @@ -76,26 +79,7 @@ func (p SubkeyPaths) Value() (driver.Value, error) { } func (p *SubkeyPaths) Scan(src interface{}) error { - var bSrc []byte - switch src := src.(type) { - case string: - bSrc = []byte(src) - case []byte: - bSrc = src - default: - return fmt.Errorf("can't scan %T into SubkeyPaths", src) - } - - if len(bSrc) == 0 || string(bSrc) == "null" { - return nil - } - - err := json.Unmarshal(bSrc, p) - if err != nil { - return fmt.Errorf("failed to scan %v into SubkeyPaths: %w", string(bSrc), err) - } - - return nil + return scanJson("SubkeyPaths", p, src) } func (p SubkeyPaths) Equal(o SubkeyPaths) bool { @@ -115,3 +99,50 @@ func (s *EventSignature) Scan(src interface{}) error { func (s EventSignature) Value() (driver.Value, error) { return s[:], nil } + +type EventTypeProvider interface { + CreateType(eventIdl codec.IdlEvent, typedefSlice codec.IdlTypeDefSlice, subKeyPath []string) (any, error) +} + +type EventIdl struct { + codec.IdlEvent + codec.IdlTypeDefSlice +} + +func (e *EventIdl) Scan(src interface{}) error { + return scanJson("EventIdl", e, src) +} + +func (e EventIdl) Value() (driver.Value, error) { + return json.Marshal(map[string]any{ + "IdlEvent": e.IdlEvent, + "IdlTypeDefSlice": e.IdlTypeDefSlice, + }) +} + +func (p EventIdl) Equal(o EventIdl) bool { + return reflect.DeepEqual(p, o) +} + +func scanJson(name string, dest, src interface{}) error { + var bSrc []byte + switch src := src.(type) { + case string: + bSrc = []byte(src) + case []byte: + bSrc = src + default: + return fmt.Errorf("can't scan %T into %s", src, name) + } + + if len(bSrc) == 0 || string(bSrc) == "null" { + return nil + } + + err := json.Unmarshal(bSrc, dest) + if err != nil { + return fmt.Errorf("failed to scan %v into %s: %w", string(bSrc), name, err) + } + + return nil +} diff --git a/pkg/solana/logpoller/utils/anchor.go b/pkg/solana/logpoller/utils/anchor.go index 4d11bcc83..b042fb67d 100644 --- a/pkg/solana/logpoller/utils/anchor.go +++ b/pkg/solana/logpoller/utils/anchor.go @@ -19,8 +19,6 @@ import ( "github.com/gagliardetto/solana-go/rpc" "github.com/stretchr/testify/require" - - "github.com/smartcontractkit/chainlink-solana/pkg/solana/logpoller" ) var ZeroAddress = [32]byte{} @@ -53,10 +51,12 @@ func Map[T, V any](ts []T, fn func(T) V) []V { return result } -func Discriminator(namespace, name string) logpoller.EventSignature { +const DiscriminatorLength = 8 + +func Discriminator(namespace, name string) [DiscriminatorLength]byte { h := sha256.New() h.Write([]byte(fmt.Sprintf("%s:%s", namespace, name))) - return logpoller.EventSignature(h.Sum(nil)[:8]) + return [DiscriminatorLength]byte(h.Sum(nil)[:DiscriminatorLength]) } func FundAccounts(ctx context.Context, accounts []solana.PrivateKey, solanaGoClient *rpc.Client, t *testing.T) { @@ -97,7 +97,7 @@ func IsEvent(event string, data []byte) bool { return false } d := Discriminator("event", event) - return bytes.Equal(d, data[:8]) + return bytes.Equal(d[:], data[:8]) } func ParseEvent(logs []string, event string, obj interface{}, print ...bool) error { From 58abe173f565f663c71ede38e6424d6d449512e1 Mon Sep 17 00:00:00 2001 From: Domino Valdano <2644901+reductionista@users.noreply.github.com> Date: Mon, 30 Dec 2024 14:10:53 -0800 Subject: [PATCH 11/43] remove unimplemented test --- pkg/solana/codec/solana_test.go | 7 ------- 1 file changed, 7 deletions(-) diff --git a/pkg/solana/codec/solana_test.go b/pkg/solana/codec/solana_test.go index 57e921fb7..4dd116691 100644 --- a/pkg/solana/codec/solana_test.go +++ b/pkg/solana/codec/solana_test.go @@ -143,13 +143,6 @@ func TestNewIDLCodec_CircularDependency(t *testing.T) { assert.ErrorIs(t, err, types.ErrInvalidConfig) } -func TestNewIDLInstructionCodec(t *testing.T) { - t.Parallel() - - var idl codec.IDL - -} - func newTestIDLAndCodec(t *testing.T, account bool) (string, codec.IDL, types.RemoteCodec) { t.Helper() From d08829eb536b5a9c7741bdf29160ac3d887ab281 Mon Sep 17 00:00:00 2001 From: Domino Valdano <2644901+reductionista@users.noreply.github.com> Date: Mon, 30 Dec 2024 14:15:21 -0800 Subject: [PATCH 12/43] Regenerate mock_orm.go --- pkg/solana/logpoller/mock_orm.go | 92 ++++++++++++++++++++++++++++++++ 1 file changed, 92 insertions(+) diff --git a/pkg/solana/logpoller/mock_orm.go b/pkg/solana/logpoller/mock_orm.go index 0595ea718..25bf0972e 100644 --- a/pkg/solana/logpoller/mock_orm.go +++ b/pkg/solana/logpoller/mock_orm.go @@ -21,6 +21,51 @@ func (_m *mockORM) EXPECT() *mockORM_Expecter { return &mockORM_Expecter{mock: &_m.Mock} } +// ChainID provides a mock function with given fields: +func (_m *mockORM) ChainID() string { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for ChainID") + } + + var r0 string + if rf, ok := ret.Get(0).(func() string); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(string) + } + + return r0 +} + +// mockORM_ChainID_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ChainID' +type mockORM_ChainID_Call struct { + *mock.Call +} + +// ChainID is a helper method to define mock.On call +func (_e *mockORM_Expecter) ChainID() *mockORM_ChainID_Call { + return &mockORM_ChainID_Call{Call: _e.mock.On("ChainID")} +} + +func (_c *mockORM_ChainID_Call) Run(run func()) *mockORM_ChainID_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *mockORM_ChainID_Call) Return(_a0 string) *mockORM_ChainID_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *mockORM_ChainID_Call) RunAndReturn(run func() string) *mockORM_ChainID_Call { + _c.Call.Return(run) + return _c +} + // DeleteFilters provides a mock function with given fields: ctx, filters func (_m *mockORM) DeleteFilters(ctx context.Context, filters map[int64]Filter) error { ret := _m.Called(ctx, filters) @@ -125,6 +170,53 @@ func (_c *mockORM_InsertFilter_Call) RunAndReturn(run func(context.Context, Filt return _c } +// InsertLogs provides a mock function with given fields: _a0, _a1 +func (_m *mockORM) InsertLogs(_a0 context.Context, _a1 []Log) error { + ret := _m.Called(_a0, _a1) + + if len(ret) == 0 { + panic("no return value specified for InsertLogs") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, []Log) error); ok { + r0 = rf(_a0, _a1) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// mockORM_InsertLogs_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'InsertLogs' +type mockORM_InsertLogs_Call struct { + *mock.Call +} + +// InsertLogs is a helper method to define mock.On call +// - _a0 context.Context +// - _a1 []Log +func (_e *mockORM_Expecter) InsertLogs(_a0 interface{}, _a1 interface{}) *mockORM_InsertLogs_Call { + return &mockORM_InsertLogs_Call{Call: _e.mock.On("InsertLogs", _a0, _a1)} +} + +func (_c *mockORM_InsertLogs_Call) Run(run func(_a0 context.Context, _a1 []Log)) *mockORM_InsertLogs_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].([]Log)) + }) + return _c +} + +func (_c *mockORM_InsertLogs_Call) Return(err error) *mockORM_InsertLogs_Call { + _c.Call.Return(err) + return _c +} + +func (_c *mockORM_InsertLogs_Call) RunAndReturn(run func(context.Context, []Log) error) *mockORM_InsertLogs_Call { + _c.Call.Return(run) + return _c +} + // MarkFilterBackfilled provides a mock function with given fields: ctx, id func (_m *mockORM) MarkFilterBackfilled(ctx context.Context, id int64) error { ret := _m.Called(ctx, id) From 3879dbd7715336fe64eb7432f323501ac54def7c Mon Sep 17 00:00:00 2001 From: Domino Valdano <2644901+reductionista@users.noreply.github.com> Date: Mon, 30 Dec 2024 14:18:32 -0800 Subject: [PATCH 13/43] Fix lints --- pkg/solana/logpoller/filters_test.go | 6 ------ pkg/solana/logpoller/log_poller.go | 7 ++----- 2 files changed, 2 insertions(+), 11 deletions(-) diff --git a/pkg/solana/logpoller/filters_test.go b/pkg/solana/logpoller/filters_test.go index 0f39b60b4..710f08a9f 100644 --- a/pkg/solana/logpoller/filters_test.go +++ b/pkg/solana/logpoller/filters_test.go @@ -96,12 +96,6 @@ func TestFilters_RegisterFilter(t *testing.T) { f.EventSig = EventSignature{3, 2, 1} }, }, - { - Name: "EventIDL", - ModifyField: func(f *Filter) { - f.EventIDL = uuid.NewString() - }, - }, { Name: "SubkeyPaths", ModifyField: func(f *Filter) { diff --git a/pkg/solana/logpoller/log_poller.go b/pkg/solana/logpoller/log_poller.go index fd9c8ab57..517922c53 100644 --- a/pkg/solana/logpoller/log_poller.go +++ b/pkg/solana/logpoller/log_poller.go @@ -49,10 +49,8 @@ type LogPoller struct { client internal.Loader[client.Reader] collector *EncodedLogCollector - filters *filters - discriminatorLookup map[string]string - events []ProgramEvent - typeProvider EventTypeProvider + filters *filters + typeProvider EventTypeProvider } func New(lggr logger.SugaredLogger, orm ORM, cl internal.Loader[client.Reader], typeProvider EventTypeProvider) ILogPoller { @@ -122,7 +120,6 @@ func (lp *LogPoller) Process(programEvent ProgramEvent) (err error) { } for _, path := range filter.SubkeyPaths { - var event any event, err = lp.typeProvider.CreateType(filter.EventIdl.IdlEvent, filter.EventIdl.IdlTypeDefSlice, path) bin.UnmarshalBorsh(&event, log.Data) From 624ef9ed2dc5396a7e6a6e0315d0024633e660b9 Mon Sep 17 00:00:00 2001 From: Domino Valdano <2644901+reductionista@users.noreply.github.com> Date: Mon, 30 Dec 2024 21:48:51 -0800 Subject: [PATCH 14/43] Add IndexedValue type for converting indexable types to be stored in db --- pkg/solana/logpoller/log_poller.go | 15 ++++++-- pkg/solana/logpoller/types.go | 57 ++++++++++++++++++++++++++++++ 2 files changed, 69 insertions(+), 3 deletions(-) diff --git a/pkg/solana/logpoller/log_poller.go b/pkg/solana/logpoller/log_poller.go index 517922c53..35429daae 100644 --- a/pkg/solana/logpoller/log_poller.go +++ b/pkg/solana/logpoller/log_poller.go @@ -119,15 +119,24 @@ func (lp *LogPoller) Process(programEvent ProgramEvent) (err error) { return err } + subKeyValues := make([]IndexedValue, 0, len(filter.SubkeyPaths)) for _, path := range filter.SubkeyPaths { - var event any - event, err = lp.typeProvider.CreateType(filter.EventIdl.IdlEvent, filter.EventIdl.IdlTypeDefSlice, path) - bin.UnmarshalBorsh(&event, log.Data) + var subKeyVal any + subKeyVal, err = lp.typeProvider.CreateType(filter.EventIdl.IdlEvent, filter.EventIdl.IdlTypeDefSlice, path) + bin.UnmarshalBorsh(&subKeyVal, log.Data) if err != nil { return err } + indexedVal, err := NewIndexedValue(subKeyVal) + if err != nil { + return err + } + subKeyValues = append(subKeyValues, indexedVal) } + lp.seqNums[filter.ID]++ + log.SequenceNum = lp.seqNums + // TODO: fill in, and keep track of SequenceNumber for each filter. (Initialize from db on LoadFilters, then increment each time?) logs = append(logs, log) diff --git a/pkg/solana/logpoller/types.go b/pkg/solana/logpoller/types.go index ba3812a3e..9ec1a4e01 100644 --- a/pkg/solana/logpoller/types.go +++ b/pkg/solana/logpoller/types.go @@ -2,8 +2,10 @@ package logpoller import ( "database/sql/driver" + "encoding/binary" "encoding/json" "fmt" + "math" "reflect" "slices" @@ -146,3 +148,58 @@ func scanJson(name string, dest, src interface{}) error { return nil } + +// IndexedValue represents a value which can be written to, read from, or compared to an indexed BYTEA +// postgres field. Maps, structs, and slices or arrays (of anything but byte) are not supported. For signed +// or unsigned integer types, strings, or byte arrays, the SQL operators <, =, & > should work in the expected +// way. +type IndexedValue []byte + +func (v *IndexedValue) FromInt64(i int64) { + v.FromUint64(uint64(i + math.MaxInt64)) +} + +func (v *IndexedValue) FromUint64(u uint64) []byte { + var b []byte + binary.BigEndian.PutUint64(b, u) +} + +func (v *IndexedValue) FromFloat64(f float64) { + if f >= 0 { + v.FromUint64(math.Float64bits(f)) + } + v.FromUint64(math.Float64bits(math.Abs(f))) +} + +func NewIndexedValue(typedVal any) (iVal IndexedValue, err error) { + // handle 2 simplest cases first + switch t := typedVal.(type) { + case []byte: + return t, nil + case string: + return []byte(t), nil + } + + // handle numeric types + v := reflect.ValueOf(typedVal) + if v.CanUint() { + iVal.FromUint64(v.Uint()) + return iVal, nil + } + if v.CanInt() { + iVal.FromInt64(v.Int()) + return iVal, nil + } + if v.CanFloat() { + iVal.FromFloat64(v.Float()) + return iVal, nil + } + + // any length array is fine as long as the element type is byte + if t := reflect.TypeOf(typedVal); t.Kind() == reflect.Array { + if t.Elem().Kind() == reflect.Uint8 { + return v.Bytes(), nil + } + } + return nil, fmt.Errorf("can't create indexed value from type %T", typedVal) +} From 2bf665619a8b28c86d959ae3b6a67debd16b266b Mon Sep 17 00:00:00 2001 From: Domino Valdano <2644901+reductionista@users.noreply.github.com> Date: Mon, 30 Dec 2024 21:57:18 -0800 Subject: [PATCH 15/43] Fill in log.ExpiresAt --- pkg/solana/ccip-router-idl.json | 3260 ++++++++++++++++++++++++++++ pkg/solana/logpoller/log_poller.go | 4 + 2 files changed, 3264 insertions(+) create mode 100644 pkg/solana/ccip-router-idl.json diff --git a/pkg/solana/ccip-router-idl.json b/pkg/solana/ccip-router-idl.json new file mode 100644 index 000000000..26436941e --- /dev/null +++ b/pkg/solana/ccip-router-idl.json @@ -0,0 +1,3260 @@ +{ + "version": "0.1.0", + "name": "ccip_router", + "docs": [ + "The `ccip_router` module contains the implementation of the Cross-Chain Interoperability Protocol (CCIP) Router.", + "", + "This is the Collapsed Router Program for CCIP.", + "As it's upgradable persisting the same program id, there is no need to have an indirection of a Proxy Program.", + "This Router handles both the OnRamp and OffRamp flow of the CCIP Messages." + ], + "constants": [ + { + "name": "MAX_ORACLES", + "type": { + "defined": "usize" + }, + "value": "16" + } + ], + "instructions": [ + { + "name": "initialize", + "docs": [ + "Initializes the CCIP Router.", + "", + "The initialization of the Router is responsibility of Admin, nothing more than calling this method should be done first.", + "", + "# Arguments", + "", + "* `ctx` - The context containing the accounts required for initialization.", + "* `solana_chain_selector` - The chain selector for Solana.", + "* `default_gas_limit` - The default gas limit for other destination chains.", + "* `default_allow_out_of_order_execution` - Whether out-of-order execution is allowed by default for other destination chains.", + "* `enable_execution_after` - The minimum amount of time required between a message has been committed and can be manually executed." + ], + "accounts": [ + { + "name": "config", + "isMut": true, + "isSigner": false + }, + { + "name": "state", + "isMut": true, + "isSigner": false + }, + { + "name": "authority", + "isMut": true, + "isSigner": true + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "program", + "isMut": false, + "isSigner": false + }, + { + "name": "programData", + "isMut": false, + "isSigner": false + }, + { + "name": "externalExecutionConfig", + "isMut": true, + "isSigner": false + }, + { + "name": "tokenPoolsSigner", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "solanaChainSelector", + "type": "u64" + }, + { + "name": "defaultGasLimit", + "type": "u128" + }, + { + "name": "defaultAllowOutOfOrderExecution", + "type": "bool" + }, + { + "name": "enableExecutionAfter", + "type": "i64" + } + ] + }, + { + "name": "transferOwnership", + "docs": [ + "Transfers the ownership of the router to a new proposed owner.", + "", + "Shared func signature with other programs", + "", + "# Arguments", + "", + "* `ctx` - The context containing the accounts required for the transfer.", + "* `proposed_owner` - The public key of the new proposed owner." + ], + "accounts": [ + { + "name": "config", + "isMut": true, + "isSigner": false + }, + { + "name": "authority", + "isMut": false, + "isSigner": true + } + ], + "args": [ + { + "name": "proposedOwner", + "type": "publicKey" + } + ] + }, + { + "name": "acceptOwnership", + "docs": [ + "Accepts the ownership of the router by the proposed owner.", + "", + "Shared func signature with other programs", + "", + "# Arguments", + "", + "* `ctx` - The context containing the accounts required for accepting ownership.", + "The new owner must be a signer of the transaction." + ], + "accounts": [ + { + "name": "config", + "isMut": true, + "isSigner": false + }, + { + "name": "authority", + "isMut": false, + "isSigner": true + } + ], + "args": [] + }, + { + "name": "addChainSelector", + "docs": [ + "Adds a new chain selector to the router.", + "", + "The Admin needs to add any new chain supported (this means both OnRamp and OffRamp).", + "When adding a new chain, the Admin needs to specify if it's enabled or not.", + "They may enable only source, or only destination, or neither, or both.", + "", + "# Arguments", + "", + "* `ctx` - The context containing the accounts required for adding the chain selector.", + "* `new_chain_selector` - The new chain selector to be added.", + "* `source_chain_config` - The configuration for the chain as source.", + "* `dest_chain_config` - The configuration for the chain as destination." + ], + "accounts": [ + { + "name": "chainState", + "isMut": true, + "isSigner": false + }, + { + "name": "config", + "isMut": false, + "isSigner": false + }, + { + "name": "authority", + "isMut": true, + "isSigner": true + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "newChainSelector", + "type": "u64" + }, + { + "name": "sourceChainConfig", + "type": { + "defined": "SourceChainConfig" + } + }, + { + "name": "destChainConfig", + "type": { + "defined": "DestChainConfig" + } + } + ] + }, + { + "name": "disableSourceChainSelector", + "docs": [ + "Disables the source chain selector.", + "", + "The Admin is the only one able to disable the chain selector as source. This method is thought of as an emergency kill-switch.", + "", + "# Arguments", + "", + "* `ctx` - The context containing the accounts required for disabling the chain selector.", + "* `source_chain_selector` - The source chain selector to be disabled." + ], + "accounts": [ + { + "name": "chainState", + "isMut": true, + "isSigner": false + }, + { + "name": "config", + "isMut": false, + "isSigner": false + }, + { + "name": "authority", + "isMut": true, + "isSigner": true + } + ], + "args": [ + { + "name": "sourceChainSelector", + "type": "u64" + } + ] + }, + { + "name": "disableDestChainSelector", + "docs": [ + "Disables the destination chain selector.", + "", + "The Admin is the only one able to disable the chain selector as destination. This method is thought of as an emergency kill-switch.", + "", + "# Arguments", + "", + "* `ctx` - The context containing the accounts required for disabling the chain selector.", + "* `dest_chain_selector` - The destination chain selector to be disabled." + ], + "accounts": [ + { + "name": "chainState", + "isMut": true, + "isSigner": false + }, + { + "name": "config", + "isMut": false, + "isSigner": false + }, + { + "name": "authority", + "isMut": true, + "isSigner": true + } + ], + "args": [ + { + "name": "destChainSelector", + "type": "u64" + } + ] + }, + { + "name": "updateSourceChainConfig", + "docs": [ + "Updates the configuration of the source chain selector.", + "", + "The Admin is the only one able to update the source chain config.", + "", + "# Arguments", + "", + "* `ctx` - The context containing the accounts required for updating the chain selector.", + "* `source_chain_selector` - The source chain selector to be updated.", + "* `source_chain_config` - The new configuration for the source chain." + ], + "accounts": [ + { + "name": "chainState", + "isMut": true, + "isSigner": false + }, + { + "name": "config", + "isMut": false, + "isSigner": false + }, + { + "name": "authority", + "isMut": true, + "isSigner": true + } + ], + "args": [ + { + "name": "sourceChainSelector", + "type": "u64" + }, + { + "name": "sourceChainConfig", + "type": { + "defined": "SourceChainConfig" + } + } + ] + }, + { + "name": "updateDestChainConfig", + "docs": [ + "Updates the configuration of the destination chain selector.", + "", + "The Admin is the only one able to update the destination chain config.", + "", + "# Arguments", + "", + "* `ctx` - The context containing the accounts required for updating the chain selector.", + "* `dest_chain_selector` - The destination chain selector to be updated.", + "* `dest_chain_config` - The new configuration for the destination chain." + ], + "accounts": [ + { + "name": "chainState", + "isMut": true, + "isSigner": false + }, + { + "name": "config", + "isMut": false, + "isSigner": false + }, + { + "name": "authority", + "isMut": true, + "isSigner": true + } + ], + "args": [ + { + "name": "destChainSelector", + "type": "u64" + }, + { + "name": "destChainConfig", + "type": { + "defined": "DestChainConfig" + } + } + ] + }, + { + "name": "updateSolanaChainSelector", + "docs": [ + "Updates the Solana chain selector in the router configuration.", + "", + "This method should only be used if there was an error with the initial configuration or if the solana chain selector changes.", + "", + "# Arguments", + "", + "* `ctx` - The context containing the accounts required for updating the configuration.", + "* `new_chain_selector` - The new chain selector for Solana." + ], + "accounts": [ + { + "name": "config", + "isMut": true, + "isSigner": false + }, + { + "name": "authority", + "isMut": false, + "isSigner": true + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "newChainSelector", + "type": "u64" + } + ] + }, + { + "name": "updateDefaultGasLimit", + "docs": [ + "Updates the default gas limit in the router configuration.", + "", + "This change affects the default value for gas limit on every other destination chain.", + "The Admin is the only one able to update the default gas limit.", + "", + "# Arguments", + "", + "* `ctx` - The context containing the accounts required for updating the configuration.", + "* `new_gas_limit` - The new default gas limit." + ], + "accounts": [ + { + "name": "config", + "isMut": true, + "isSigner": false + }, + { + "name": "authority", + "isMut": false, + "isSigner": true + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "newGasLimit", + "type": "u128" + } + ] + }, + { + "name": "updateDefaultAllowOutOfOrderExecution", + "docs": [ + "Updates the default setting for allowing out-of-order execution for other destination chains.", + "The Admin is the only one able to update this config.", + "", + "# Arguments", + "", + "* `ctx` - The context containing the accounts required for updating the configuration.", + "* `new_allow_out_of_order_execution` - The new setting for allowing out-of-order execution." + ], + "accounts": [ + { + "name": "config", + "isMut": true, + "isSigner": false + }, + { + "name": "authority", + "isMut": false, + "isSigner": true + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "newAllowOutOfOrderExecution", + "type": "bool" + } + ] + }, + { + "name": "updateEnableManualExecutionAfter", + "docs": [ + "Updates the minimum amount of time required between a message being committed and when it can be manually executed.", + "", + "This is part of the OffRamp Configuration for Solana.", + "The Admin is the only one able to update this config.", + "", + "# Arguments", + "", + "* `ctx` - The context containing the accounts required for updating the configuration.", + "* `new_enable_manual_execution_after` - The new minimum amount of time required." + ], + "accounts": [ + { + "name": "config", + "isMut": true, + "isSigner": false + }, + { + "name": "authority", + "isMut": false, + "isSigner": true + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "newEnableManualExecutionAfter", + "type": "i64" + } + ] + }, + { + "name": "registerTokenAdminRegistryViaGetCcipAdmin", + "docs": [ + "Registers the Token Admin Registry via the CCIP Admin", + "", + "# Arguments", + "", + "* `ctx` - The context containing the accounts required for registration.", + "* `mint` - The public key of the token mint.", + "* `token_admin_registry_admin` - The public key of the token admin registry admin." + ], + "accounts": [ + { + "name": "config", + "isMut": false, + "isSigner": false + }, + { + "name": "tokenAdminRegistry", + "isMut": true, + "isSigner": false + }, + { + "name": "authority", + "isMut": true, + "isSigner": true + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "mint", + "type": "publicKey" + }, + { + "name": "tokenAdminRegistryAdmin", + "type": "publicKey" + } + ] + }, + { + "name": "registerTokenAdminRegistryViaOwner", + "docs": [ + "Registers the Token Admin Registry via the token owner.", + "", + "The Authority of the Mint Token can claim the registry of the token.", + "", + "# Arguments", + "", + "* `ctx` - The context containing the accounts required for registration." + ], + "accounts": [ + { + "name": "tokenAdminRegistry", + "isMut": true, + "isSigner": false + }, + { + "name": "mint", + "isMut": true, + "isSigner": false + }, + { + "name": "authority", + "isMut": true, + "isSigner": true + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [] + }, + { + "name": "setPool", + "docs": [ + "Sets the pool lookup table for a given token mint.", + "", + "The administrator of the token admin registry can set the pool lookup table for a given token mint.", + "", + "# Arguments", + "", + "* `ctx` - The context containing the accounts required for setting the pool.", + "* `mint` - The public key of the token mint.", + "* `pool_lookup_table` - The public key of the pool lookup table, this address will be used for validations when interacting with the pool." + ], + "accounts": [ + { + "name": "tokenAdminRegistry", + "isMut": true, + "isSigner": false + }, + { + "name": "authority", + "isMut": true, + "isSigner": true + } + ], + "args": [ + { + "name": "mint", + "type": "publicKey" + }, + { + "name": "poolLookupTable", + "type": "publicKey" + } + ] + }, + { + "name": "transferAdminRoleTokenAdminRegistry", + "docs": [ + "Transfers the admin role of the token admin registry to a new admin.", + "", + "Only the Admin can transfer the Admin Role of the Token Admin Registry, this setups the Pending Admin and then it's their responsibility to accept the role.", + "", + "# Arguments", + "", + "* `ctx` - The context containing the accounts required for the transfer.", + "* `mint` - The public key of the token mint.", + "* `new_admin` - The public key of the new admin." + ], + "accounts": [ + { + "name": "tokenAdminRegistry", + "isMut": true, + "isSigner": false + }, + { + "name": "authority", + "isMut": true, + "isSigner": true + } + ], + "args": [ + { + "name": "mint", + "type": "publicKey" + }, + { + "name": "newAdmin", + "type": "publicKey" + } + ] + }, + { + "name": "acceptAdminRoleTokenAdminRegistry", + "docs": [ + "Accepts the admin role of the token admin registry.", + "", + "The Pending Admin must call this function to accept the admin role of the Token Admin Registry.", + "", + "# Arguments", + "", + "* `ctx` - The context containing the accounts required for accepting the admin role.", + "* `mint` - The public key of the token mint." + ], + "accounts": [ + { + "name": "tokenAdminRegistry", + "isMut": true, + "isSigner": false + }, + { + "name": "authority", + "isMut": true, + "isSigner": true + } + ], + "args": [ + { + "name": "mint", + "type": "publicKey" + } + ] + }, + { + "name": "setTokenBilling", + "docs": [ + "Sets the token billing configuration.", + "", + "Only CCIP Admin can set the token billing configuration.", + "", + "# Arguments", + "", + "* `ctx` - The context containing the accounts required for setting the token billing configuration.", + "* `_chain_selector` - The chain selector.", + "* `_mint` - The public key of the token mint.", + "* `cfg` - The token billing configuration." + ], + "accounts": [ + { + "name": "config", + "isMut": false, + "isSigner": false + }, + { + "name": "perChainPerTokenConfig", + "isMut": true, + "isSigner": false + }, + { + "name": "authority", + "isMut": true, + "isSigner": true + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "chainSelector", + "type": "u64" + }, + { + "name": "mint", + "type": "publicKey" + }, + { + "name": "cfg", + "type": { + "defined": "TokenBilling" + } + } + ] + }, + { + "name": "setOcrConfig", + "docs": [ + "Sets the OCR configuration.", + "Only CCIP Admin can set the OCR configuration.", + "", + "# Arguments", + "", + "* `ctx` - The context containing the accounts required for setting the OCR configuration.", + "* `plugin_type` - The type of OCR plugin [0: Commit, 1: Execution].", + "* `config_info` - The OCR configuration information.", + "* `signers` - The list of signers.", + "* `transmitters` - The list of transmitters." + ], + "accounts": [ + { + "name": "config", + "isMut": true, + "isSigner": false + }, + { + "name": "state", + "isMut": true, + "isSigner": false + }, + { + "name": "authority", + "isMut": false, + "isSigner": true + } + ], + "args": [ + { + "name": "pluginType", + "type": "u8" + }, + { + "name": "configInfo", + "type": { + "defined": "Ocr3ConfigInfo" + } + }, + { + "name": "signers", + "type": { + "vec": { + "array": [ + "u8", + 20 + ] + } + } + }, + { + "name": "transmitters", + "type": { + "vec": "publicKey" + } + } + ] + }, + { + "name": "addBillingTokenConfig", + "docs": [ + "Adds a billing token configuration.", + "Only CCIP Admin can add a billing token configuration.", + "", + "# Arguments", + "", + "* `ctx` - The context containing the accounts required for adding the billing token configuration.", + "* `config` - The billing token configuration to be added." + ], + "accounts": [ + { + "name": "config", + "isMut": false, + "isSigner": false + }, + { + "name": "billingTokenConfig", + "isMut": true, + "isSigner": false + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false, + "docs": [ + "type of a specific program (which would enforce its ID). Thus, it's an UncheckedAccount", + "with a constraint enforcing that it is one of the two allowed programs." + ] + }, + { + "name": "feeTokenMint", + "isMut": false, + "isSigner": false + }, + { + "name": "feeTokenReceiver", + "isMut": true, + "isSigner": false + }, + { + "name": "authority", + "isMut": true, + "isSigner": true + }, + { + "name": "feeBillingSigner", + "isMut": false, + "isSigner": false + }, + { + "name": "associatedTokenProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "config", + "type": { + "defined": "BillingTokenConfig" + } + } + ] + }, + { + "name": "updateBillingTokenConfig", + "docs": [ + "Updates the billing token configuration.", + "Only CCIP Admin can update a billing token configuration.", + "", + "# Arguments", + "", + "* `ctx` - The context containing the accounts required for updating the billing token configuration.", + "* `config` - The new billing token configuration." + ], + "accounts": [ + { + "name": "config", + "isMut": false, + "isSigner": false + }, + { + "name": "billingTokenConfig", + "isMut": true, + "isSigner": false + }, + { + "name": "authority", + "isMut": false, + "isSigner": true + } + ], + "args": [ + { + "name": "config", + "type": { + "defined": "BillingTokenConfig" + } + } + ] + }, + { + "name": "removeBillingTokenConfig", + "docs": [ + "Removes the billing token configuration.", + "Only CCIP Admin can remove a billing token configuration.", + "", + "# Arguments", + "", + "* `ctx` - The context containing the accounts required for removing the billing token configuration." + ], + "accounts": [ + { + "name": "config", + "isMut": false, + "isSigner": false + }, + { + "name": "billingTokenConfig", + "isMut": true, + "isSigner": false + }, + { + "name": "tokenProgram", + "isMut": false, + "isSigner": false, + "docs": [ + "type of a specific program (which would enforce its ID). Thus, it's an UncheckedAccount", + "with a constraint enforcing that it is one of the two allowed programs." + ] + }, + { + "name": "feeTokenMint", + "isMut": false, + "isSigner": false + }, + { + "name": "feeTokenReceiver", + "isMut": true, + "isSigner": false + }, + { + "name": "feeBillingSigner", + "isMut": true, + "isSigner": false + }, + { + "name": "authority", + "isMut": true, + "isSigner": true + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + } + ], + "args": [] + }, + { + "name": "getFee", + "docs": [ + "Calculates the fee for sending a message to the destination chain.", + "", + "# Arguments", + "", + "* `_ctx` - The context containing the accounts required for the fee calculation.", + "* `dest_chain_selector` - The chain selector for the destination chain.", + "* `message` - The message to be sent.", + "", + "# Returns", + "", + "The fee amount in u64." + ], + "accounts": [ + { + "name": "chainState", + "isMut": false, + "isSigner": false + }, + { + "name": "billingTokenConfig", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "destChainSelector", + "type": "u64" + }, + { + "name": "message", + "type": { + "defined": "Solana2AnyMessage" + } + } + ], + "returns": "u64" + }, + { + "name": "ccipSend", + "docs": [ + "ON RAMP FLOW", + "Sends a message to the destination chain.", + "", + "Request a message to be sent to the destination chain.", + "The method name needs to be ccip_send with Anchor encoding.", + "This function is called by the CCIP Sender Contract (or final user) to send a message to the CCIP Router.", + "The message will be sent to the receiver on the destination chain selector.", + "This message emits the event CCIPSendRequested with all the necessary data to be retrieved by the OffChain Code", + "", + "# Arguments", + "", + "* `ctx` - The context containing the accounts required for sending the message.", + "* `dest_chain_selector` - The chain selector for the destination chain.", + "* `message` - The message to be sent. The size limit of data is 256 bytes." + ], + "accounts": [ + { + "name": "config", + "isMut": false, + "isSigner": false + }, + { + "name": "chainState", + "isMut": true, + "isSigner": false + }, + { + "name": "nonce", + "isMut": true, + "isSigner": false + }, + { + "name": "authority", + "isMut": true, + "isSigner": true + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "feeTokenProgram", + "isMut": false, + "isSigner": false, + "docs": [ + "type of a specific program (which would enforce its ID). Thus, it's an UncheckedAccount", + "with a constraint enforcing that it is one of the two allowed programs." + ] + }, + { + "name": "feeTokenMint", + "isMut": false, + "isSigner": false + }, + { + "name": "feeTokenConfig", + "isMut": false, + "isSigner": false + }, + { + "name": "feeTokenUserAssociatedAccount", + "isMut": true, + "isSigner": false + }, + { + "name": "feeTokenReceiver", + "isMut": true, + "isSigner": false + }, + { + "name": "feeBillingSigner", + "isMut": false, + "isSigner": false + }, + { + "name": "tokenPoolsSigner", + "isMut": true, + "isSigner": false + } + ], + "args": [ + { + "name": "destChainSelector", + "type": "u64" + }, + { + "name": "message", + "type": { + "defined": "Solana2AnyMessage" + } + } + ] + }, + { + "name": "commit", + "docs": [ + "OFF RAMP FLOW", + "Commits a report to the router.", + "", + "The method name needs to be commit with Anchor encoding.", + "", + "This function is called by the OffChain when committing one Report to the Solana Router.", + "In this Flow only one report is sent, the Commit Report. This is different as EVM does,", + "this is because here all the chain state is stored in one account per Merkle Tree Root.", + "So, to avoid having to send a dynamic size array of accounts, in this message only one Commit Report Account is sent.", + "This message validates the signatures of the report and stores the Merkle Root in the Commit Report Account.", + "The Report must contain an interval of messages, and the min of them must be the next sequence number expected.", + "The max size of the interval is 64.", + "This message emits two events: CommitReportAccepted and Transmitted.", + "", + "# Arguments", + "", + "* `ctx` - The context containing the accounts required for the commit.", + "* `report_context_byte_words` - consists of:", + "* report_context_byte_words[0]: ConfigDigest", + "* report_context_byte_words[1]: 24 byte padding, 8 byte sequence number", + "* report_context_byte_words[2]: ExtraHash", + "* `report` - The commit input report, single merkle root with RMN signatures and price updates", + "* `signatures` - The list of signatures. v0.29.0 - anchor idl does not build with ocr3base::SIGNATURE_LENGTH" + ], + "accounts": [ + { + "name": "config", + "isMut": false, + "isSigner": false + }, + { + "name": "chainState", + "isMut": true, + "isSigner": false + }, + { + "name": "commitReport", + "isMut": true, + "isSigner": false + }, + { + "name": "authority", + "isMut": true, + "isSigner": true + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "sysvarInstructions", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "reportContextByteWords", + "type": { + "array": [ + { + "array": [ + "u8", + 32 + ] + }, + 3 + ] + } + }, + { + "name": "report", + "type": { + "defined": "CommitInput" + } + }, + { + "name": "signatures", + "type": { + "vec": { + "array": [ + "u8", + 65 + ] + } + } + } + ] + }, + { + "name": "execute", + "docs": [ + "OFF RAMP FLOW", + "Executes a message on the destination chain.", + "", + "The method name needs to be execute with Anchor encoding.", + "", + "This function is called by the OffChain when executing one Report to the Solana Router.", + "In this Flow only one message is sent, the Execution Report. This is different as EVM does,", + "this is because there is no try/catch mechanism to allow batch execution.", + "This message validates that the Merkle Tree Proof of the given message is correct and is stored in the Commit Report Account.", + "The message must be untouched to be executed.", + "This message emits the event ExecutionStateChanged with the new state of the message.", + "Finally, executes the CPI instruction to the receiver program in the ccip_receive message.", + "", + "# Arguments", + "", + "* `ctx` - The context containing the accounts required for the execute.", + "* `execution_report` - the execution report containing only one message and proofs", + "* `report_context_byte_words` - report_context after execution_report to match context for manually execute (proper decoding order)", + "* consists of:", + "* report_context_byte_words[0]: ConfigDigest", + "* report_context_byte_words[1]: 24 byte padding, 8 byte sequence number", + "* report_context_byte_words[2]: ExtraHash" + ], + "accounts": [ + { + "name": "config", + "isMut": false, + "isSigner": false + }, + { + "name": "chainState", + "isMut": false, + "isSigner": false + }, + { + "name": "commitReport", + "isMut": true, + "isSigner": false + }, + { + "name": "externalExecutionConfig", + "isMut": false, + "isSigner": false + }, + { + "name": "authority", + "isMut": true, + "isSigner": true + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "sysvarInstructions", + "isMut": false, + "isSigner": false + }, + { + "name": "tokenPoolsSigner", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "executionReport", + "type": { + "defined": "ExecutionReportSingleChain" + } + }, + { + "name": "reportContextByteWords", + "type": { + "array": [ + { + "array": [ + "u8", + 32 + ] + }, + 3 + ] + } + } + ] + }, + { + "name": "manuallyExecute", + "docs": [ + "Manually executes a report to the router.", + "", + "When a message is not being executed, then the user can trigger the execution manually.", + "No verification over the transmitter, but the message needs to be in some commit report.", + "It validates that the required time has passed since the commit and then executes the report.", + "", + "# Arguments", + "", + "* `ctx` - The context containing the accounts required for the execution.", + "* `execution_report` - The execution report containing the message and proofs." + ], + "accounts": [ + { + "name": "config", + "isMut": false, + "isSigner": false + }, + { + "name": "chainState", + "isMut": false, + "isSigner": false + }, + { + "name": "commitReport", + "isMut": true, + "isSigner": false + }, + { + "name": "externalExecutionConfig", + "isMut": false, + "isSigner": false + }, + { + "name": "authority", + "isMut": true, + "isSigner": true + }, + { + "name": "systemProgram", + "isMut": false, + "isSigner": false + }, + { + "name": "sysvarInstructions", + "isMut": false, + "isSigner": false + }, + { + "name": "tokenPoolsSigner", + "isMut": false, + "isSigner": false + } + ], + "args": [ + { + "name": "executionReport", + "type": { + "defined": "ExecutionReportSingleChain" + } + } + ] + } + ], + "accounts": [ + { + "name": "Config", + "type": { + "kind": "struct", + "fields": [ + { + "name": "version", + "type": "u8" + }, + { + "name": "defaultAllowOutOfOrderExecution", + "type": "u8" + }, + { + "name": "padding0", + "type": { + "array": [ + "u8", + 6 + ] + } + }, + { + "name": "solanaChainSelector", + "type": "u64" + }, + { + "name": "defaultGasLimit", + "type": "u128" + }, + { + "name": "padding1", + "type": { + "array": [ + "u8", + 8 + ] + } + }, + { + "name": "owner", + "type": "publicKey" + }, + { + "name": "proposedOwner", + "type": "publicKey" + }, + { + "name": "enableManualExecutionAfter", + "type": "i64" + }, + { + "name": "padding2", + "type": { + "array": [ + "u8", + 8 + ] + } + }, + { + "name": "ocr3", + "type": { + "array": [ + { + "defined": "Ocr3Config" + }, + 2 + ] + } + } + ] + } + }, + { + "name": "GlobalState", + "type": { + "kind": "struct", + "fields": [ + { + "name": "latestPriceSequenceNumber", + "type": "u64" + } + ] + } + }, + { + "name": "ChainState", + "type": { + "kind": "struct", + "fields": [ + { + "name": "version", + "type": "u8" + }, + { + "name": "sourceChain", + "type": { + "defined": "SourceChain" + } + }, + { + "name": "destChain", + "type": { + "defined": "DestChain" + } + } + ] + } + }, + { + "name": "Nonce", + "type": { + "kind": "struct", + "fields": [ + { + "name": "version", + "type": "u8" + }, + { + "name": "counter", + "type": "u64" + } + ] + } + }, + { + "name": "ExternalExecutionConfig", + "type": { + "kind": "struct", + "fields": [] + } + }, + { + "name": "CommitReport", + "type": { + "kind": "struct", + "fields": [ + { + "name": "version", + "type": "u8" + }, + { + "name": "timestamp", + "type": "i64" + }, + { + "name": "minMsgNr", + "type": "u64" + }, + { + "name": "maxMsgNr", + "type": "u64" + }, + { + "name": "executionStates", + "type": "u128" + } + ] + } + }, + { + "name": "PerChainPerTokenConfig", + "type": { + "kind": "struct", + "fields": [ + { + "name": "version", + "type": "u8" + }, + { + "name": "chainSelector", + "type": "u64" + }, + { + "name": "mint", + "type": "publicKey" + }, + { + "name": "billing", + "type": { + "defined": "TokenBilling" + } + } + ] + } + }, + { + "name": "BillingTokenConfigWrapper", + "type": { + "kind": "struct", + "fields": [ + { + "name": "version", + "type": "u8" + }, + { + "name": "config", + "type": { + "defined": "BillingTokenConfig" + } + } + ] + } + }, + { + "name": "TokenAdminRegistry", + "type": { + "kind": "struct", + "fields": [ + { + "name": "version", + "type": "u8" + }, + { + "name": "administrator", + "type": "publicKey" + }, + { + "name": "pendingAdministrator", + "type": "publicKey" + }, + { + "name": "lookupTable", + "type": "publicKey" + } + ] + } + } + ], + "types": [ + { + "name": "CommitInput", + "type": { + "kind": "struct", + "fields": [ + { + "name": "priceUpdates", + "type": { + "defined": "PriceUpdates" + } + }, + { + "name": "merkleRoot", + "type": { + "defined": "MerkleRoot" + } + } + ] + } + }, + { + "name": "PriceUpdates", + "type": { + "kind": "struct", + "fields": [ + { + "name": "tokenPriceUpdates", + "type": { + "vec": { + "defined": "TokenPriceUpdate" + } + } + }, + { + "name": "gasPriceUpdates", + "type": { + "vec": { + "defined": "GasPriceUpdate" + } + } + } + ] + } + }, + { + "name": "TokenPriceUpdate", + "type": { + "kind": "struct", + "fields": [ + { + "name": "sourceToken", + "type": "publicKey" + }, + { + "name": "usdPerToken", + "type": { + "array": [ + "u8", + 28 + ] + } + } + ] + } + }, + { + "name": "GasPriceUpdate", + "type": { + "kind": "struct", + "fields": [ + { + "name": "destChainSelector", + "type": "u64" + }, + { + "name": "usdPerUnitGas", + "type": { + "array": [ + "u8", + 28 + ] + } + } + ] + } + }, + { + "name": "MerkleRoot", + "type": { + "kind": "struct", + "fields": [ + { + "name": "sourceChainSelector", + "type": "u64" + }, + { + "name": "onRampAddress", + "type": "bytes" + }, + { + "name": "minSeqNr", + "type": "u64" + }, + { + "name": "maxSeqNr", + "type": "u64" + }, + { + "name": "merkleRoot", + "type": { + "array": [ + "u8", + 32 + ] + } + } + ] + } + }, + { + "name": "RampMessageHeader", + "type": { + "kind": "struct", + "fields": [ + { + "name": "messageId", + "type": { + "array": [ + "u8", + 32 + ] + } + }, + { + "name": "sourceChainSelector", + "type": "u64" + }, + { + "name": "destChainSelector", + "type": "u64" + }, + { + "name": "sequenceNumber", + "type": "u64" + }, + { + "name": "nonce", + "type": "u64" + } + ] + } + }, + { + "name": "ExecutionReportSingleChain", + "docs": [ + "Report that is submitted by the execution DON at the execution phase. (including chain selector data)" + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "sourceChainSelector", + "type": "u64" + }, + { + "name": "message", + "type": { + "defined": "Any2SolanaRampMessage" + } + }, + { + "name": "offchainTokenData", + "type": { + "vec": "bytes" + } + }, + { + "name": "root", + "type": { + "array": [ + "u8", + 32 + ] + } + }, + { + "name": "proofs", + "type": { + "vec": { + "array": [ + "u8", + 32 + ] + } + } + }, + { + "name": "tokenIndexes", + "type": "bytes" + } + ] + } + }, + { + "name": "SolanaAccountMeta", + "type": { + "kind": "struct", + "fields": [ + { + "name": "pubkey", + "type": "publicKey" + }, + { + "name": "isWritable", + "type": "bool" + } + ] + } + }, + { + "name": "SolanaExtraArgs", + "type": { + "kind": "struct", + "fields": [ + { + "name": "computeUnits", + "type": "u32" + }, + { + "name": "accounts", + "type": { + "vec": { + "defined": "SolanaAccountMeta" + } + } + } + ] + } + }, + { + "name": "EvmExtraArgs", + "type": { + "kind": "struct", + "fields": [ + { + "name": "gasLimit", + "type": "u128" + }, + { + "name": "allowOutOfOrderExecution", + "type": "bool" + } + ] + } + }, + { + "name": "Any2SolanaRampMessage", + "type": { + "kind": "struct", + "fields": [ + { + "name": "header", + "type": { + "defined": "RampMessageHeader" + } + }, + { + "name": "sender", + "type": "bytes" + }, + { + "name": "data", + "type": "bytes" + }, + { + "name": "receiver", + "type": "publicKey" + }, + { + "name": "tokenAmounts", + "type": { + "vec": { + "defined": "Any2SolanaTokenTransfer" + } + } + }, + { + "name": "extraArgs", + "type": { + "defined": "SolanaExtraArgs" + } + } + ] + } + }, + { + "name": "Solana2AnyRampMessage", + "type": { + "kind": "struct", + "fields": [ + { + "name": "header", + "type": { + "defined": "RampMessageHeader" + } + }, + { + "name": "sender", + "type": "publicKey" + }, + { + "name": "data", + "type": "bytes" + }, + { + "name": "receiver", + "type": "bytes" + }, + { + "name": "extraArgs", + "type": { + "defined": "EvmExtraArgs" + } + }, + { + "name": "feeToken", + "type": "publicKey" + }, + { + "name": "tokenAmounts", + "type": { + "vec": { + "defined": "Solana2AnyTokenTransfer" + } + } + } + ] + } + }, + { + "name": "Solana2AnyTokenTransfer", + "type": { + "kind": "struct", + "fields": [ + { + "name": "sourcePoolAddress", + "type": "publicKey" + }, + { + "name": "destTokenAddress", + "type": "bytes" + }, + { + "name": "extraData", + "type": "bytes" + }, + { + "name": "amount", + "type": { + "array": [ + "u8", + 32 + ] + } + }, + { + "name": "destExecData", + "type": "bytes" + } + ] + } + }, + { + "name": "Any2SolanaTokenTransfer", + "type": { + "kind": "struct", + "fields": [ + { + "name": "sourcePoolAddress", + "type": "bytes" + }, + { + "name": "destTokenAddress", + "type": "publicKey" + }, + { + "name": "destGasAmount", + "type": "u32" + }, + { + "name": "extraData", + "type": "bytes" + }, + { + "name": "amount", + "type": { + "array": [ + "u8", + 32 + ] + } + } + ] + } + }, + { + "name": "LockOrBurnInV1", + "type": { + "kind": "struct", + "fields": [ + { + "name": "receiver", + "type": "bytes" + }, + { + "name": "remoteChainSelector", + "type": "u64" + }, + { + "name": "originalSender", + "type": "publicKey" + }, + { + "name": "amount", + "type": "u64" + }, + { + "name": "localToken", + "type": "publicKey" + } + ] + } + }, + { + "name": "ReleaseOrMintInV1", + "type": { + "kind": "struct", + "fields": [ + { + "name": "originalSender", + "type": "bytes" + }, + { + "name": "remoteChainSelector", + "type": "u64" + }, + { + "name": "receiver", + "type": "publicKey" + }, + { + "name": "amount", + "type": { + "array": [ + "u8", + 32 + ] + } + }, + { + "name": "localToken", + "type": "publicKey" + }, + { + "name": "sourcePoolAddress", + "docs": [ + "@dev WARNING: sourcePoolAddress should be checked prior to any processing of funds. Make sure it matches the", + "expected pool address for the given remoteChainSelector." + ], + "type": "bytes" + }, + { + "name": "sourcePoolData", + "type": "bytes" + }, + { + "name": "offchainTokenData", + "docs": [ + "@dev WARNING: offchainTokenData is untrusted data." + ], + "type": "bytes" + } + ] + } + }, + { + "name": "LockOrBurnOutV1", + "type": { + "kind": "struct", + "fields": [ + { + "name": "destTokenAddress", + "type": "bytes" + }, + { + "name": "destPoolData", + "type": "bytes" + } + ] + } + }, + { + "name": "ReleaseOrMintOutV1", + "type": { + "kind": "struct", + "fields": [ + { + "name": "destinationAmount", + "type": "u64" + } + ] + } + }, + { + "name": "Solana2AnyMessage", + "type": { + "kind": "struct", + "fields": [ + { + "name": "receiver", + "type": "bytes" + }, + { + "name": "data", + "type": "bytes" + }, + { + "name": "tokenAmounts", + "type": { + "vec": { + "defined": "SolanaTokenAmount" + } + } + }, + { + "name": "feeToken", + "type": "publicKey" + }, + { + "name": "extraArgs", + "type": { + "defined": "ExtraArgsInput" + } + }, + { + "name": "tokenIndexes", + "type": "bytes" + } + ] + } + }, + { + "name": "SolanaTokenAmount", + "type": { + "kind": "struct", + "fields": [ + { + "name": "token", + "type": "publicKey" + }, + { + "name": "amount", + "type": "u64" + } + ] + } + }, + { + "name": "ExtraArgsInput", + "type": { + "kind": "struct", + "fields": [ + { + "name": "gasLimit", + "type": { + "option": "u128" + } + }, + { + "name": "allowOutOfOrderExecution", + "type": { + "option": "bool" + } + } + ] + } + }, + { + "name": "Any2SolanaMessage", + "type": { + "kind": "struct", + "fields": [ + { + "name": "messageId", + "type": { + "array": [ + "u8", + 32 + ] + } + }, + { + "name": "sourceChainSelector", + "type": "u64" + }, + { + "name": "sender", + "type": "bytes" + }, + { + "name": "data", + "type": "bytes" + }, + { + "name": "tokenAmounts", + "type": { + "vec": { + "defined": "SolanaTokenAmount" + } + } + } + ] + } + }, + { + "name": "ReportContext", + "type": { + "kind": "struct", + "fields": [ + { + "name": "byteWords", + "type": { + "array": [ + { + "array": [ + "u8", + 32 + ] + }, + 3 + ] + } + } + ] + } + }, + { + "name": "Ocr3Config", + "type": { + "kind": "struct", + "fields": [ + { + "name": "pluginType", + "type": "u8" + }, + { + "name": "configInfo", + "type": { + "defined": "Ocr3ConfigInfo" + } + }, + { + "name": "signers", + "type": { + "array": [ + { + "array": [ + "u8", + 20 + ] + }, + 16 + ] + } + }, + { + "name": "transmitters", + "type": { + "array": [ + { + "array": [ + "u8", + 32 + ] + }, + 16 + ] + } + } + ] + } + }, + { + "name": "Ocr3ConfigInfo", + "type": { + "kind": "struct", + "fields": [ + { + "name": "configDigest", + "type": { + "array": [ + "u8", + 32 + ] + } + }, + { + "name": "f", + "type": "u8" + }, + { + "name": "n", + "type": "u8" + }, + { + "name": "isSignatureVerificationEnabled", + "type": "u8" + } + ] + } + }, + { + "name": "SourceChainConfig", + "type": { + "kind": "struct", + "fields": [ + { + "name": "isEnabled", + "type": "bool" + }, + { + "name": "onRamp", + "type": "bytes" + } + ] + } + }, + { + "name": "SourceChainState", + "type": { + "kind": "struct", + "fields": [ + { + "name": "minSeqNr", + "type": "u64" + } + ] + } + }, + { + "name": "SourceChain", + "type": { + "kind": "struct", + "fields": [ + { + "name": "state", + "type": { + "defined": "SourceChainState" + } + }, + { + "name": "config", + "type": { + "defined": "SourceChainConfig" + } + } + ] + } + }, + { + "name": "DestChainState", + "type": { + "kind": "struct", + "fields": [ + { + "name": "sequenceNumber", + "type": "u64" + }, + { + "name": "usdPerUnitGas", + "type": { + "defined": "TimestampedPackedU224" + } + } + ] + } + }, + { + "name": "DestChainConfig", + "type": { + "kind": "struct", + "fields": [ + { + "name": "isEnabled", + "type": "bool" + }, + { + "name": "maxNumberOfTokensPerMsg", + "type": "u16" + }, + { + "name": "maxDataBytes", + "type": "u32" + }, + { + "name": "maxPerMsgGasLimit", + "type": "u32" + }, + { + "name": "destGasOverhead", + "type": "u32" + }, + { + "name": "destGasPerPayloadByte", + "type": "u16" + }, + { + "name": "destDataAvailabilityOverheadGas", + "type": "u32" + }, + { + "name": "destGasPerDataAvailabilityByte", + "type": "u16" + }, + { + "name": "destDataAvailabilityMultiplierBps", + "type": "u16" + }, + { + "name": "defaultTokenFeeUsdcents", + "type": "u16" + }, + { + "name": "defaultTokenDestGasOverhead", + "type": "u32" + }, + { + "name": "defaultTxGasLimit", + "type": "u32" + }, + { + "name": "gasMultiplierWeiPerEth", + "type": "u64" + }, + { + "name": "networkFeeUsdcents", + "type": "u32" + }, + { + "name": "gasPriceStalenessThreshold", + "type": "u32" + }, + { + "name": "enforceOutOfOrder", + "type": "bool" + }, + { + "name": "chainFamilySelector", + "type": { + "array": [ + "u8", + 4 + ] + } + } + ] + } + }, + { + "name": "DestChain", + "type": { + "kind": "struct", + "fields": [ + { + "name": "state", + "type": { + "defined": "DestChainState" + } + }, + { + "name": "config", + "type": { + "defined": "DestChainConfig" + } + } + ] + } + }, + { + "name": "TokenBilling", + "type": { + "kind": "struct", + "fields": [ + { + "name": "minFeeUsdcents", + "type": "u32" + }, + { + "name": "maxFeeUsdcents", + "type": "u32" + }, + { + "name": "deciBps", + "type": "u16" + }, + { + "name": "destGasOverhead", + "type": "u32" + }, + { + "name": "destBytesOverhead", + "type": "u32" + }, + { + "name": "isEnabled", + "type": "bool" + } + ] + } + }, + { + "name": "RateLimitTokenBucket", + "type": { + "kind": "struct", + "fields": [ + { + "name": "tokens", + "type": "u128" + }, + { + "name": "lastUpdated", + "type": "u32" + }, + { + "name": "isEnabled", + "type": "bool" + }, + { + "name": "capacity", + "type": "u128" + }, + { + "name": "rate", + "type": "u128" + } + ] + } + }, + { + "name": "BillingTokenConfig", + "type": { + "kind": "struct", + "fields": [ + { + "name": "enabled", + "type": "bool" + }, + { + "name": "mint", + "type": "publicKey" + }, + { + "name": "usdPerToken", + "type": { + "defined": "TimestampedPackedU224" + } + }, + { + "name": "premiumMultiplierWeiPerEth", + "type": "u64" + } + ] + } + }, + { + "name": "TimestampedPackedU224", + "type": { + "kind": "struct", + "fields": [ + { + "name": "value", + "type": { + "array": [ + "u8", + 28 + ] + } + }, + { + "name": "timestamp", + "type": "i64" + } + ] + } + }, + { + "name": "OcrPluginType", + "type": { + "kind": "enum", + "variants": [ + { + "name": "Commit" + }, + { + "name": "Execution" + } + ] + } + }, + { + "name": "MerkleError", + "type": { + "kind": "enum", + "variants": [ + { + "name": "InvalidProof" + } + ] + } + }, + { + "name": "MessageExecutionState", + "type": { + "kind": "enum", + "variants": [ + { + "name": "Untouched" + }, + { + "name": "InProgress" + }, + { + "name": "Success" + }, + { + "name": "Failure" + } + ] + } + }, + { + "name": "CcipRouterError", + "type": { + "kind": "enum", + "variants": [ + { + "name": "InvalidSequenceInterval" + }, + { + "name": "RootNotCommitted" + }, + { + "name": "ExistingMerkleRoot" + }, + { + "name": "Unauthorized" + }, + { + "name": "InvalidInputs" + }, + { + "name": "UnsupportedSourceChainSelector" + }, + { + "name": "UnsupportedDestinationChainSelector" + }, + { + "name": "InvalidProof" + }, + { + "name": "InvalidMessage" + }, + { + "name": "ReachedMaxSequenceNumber" + }, + { + "name": "ManualExecutionNotAllowed" + }, + { + "name": "InvalidInputsTokenIndices" + }, + { + "name": "InvalidInputsPoolAccounts" + }, + { + "name": "InvalidInputsTokenAccounts" + }, + { + "name": "InvalidInputsConfigAccounts" + }, + { + "name": "InvalidInputsTokenAdminRegistryAccounts" + }, + { + "name": "InvalidInputsLookupTableAccounts" + }, + { + "name": "InvalidInputsTokenAmount" + }, + { + "name": "OfframpReleaseMintBalanceMismatch" + }, + { + "name": "OfframpInvalidDataLength" + }, + { + "name": "StaleCommitReport" + }, + { + "name": "DestinationChainDisabled" + }, + { + "name": "FeeTokenDisabled" + }, + { + "name": "MessageTooLarge" + }, + { + "name": "UnsupportedNumberOfTokens" + }, + { + "name": "UnsupportedChainFamilySelector" + }, + { + "name": "InvalidEVMAddress" + }, + { + "name": "InvalidEncoding" + } + ] + } + } + ], + "events": [ + { + "name": "CCIPMessageSent", + "fields": [ + { + "name": "destChainSelector", + "type": "u64", + "index": false + }, + { + "name": "sequenceNumber", + "type": "u64", + "index": false + }, + { + "name": "message", + "type": { + "defined": "Solana2AnyRampMessage" + }, + "index": false + } + ] + }, + { + "name": "CommitReportAccepted", + "fields": [ + { + "name": "merkleRoot", + "type": { + "defined": "MerkleRoot" + }, + "index": false + }, + { + "name": "priceUpdates", + "type": { + "defined": "PriceUpdates" + }, + "index": false + } + ] + }, + { + "name": "SkippedAlreadyExecutedMessage", + "fields": [ + { + "name": "sourceChainSelector", + "type": "u64", + "index": false + }, + { + "name": "sequenceNumber", + "type": "u64", + "index": false + } + ] + }, + { + "name": "AlreadyAttempted", + "fields": [ + { + "name": "sourceChainSelector", + "type": "u64", + "index": false + }, + { + "name": "sequenceNumber", + "type": "u64", + "index": false + } + ] + }, + { + "name": "ExecutionStateChanged", + "fields": [ + { + "name": "sourceChainSelector", + "type": "u64", + "index": false + }, + { + "name": "sequenceNumber", + "type": "u64", + "index": false + }, + { + "name": "messageId", + "type": { + "array": [ + "u8", + 32 + ] + }, + "index": false + }, + { + "name": "messageHash", + "type": { + "array": [ + "u8", + 32 + ] + }, + "index": false + }, + { + "name": "state", + "type": { + "defined": "MessageExecutionState" + }, + "index": false + } + ] + }, + { + "name": "PoolSet", + "fields": [ + { + "name": "token", + "type": "publicKey", + "index": false + }, + { + "name": "previousPoolLookupTable", + "type": "publicKey", + "index": false + }, + { + "name": "newPoolLookupTable", + "type": "publicKey", + "index": false + } + ] + }, + { + "name": "AdministratorTransferRequested", + "fields": [ + { + "name": "token", + "type": "publicKey", + "index": false + }, + { + "name": "currentAdmin", + "type": "publicKey", + "index": false + }, + { + "name": "newAdmin", + "type": "publicKey", + "index": false + } + ] + }, + { + "name": "AdministratorTransferred", + "fields": [ + { + "name": "token", + "type": "publicKey", + "index": false + }, + { + "name": "newAdmin", + "type": "publicKey", + "index": false + } + ] + }, + { + "name": "FeeTokenAdded", + "fields": [ + { + "name": "feeToken", + "type": "publicKey", + "index": false + }, + { + "name": "enabled", + "type": "bool", + "index": false + } + ] + }, + { + "name": "FeeTokenEnabled", + "fields": [ + { + "name": "feeToken", + "type": "publicKey", + "index": false + } + ] + }, + { + "name": "FeeTokenDisabled", + "fields": [ + { + "name": "feeToken", + "type": "publicKey", + "index": false + } + ] + }, + { + "name": "FeeTokenRemoved", + "fields": [ + { + "name": "feeToken", + "type": "publicKey", + "index": false + } + ] + }, + { + "name": "UsdPerUnitGasUpdated", + "fields": [ + { + "name": "destChain", + "type": "u64", + "index": false + }, + { + "name": "value", + "type": { + "array": [ + "u8", + 28 + ] + }, + "index": false + }, + { + "name": "timestamp", + "type": "i64", + "index": false + } + ] + }, + { + "name": "UsdPerTokenUpdated", + "fields": [ + { + "name": "token", + "type": "publicKey", + "index": false + }, + { + "name": "value", + "type": { + "array": [ + "u8", + 28 + ] + }, + "index": false + }, + { + "name": "timestamp", + "type": "i64", + "index": false + } + ] + }, + { + "name": "TokenTransferFeeConfigUpdated", + "fields": [ + { + "name": "destChainSelector", + "type": "u64", + "index": false + }, + { + "name": "token", + "type": "publicKey", + "index": false + }, + { + "name": "tokenTransferFeeConfig", + "type": { + "defined": "TokenBilling" + }, + "index": false + } + ] + }, + { + "name": "PremiumMultiplierWeiPerEthUpdated", + "fields": [ + { + "name": "token", + "type": "publicKey", + "index": false + }, + { + "name": "premiumMultiplierWeiPerEth", + "type": "u64", + "index": false + } + ] + }, + { + "name": "SourceChainConfigUpdated", + "fields": [ + { + "name": "sourceChainSelector", + "type": "u64", + "index": false + }, + { + "name": "sourceChainConfig", + "type": { + "defined": "SourceChainConfig" + }, + "index": false + } + ] + }, + { + "name": "SourceChainAdded", + "fields": [ + { + "name": "sourceChainSelector", + "type": "u64", + "index": false + }, + { + "name": "sourceChainConfig", + "type": { + "defined": "SourceChainConfig" + }, + "index": false + } + ] + }, + { + "name": "DestChainConfigUpdated", + "fields": [ + { + "name": "destChainSelector", + "type": "u64", + "index": false + }, + { + "name": "destChainConfig", + "type": { + "defined": "DestChainConfig" + }, + "index": false + } + ] + }, + { + "name": "DestChainAdded", + "fields": [ + { + "name": "destChainSelector", + "type": "u64", + "index": false + }, + { + "name": "destChainConfig", + "type": { + "defined": "DestChainConfig" + }, + "index": false + } + ] + }, + { + "name": "AdministratorRegistered", + "fields": [ + { + "name": "tokenMint", + "type": "publicKey", + "index": false + }, + { + "name": "administrator", + "type": "publicKey", + "index": false + } + ] + }, + { + "name": "ConfigSet", + "fields": [ + { + "name": "ocrPluginType", + "type": "u8", + "index": false + }, + { + "name": "configDigest", + "type": { + "array": [ + "u8", + 32 + ] + }, + "index": false + }, + { + "name": "signers", + "type": { + "vec": { + "array": [ + "u8", + 20 + ] + } + }, + "index": false + }, + { + "name": "transmitters", + "type": { + "vec": "publicKey" + }, + "index": false + }, + { + "name": "f", + "type": "u8", + "index": false + } + ] + }, + { + "name": "Transmitted", + "fields": [ + { + "name": "ocrPluginType", + "type": "u8", + "index": false + }, + { + "name": "configDigest", + "type": { + "array": [ + "u8", + 32 + ] + }, + "index": false + }, + { + "name": "sequenceNumber", + "type": "u64", + "index": false + } + ] + } + ], + "errors": [ + { + "code": 6000, + "name": "InvalidConfigFMustBePositive", + "msg": "Invalid config: F must be positive" + }, + { + "code": 6001, + "name": "InvalidConfigTooManyTransmitters", + "msg": "Invalid config: Too many transmitters" + }, + { + "code": 6002, + "name": "InvalidConfigTooManySigners", + "msg": "Invalid config: Too many signers" + }, + { + "code": 6003, + "name": "InvalidConfigFIsTooHigh", + "msg": "Invalid config: F is too high" + }, + { + "code": 6004, + "name": "InvalidConfigRepeatedOracle", + "msg": "Invalid config: Repeated oracle address" + }, + { + "code": 6005, + "name": "WrongMessageLength", + "msg": "Wrong message length" + }, + { + "code": 6006, + "name": "ConfigDigestMismatch", + "msg": "Config digest mismatch" + }, + { + "code": 6007, + "name": "WrongNumberOfSignatures", + "msg": "Wrong number signatures" + }, + { + "code": 6008, + "name": "UnauthorizedTransmitter", + "msg": "Unauthorized transmitter" + }, + { + "code": 6009, + "name": "UnauthorizedSigner", + "msg": "Unauthorized signer" + }, + { + "code": 6010, + "name": "NonUniqueSignatures", + "msg": "Non unique signatures" + }, + { + "code": 6011, + "name": "OracleCannotBeZeroAddress", + "msg": "Oracle cannot be zero address" + }, + { + "code": 6012, + "name": "StaticConfigCannotBeChanged", + "msg": "Static config cannot be changed" + }, + { + "code": 6013, + "name": "InvalidPluginType", + "msg": "Incorrect plugin type" + }, + { + "code": 6014, + "name": "InvalidSignature", + "msg": "Invalid signature" + } + ] +} diff --git a/pkg/solana/logpoller/log_poller.go b/pkg/solana/logpoller/log_poller.go index 35429daae..2abfe06ed 100644 --- a/pkg/solana/logpoller/log_poller.go +++ b/pkg/solana/logpoller/log_poller.go @@ -139,6 +139,10 @@ func (lp *LogPoller) Process(programEvent ProgramEvent) (err error) { // TODO: fill in, and keep track of SequenceNumber for each filter. (Initialize from db on LoadFilters, then increment each time?) + expiresAt := time.Now() // TODO: account for possible discrepencies in time? Seems like retention should be passed directly to ORM + expiresAt.Add(filter.Retention) + log.ExpiresAt = &expiresAt + logs = append(logs, log) } From bc8022e2ec82ba4b98afaaf643493de6ce5bcf1b Mon Sep 17 00:00:00 2001 From: Domino Valdano <2644901+reductionista@users.noreply.github.com> Date: Thu, 2 Jan 2025 15:05:38 -0800 Subject: [PATCH 16/43] Add sequence # tracking, fix floating point encoding, add tests for NewIndexedValue --- pkg/solana/logpoller/filters.go | 11 + pkg/solana/logpoller/log_poller.go | 6 +- pkg/solana/logpoller/mock_orm.go | 58 +++ pkg/solana/logpoller/orm.go | 10 + .../pkg/solana/logpoller/mock_orm.go | 442 ++++++++++++++++++ pkg/solana/logpoller/types.go | 14 +- pkg/solana/logpoller/types_test.go | 35 ++ 7 files changed, 566 insertions(+), 10 deletions(-) create mode 100644 pkg/solana/logpoller/pkg/solana/logpoller/mock_orm.go diff --git a/pkg/solana/logpoller/filters.go b/pkg/solana/logpoller/filters.go index 1458abe39..93af60a85 100644 --- a/pkg/solana/logpoller/filters.go +++ b/pkg/solana/logpoller/filters.go @@ -29,6 +29,7 @@ type filters struct { loadedFilters atomic.Bool knownPrograms map[string]uint // fast lookup to see if a base58-encoded ProgramID matches any registered filters knownDiscriminators map[string]uint // fast lookup by first 10 characters (60-bits) of a base64-encoded discriminator + seqNums map[int64]int64 } func newFilters(lggr logger.SugaredLogger, orm ORM) *filters { @@ -38,6 +39,11 @@ func newFilters(lggr logger.SugaredLogger, orm ORM) *filters { } } +func (fl *filters) IncrementSeqNums(filterID int64) int64 { + fl.seqNums[filterID]++ + return fl.seqNums[filterID] +} + // PruneFilters - prunes all filters marked to be deleted from the database and all corresponding logs. func (fl *filters) PruneFilters(ctx context.Context) error { err := fl.LoadFilters(ctx) @@ -385,6 +391,11 @@ func (fl *filters) LoadFilters(ctx context.Context) error { } } + fl.seqNums, err = fl.orm.SelectSeqNums(ctx) + if err != nil { + return fmt.Errorf("failed to select sequence numbers from db: %w", err) + } + fl.loadedFilters.Store(true) return nil diff --git a/pkg/solana/logpoller/log_poller.go b/pkg/solana/logpoller/log_poller.go index 2abfe06ed..e454fdc77 100644 --- a/pkg/solana/logpoller/log_poller.go +++ b/pkg/solana/logpoller/log_poller.go @@ -30,6 +30,7 @@ type ORM interface { MarkFilterDeleted(ctx context.Context, id int64) (err error) MarkFilterBackfilled(ctx context.Context, id int64) (err error) InsertLogs(context.Context, []Log) (err error) + SelectSeqNums(ctx context.Context) (map[int64]int64, error) } type ILogPoller interface { @@ -134,10 +135,7 @@ func (lp *LogPoller) Process(programEvent ProgramEvent) (err error) { subKeyValues = append(subKeyValues, indexedVal) } - lp.seqNums[filter.ID]++ - log.SequenceNum = lp.seqNums - - // TODO: fill in, and keep track of SequenceNumber for each filter. (Initialize from db on LoadFilters, then increment each time?) + log.SequenceNum = lp.filters.IncrementSeqNums(filter.ID) expiresAt := time.Now() // TODO: account for possible discrepencies in time? Seems like retention should be passed directly to ORM expiresAt.Add(filter.Retention) diff --git a/pkg/solana/logpoller/mock_orm.go b/pkg/solana/logpoller/mock_orm.go index 25bf0972e..1508ba4aa 100644 --- a/pkg/solana/logpoller/mock_orm.go +++ b/pkg/solana/logpoller/mock_orm.go @@ -369,6 +369,64 @@ func (_c *mockORM_SelectFilters_Call) RunAndReturn(run func(context.Context) ([] return _c } +// SelectSeqNums provides a mock function with given fields: ctx +func (_m *mockORM) SelectSeqNums(ctx context.Context) (map[int64]int64, error) { + ret := _m.Called(ctx) + + if len(ret) == 0 { + panic("no return value specified for SelectSeqNums") + } + + var r0 map[int64]int64 + var r1 error + if rf, ok := ret.Get(0).(func(context.Context) (map[int64]int64, error)); ok { + return rf(ctx) + } + if rf, ok := ret.Get(0).(func(context.Context) map[int64]int64); ok { + r0 = rf(ctx) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(map[int64]int64) + } + } + + if rf, ok := ret.Get(1).(func(context.Context) error); ok { + r1 = rf(ctx) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// mockORM_SelectSeqNums_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SelectSeqNums' +type mockORM_SelectSeqNums_Call struct { + *mock.Call +} + +// SelectSeqNums is a helper method to define mock.On call +// - ctx context.Context +func (_e *mockORM_Expecter) SelectSeqNums(ctx interface{}) *mockORM_SelectSeqNums_Call { + return &mockORM_SelectSeqNums_Call{Call: _e.mock.On("SelectSeqNums", ctx)} +} + +func (_c *mockORM_SelectSeqNums_Call) Run(run func(ctx context.Context)) *mockORM_SelectSeqNums_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context)) + }) + return _c +} + +func (_c *mockORM_SelectSeqNums_Call) Return(_a0 map[int64]int64, _a1 error) *mockORM_SelectSeqNums_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *mockORM_SelectSeqNums_Call) RunAndReturn(run func(context.Context) (map[int64]int64, error)) *mockORM_SelectSeqNums_Call { + _c.Call.Return(run) + return _c +} + // newMockORM creates a new instance of mockORM. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. // The first argument is typically a *testing.T value. func newMockORM(t interface { diff --git a/pkg/solana/logpoller/orm.go b/pkg/solana/logpoller/orm.go index 38ba77da2..1504beea7 100644 --- a/pkg/solana/logpoller/orm.go +++ b/pkg/solana/logpoller/orm.go @@ -229,3 +229,13 @@ func (o *DSORM) FilteredLogs(ctx context.Context, filter []query.Expression, lim return logs, nil } + +func (o *DSORM) SelectSeqNums(ctx context.Context) (map[int64]int64, error) { + seqNums := make(map[int64]int64) + query := "SELECT id, MAX(sequence_num) FROM solana.logs WHERE chain_id=%s GROUP BY id" + err := o.ds.SelectContext(ctx, &seqNums, query, o.chainID) + if err != nil { + return nil, err + } + return seqNums, nil +} diff --git a/pkg/solana/logpoller/pkg/solana/logpoller/mock_orm.go b/pkg/solana/logpoller/pkg/solana/logpoller/mock_orm.go new file mode 100644 index 000000000..1508ba4aa --- /dev/null +++ b/pkg/solana/logpoller/pkg/solana/logpoller/mock_orm.go @@ -0,0 +1,442 @@ +// Code generated by mockery v2.43.2. DO NOT EDIT. + +package logpoller + +import ( + context "context" + + mock "github.com/stretchr/testify/mock" +) + +// mockORM is an autogenerated mock type for the ORM type +type mockORM struct { + mock.Mock +} + +type mockORM_Expecter struct { + mock *mock.Mock +} + +func (_m *mockORM) EXPECT() *mockORM_Expecter { + return &mockORM_Expecter{mock: &_m.Mock} +} + +// ChainID provides a mock function with given fields: +func (_m *mockORM) ChainID() string { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for ChainID") + } + + var r0 string + if rf, ok := ret.Get(0).(func() string); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(string) + } + + return r0 +} + +// mockORM_ChainID_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ChainID' +type mockORM_ChainID_Call struct { + *mock.Call +} + +// ChainID is a helper method to define mock.On call +func (_e *mockORM_Expecter) ChainID() *mockORM_ChainID_Call { + return &mockORM_ChainID_Call{Call: _e.mock.On("ChainID")} +} + +func (_c *mockORM_ChainID_Call) Run(run func()) *mockORM_ChainID_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *mockORM_ChainID_Call) Return(_a0 string) *mockORM_ChainID_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *mockORM_ChainID_Call) RunAndReturn(run func() string) *mockORM_ChainID_Call { + _c.Call.Return(run) + return _c +} + +// DeleteFilters provides a mock function with given fields: ctx, filters +func (_m *mockORM) DeleteFilters(ctx context.Context, filters map[int64]Filter) error { + ret := _m.Called(ctx, filters) + + if len(ret) == 0 { + panic("no return value specified for DeleteFilters") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, map[int64]Filter) error); ok { + r0 = rf(ctx, filters) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// mockORM_DeleteFilters_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'DeleteFilters' +type mockORM_DeleteFilters_Call struct { + *mock.Call +} + +// DeleteFilters is a helper method to define mock.On call +// - ctx context.Context +// - filters map[int64]Filter +func (_e *mockORM_Expecter) DeleteFilters(ctx interface{}, filters interface{}) *mockORM_DeleteFilters_Call { + return &mockORM_DeleteFilters_Call{Call: _e.mock.On("DeleteFilters", ctx, filters)} +} + +func (_c *mockORM_DeleteFilters_Call) Run(run func(ctx context.Context, filters map[int64]Filter)) *mockORM_DeleteFilters_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(map[int64]Filter)) + }) + return _c +} + +func (_c *mockORM_DeleteFilters_Call) Return(_a0 error) *mockORM_DeleteFilters_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *mockORM_DeleteFilters_Call) RunAndReturn(run func(context.Context, map[int64]Filter) error) *mockORM_DeleteFilters_Call { + _c.Call.Return(run) + return _c +} + +// InsertFilter provides a mock function with given fields: ctx, filter +func (_m *mockORM) InsertFilter(ctx context.Context, filter Filter) (int64, error) { + ret := _m.Called(ctx, filter) + + if len(ret) == 0 { + panic("no return value specified for InsertFilter") + } + + var r0 int64 + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, Filter) (int64, error)); ok { + return rf(ctx, filter) + } + if rf, ok := ret.Get(0).(func(context.Context, Filter) int64); ok { + r0 = rf(ctx, filter) + } else { + r0 = ret.Get(0).(int64) + } + + if rf, ok := ret.Get(1).(func(context.Context, Filter) error); ok { + r1 = rf(ctx, filter) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// mockORM_InsertFilter_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'InsertFilter' +type mockORM_InsertFilter_Call struct { + *mock.Call +} + +// InsertFilter is a helper method to define mock.On call +// - ctx context.Context +// - filter Filter +func (_e *mockORM_Expecter) InsertFilter(ctx interface{}, filter interface{}) *mockORM_InsertFilter_Call { + return &mockORM_InsertFilter_Call{Call: _e.mock.On("InsertFilter", ctx, filter)} +} + +func (_c *mockORM_InsertFilter_Call) Run(run func(ctx context.Context, filter Filter)) *mockORM_InsertFilter_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(Filter)) + }) + return _c +} + +func (_c *mockORM_InsertFilter_Call) Return(id int64, err error) *mockORM_InsertFilter_Call { + _c.Call.Return(id, err) + return _c +} + +func (_c *mockORM_InsertFilter_Call) RunAndReturn(run func(context.Context, Filter) (int64, error)) *mockORM_InsertFilter_Call { + _c.Call.Return(run) + return _c +} + +// InsertLogs provides a mock function with given fields: _a0, _a1 +func (_m *mockORM) InsertLogs(_a0 context.Context, _a1 []Log) error { + ret := _m.Called(_a0, _a1) + + if len(ret) == 0 { + panic("no return value specified for InsertLogs") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, []Log) error); ok { + r0 = rf(_a0, _a1) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// mockORM_InsertLogs_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'InsertLogs' +type mockORM_InsertLogs_Call struct { + *mock.Call +} + +// InsertLogs is a helper method to define mock.On call +// - _a0 context.Context +// - _a1 []Log +func (_e *mockORM_Expecter) InsertLogs(_a0 interface{}, _a1 interface{}) *mockORM_InsertLogs_Call { + return &mockORM_InsertLogs_Call{Call: _e.mock.On("InsertLogs", _a0, _a1)} +} + +func (_c *mockORM_InsertLogs_Call) Run(run func(_a0 context.Context, _a1 []Log)) *mockORM_InsertLogs_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].([]Log)) + }) + return _c +} + +func (_c *mockORM_InsertLogs_Call) Return(err error) *mockORM_InsertLogs_Call { + _c.Call.Return(err) + return _c +} + +func (_c *mockORM_InsertLogs_Call) RunAndReturn(run func(context.Context, []Log) error) *mockORM_InsertLogs_Call { + _c.Call.Return(run) + return _c +} + +// MarkFilterBackfilled provides a mock function with given fields: ctx, id +func (_m *mockORM) MarkFilterBackfilled(ctx context.Context, id int64) error { + ret := _m.Called(ctx, id) + + if len(ret) == 0 { + panic("no return value specified for MarkFilterBackfilled") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, int64) error); ok { + r0 = rf(ctx, id) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// mockORM_MarkFilterBackfilled_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'MarkFilterBackfilled' +type mockORM_MarkFilterBackfilled_Call struct { + *mock.Call +} + +// MarkFilterBackfilled is a helper method to define mock.On call +// - ctx context.Context +// - id int64 +func (_e *mockORM_Expecter) MarkFilterBackfilled(ctx interface{}, id interface{}) *mockORM_MarkFilterBackfilled_Call { + return &mockORM_MarkFilterBackfilled_Call{Call: _e.mock.On("MarkFilterBackfilled", ctx, id)} +} + +func (_c *mockORM_MarkFilterBackfilled_Call) Run(run func(ctx context.Context, id int64)) *mockORM_MarkFilterBackfilled_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(int64)) + }) + return _c +} + +func (_c *mockORM_MarkFilterBackfilled_Call) Return(err error) *mockORM_MarkFilterBackfilled_Call { + _c.Call.Return(err) + return _c +} + +func (_c *mockORM_MarkFilterBackfilled_Call) RunAndReturn(run func(context.Context, int64) error) *mockORM_MarkFilterBackfilled_Call { + _c.Call.Return(run) + return _c +} + +// MarkFilterDeleted provides a mock function with given fields: ctx, id +func (_m *mockORM) MarkFilterDeleted(ctx context.Context, id int64) error { + ret := _m.Called(ctx, id) + + if len(ret) == 0 { + panic("no return value specified for MarkFilterDeleted") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, int64) error); ok { + r0 = rf(ctx, id) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// mockORM_MarkFilterDeleted_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'MarkFilterDeleted' +type mockORM_MarkFilterDeleted_Call struct { + *mock.Call +} + +// MarkFilterDeleted is a helper method to define mock.On call +// - ctx context.Context +// - id int64 +func (_e *mockORM_Expecter) MarkFilterDeleted(ctx interface{}, id interface{}) *mockORM_MarkFilterDeleted_Call { + return &mockORM_MarkFilterDeleted_Call{Call: _e.mock.On("MarkFilterDeleted", ctx, id)} +} + +func (_c *mockORM_MarkFilterDeleted_Call) Run(run func(ctx context.Context, id int64)) *mockORM_MarkFilterDeleted_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(int64)) + }) + return _c +} + +func (_c *mockORM_MarkFilterDeleted_Call) Return(err error) *mockORM_MarkFilterDeleted_Call { + _c.Call.Return(err) + return _c +} + +func (_c *mockORM_MarkFilterDeleted_Call) RunAndReturn(run func(context.Context, int64) error) *mockORM_MarkFilterDeleted_Call { + _c.Call.Return(run) + return _c +} + +// SelectFilters provides a mock function with given fields: ctx +func (_m *mockORM) SelectFilters(ctx context.Context) ([]Filter, error) { + ret := _m.Called(ctx) + + if len(ret) == 0 { + panic("no return value specified for SelectFilters") + } + + var r0 []Filter + var r1 error + if rf, ok := ret.Get(0).(func(context.Context) ([]Filter, error)); ok { + return rf(ctx) + } + if rf, ok := ret.Get(0).(func(context.Context) []Filter); ok { + r0 = rf(ctx) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]Filter) + } + } + + if rf, ok := ret.Get(1).(func(context.Context) error); ok { + r1 = rf(ctx) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// mockORM_SelectFilters_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SelectFilters' +type mockORM_SelectFilters_Call struct { + *mock.Call +} + +// SelectFilters is a helper method to define mock.On call +// - ctx context.Context +func (_e *mockORM_Expecter) SelectFilters(ctx interface{}) *mockORM_SelectFilters_Call { + return &mockORM_SelectFilters_Call{Call: _e.mock.On("SelectFilters", ctx)} +} + +func (_c *mockORM_SelectFilters_Call) Run(run func(ctx context.Context)) *mockORM_SelectFilters_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context)) + }) + return _c +} + +func (_c *mockORM_SelectFilters_Call) Return(_a0 []Filter, _a1 error) *mockORM_SelectFilters_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *mockORM_SelectFilters_Call) RunAndReturn(run func(context.Context) ([]Filter, error)) *mockORM_SelectFilters_Call { + _c.Call.Return(run) + return _c +} + +// SelectSeqNums provides a mock function with given fields: ctx +func (_m *mockORM) SelectSeqNums(ctx context.Context) (map[int64]int64, error) { + ret := _m.Called(ctx) + + if len(ret) == 0 { + panic("no return value specified for SelectSeqNums") + } + + var r0 map[int64]int64 + var r1 error + if rf, ok := ret.Get(0).(func(context.Context) (map[int64]int64, error)); ok { + return rf(ctx) + } + if rf, ok := ret.Get(0).(func(context.Context) map[int64]int64); ok { + r0 = rf(ctx) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(map[int64]int64) + } + } + + if rf, ok := ret.Get(1).(func(context.Context) error); ok { + r1 = rf(ctx) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// mockORM_SelectSeqNums_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SelectSeqNums' +type mockORM_SelectSeqNums_Call struct { + *mock.Call +} + +// SelectSeqNums is a helper method to define mock.On call +// - ctx context.Context +func (_e *mockORM_Expecter) SelectSeqNums(ctx interface{}) *mockORM_SelectSeqNums_Call { + return &mockORM_SelectSeqNums_Call{Call: _e.mock.On("SelectSeqNums", ctx)} +} + +func (_c *mockORM_SelectSeqNums_Call) Run(run func(ctx context.Context)) *mockORM_SelectSeqNums_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context)) + }) + return _c +} + +func (_c *mockORM_SelectSeqNums_Call) Return(_a0 map[int64]int64, _a1 error) *mockORM_SelectSeqNums_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *mockORM_SelectSeqNums_Call) RunAndReturn(run func(context.Context) (map[int64]int64, error)) *mockORM_SelectSeqNums_Call { + _c.Call.Return(run) + return _c +} + +// newMockORM creates a new instance of mockORM. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func newMockORM(t interface { + mock.TestingT + Cleanup(func()) +}) *mockORM { + mock := &mockORM{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/pkg/solana/logpoller/types.go b/pkg/solana/logpoller/types.go index 9ec1a4e01..938fb12f3 100644 --- a/pkg/solana/logpoller/types.go +++ b/pkg/solana/logpoller/types.go @@ -159,16 +159,18 @@ func (v *IndexedValue) FromInt64(i int64) { v.FromUint64(uint64(i + math.MaxInt64)) } -func (v *IndexedValue) FromUint64(u uint64) []byte { - var b []byte - binary.BigEndian.PutUint64(b, u) +func (v *IndexedValue) FromUint64(u uint64) { + *v = make([]byte, 8) + binary.BigEndian.PutUint64(*v, u) } func (v *IndexedValue) FromFloat64(f float64) { - if f >= 0 { - v.FromUint64(math.Float64bits(f)) + if f > 0 { + v.FromUint64(math.Float64bits(f) + math.MaxInt64) + return } - v.FromUint64(math.Float64bits(math.Abs(f))) + v.FromUint64(math.MaxInt64 - math.Float64bits(f)) + return } func NewIndexedValue(typedVal any) (iVal IndexedValue, err error) { diff --git a/pkg/solana/logpoller/types_test.go b/pkg/solana/logpoller/types_test.go index 263c22bab..c60ca4794 100644 --- a/pkg/solana/logpoller/types_test.go +++ b/pkg/solana/logpoller/types_test.go @@ -4,6 +4,7 @@ import ( "testing" "github.com/gagliardetto/solana-go" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -18,3 +19,37 @@ func newRandomEventSignature(t *testing.T) EventSignature { pubKey := newRandomPublicKey(t) return EventSignature(pubKey[:8]) } + +func TestIndexedValue(t *testing.T) { + cases := []struct { + typeName string + lower any + higher any + }{ + {"int32", int32(-5), int32(5)}, + {"int32", int32(-8), int32(-5)}, + {"int32", int32(5), int32(8)}, + {"int64", int64(-5), int64(5)}, + {"int64", int64(-8), int64(-5)}, + {"int64", int64(5), int64(8)}, + {"float32", float32(-5), float32(5)}, + {"float32", float32(-8), float32(-5)}, + {"float32", float32(5), float32(8)}, + {"float64", float64(-5), float64(5)}, + {"float64", float64(-8), float64(-5)}, + {"float64", float64(5), float64(8)}, + {"string", "abcc", "abcd"}, + {"string", "abcd", "abcdef"}, + {"[]byte", []byte("abcc"), []byte("abcd")}, + {"[]byte", []byte("abcd"), []byte("abcdef")}, + } + for _, c := range cases { + t.Run(c.typeName, func(t *testing.T) { + iVal1, err := NewIndexedValue(c.lower) + require.NoError(t, err) + iVal2, err := NewIndexedValue(c.higher) + require.NoError(t, err) + assert.Less(t, iVal1, iVal2) + }) + } +} From 8982cb813e9479fdbfe74bb2722c3dad8f9b31d5 Mon Sep 17 00:00:00 2001 From: Domino Valdano <2644901+reductionista@users.noreply.github.com> Date: Mon, 6 Jan 2025 19:47:40 -0800 Subject: [PATCH 17/43] Fix tests --- integration-tests/smoke/event_loader_test.go | 6 +- pkg/solana/client/client.go | 15 ++- pkg/solana/logpoller/filters.go | 22 ++-- pkg/solana/logpoller/filters_test.go | 29 +++++ pkg/solana/logpoller/job.go | 2 +- pkg/solana/logpoller/loader_test.go | 114 +++++++++++-------- pkg/solana/logpoller/models.go | 8 ++ 7 files changed, 129 insertions(+), 67 deletions(-) diff --git a/integration-tests/smoke/event_loader_test.go b/integration-tests/smoke/event_loader_test.go index cd4bc678c..cd8518e81 100644 --- a/integration-tests/smoke/event_loader_test.go +++ b/integration-tests/smoke/event_loader_test.go @@ -25,6 +25,7 @@ import ( contract "github.com/smartcontractkit/chainlink-solana/contracts/generated/log_read_test" "github.com/smartcontractkit/chainlink-solana/pkg/solana/client" + "github.com/smartcontractkit/chainlink-solana/pkg/solana/config" "github.com/smartcontractkit/chainlink-solana/pkg/solana/logpoller" "github.com/smartcontractkit/chainlink-solana/integration-tests/solclient" @@ -49,7 +50,8 @@ func TestEventLoader(t *testing.T) { require.NoError(t, err) rpcURL, wsURL := setupTestValidator(t, privateKey.PublicKey().String()) - rpcClient := rpc.New(rpcURL) + cl, rpcClient, err := client.NewTestClient(rpcURL, config.NewDefault(), 1*time.Second, logger.Nop()) + require.NoError(t, err) wsClient, err := ws.Connect(ctx, wsURL) require.NoError(t, err) @@ -62,7 +64,7 @@ func TestEventLoader(t *testing.T) { parser := &printParser{t: t} sender := newLogSender(t, rpcClient, wsClient) collector := logpoller.NewEncodedLogCollector( - rpcClient, + cl, parser, logger.Nop(), ) diff --git a/pkg/solana/client/client.go b/pkg/solana/client/client.go index 5fa529f12..9f38dfa1d 100644 --- a/pkg/solana/client/client.go +++ b/pkg/solana/client/client.go @@ -73,10 +73,10 @@ type Client struct { requestGroup *singleflight.Group } -func NewClient(endpoint string, cfg config.Config, requestTimeout time.Duration, log logger.Logger) (*Client, error) { - return &Client{ +// Return both the client and the underlying rpc client for testing +func NewTestClient(endpoint string, cfg config.Config, requestTimeout time.Duration, log logger.Logger) (*Client, *rpc.Client, error) { + rpcClient := Client{ url: endpoint, - rpc: rpc.New(endpoint), skipPreflight: cfg.SkipPreflight(), commitment: cfg.Commitment(), maxRetries: cfg.MaxRetries(), @@ -84,7 +84,14 @@ func NewClient(endpoint string, cfg config.Config, requestTimeout time.Duration, contextDuration: requestTimeout, log: log, requestGroup: &singleflight.Group{}, - }, nil + } + rpcClient.rpc = rpc.New(endpoint) + return &rpcClient, rpcClient.rpc, nil +} + +func NewClient(endpoint string, cfg config.Config, requestTimeout time.Duration, log logger.Logger) (*Client, error) { + rpcClient, _, err := NewTestClient(endpoint, cfg, requestTimeout, log) + return rpcClient, err } func (c *Client) latency(name string) func() { diff --git a/pkg/solana/logpoller/filters.go b/pkg/solana/logpoller/filters.go index 93af60a85..20f971ec2 100644 --- a/pkg/solana/logpoller/filters.go +++ b/pkg/solana/logpoller/filters.go @@ -12,8 +12,6 @@ import ( "github.com/gagliardetto/solana-go" "github.com/smartcontractkit/chainlink-common/pkg/logger" - - "github.com/smartcontractkit/chainlink-solana/pkg/solana/logpoller/utils" ) type filters struct { @@ -88,8 +86,6 @@ func (fl *filters) RegisterFilter(ctx context.Context, filter Filter) error { return fmt.Errorf("failed to load filters: %w", err) } - filter.EventSig = utils.Discriminator("event", filter.EventName) - fl.filtersMutex.Lock() defer fl.filtersMutex.Unlock() @@ -134,17 +130,17 @@ func (fl *filters) RegisterFilter(ctx context.Context, filter Filter) error { } programID := filter.Address.ToSolana().String() - if _, ok := fl.knownPrograms[programID]; !ok { + if _, ok = fl.knownPrograms[programID]; !ok { fl.knownPrograms[programID] = 1 } else { fl.knownPrograms[programID]++ } - discriminator := base64.StdEncoding.EncodeToString(filter.EventSig[:])[:10] + discriminatorHead := filter.Discriminator()[:10] if _, ok := fl.knownPrograms[programID]; !ok { - fl.knownDiscriminators[discriminator] = 1 + fl.knownDiscriminators[discriminatorHead] = 1 } else { - fl.knownDiscriminators[discriminator]++ + fl.knownDiscriminators[discriminatorHead]++ } return nil @@ -220,13 +216,13 @@ func (fl *filters) removeFilterFromIndexes(filter Filter) { } } - discriminator := base64.StdEncoding.EncodeToString(filter.EventSig[:])[:10] - if refcount, ok := fl.knownDiscriminators[discriminator]; ok { + discriminatorHead := filter.Discriminator()[:10] + if refcount, ok := fl.knownDiscriminators[discriminatorHead]; ok { refcount-- if refcount > 0 { - fl.knownDiscriminators[discriminator] = refcount + fl.knownDiscriminators[discriminatorHead] = refcount } else { - delete(fl.knownDiscriminators, discriminator) + delete(fl.knownDiscriminators, discriminatorHead) } } } @@ -345,6 +341,8 @@ func (fl *filters) LoadFilters(ctx context.Context) error { fl.filtersByAddress = make(map[PublicKey]map[EventSignature]map[int64]struct{}) fl.filtersToBackfill = make(map[int64]struct{}) fl.filtersToDelete = make(map[int64]Filter) + fl.knownPrograms = make(map[string]uint) + fl.knownDiscriminators = make(map[string]uint) filters, err := fl.orm.SelectFilters(ctx) if err != nil { diff --git a/pkg/solana/logpoller/filters_test.go b/pkg/solana/logpoller/filters_test.go index 710f08a9f..15b14c22b 100644 --- a/pkg/solana/logpoller/filters_test.go +++ b/pkg/solana/logpoller/filters_test.go @@ -39,6 +39,12 @@ func TestFilters_LoadFilters(t *testing.T) { happyPath2, }, nil).Once() + orm.On("SelectSeqNums", mock.Anything).Return(map[int64]int64{ + 1: 18, + 2: 25, + 3: 0, + }, nil) + err := fs.LoadFilters(ctx) require.EqualError(t, err, "failed to select filters from db: db failed") err = fs.LoadFilters(ctx) @@ -110,6 +116,7 @@ func TestFilters_RegisterFilter(t *testing.T) { const filterName = "Filter" dbFilter := Filter{Name: filterName} orm.On("SelectFilters", mock.Anything).Return([]Filter{dbFilter}, nil).Once() + orm.On("SelectSeqNums", mock.Anything).Return(map[int64]int64{}, nil) newFilter := dbFilter tc.ModifyField(&newFilter) err := fs.RegisterFilter(tests.Context(t), newFilter) @@ -122,6 +129,7 @@ func TestFilters_RegisterFilter(t *testing.T) { fs := newFilters(lggr, orm) const filterName = "Filter" orm.On("SelectFilters", mock.Anything).Return(nil, nil).Once() + orm.On("SelectSeqNums", mock.Anything).Return(map[int64]int64{}, nil).Once() orm.On("InsertFilter", mock.Anything, mock.Anything).Return(int64(0), errors.New("failed to insert")).Once() filter := Filter{Name: filterName} err := fs.RegisterFilter(tests.Context(t), filter) @@ -149,6 +157,7 @@ func TestFilters_RegisterFilter(t *testing.T) { fs := newFilters(lggr, orm) const filterName = "Filter" orm.On("SelectFilters", mock.Anything).Return(nil, nil).Once() + orm.On("SelectSeqNums", mock.Anything).Return(map[int64]int64{}, nil).Once() const filterID = int64(10) orm.On("InsertFilter", mock.Anything, mock.Anything).Return(filterID, nil).Once() err := fs.RegisterFilter(tests.Context(t), Filter{Name: filterName}) @@ -180,6 +189,7 @@ func TestFilters_UnregisterFilter(t *testing.T) { fs := newFilters(lggr, orm) const filterName = "Filter" orm.On("SelectFilters", mock.Anything).Return(nil, nil).Once() + orm.On("SelectSeqNums", mock.Anything).Return(map[int64]int64{}, nil).Once() err := fs.UnregisterFilter(tests.Context(t), filterName) require.NoError(t, err) }) @@ -189,6 +199,7 @@ func TestFilters_UnregisterFilter(t *testing.T) { const filterName = "Filter" const id int64 = 10 orm.On("SelectFilters", mock.Anything).Return([]Filter{{ID: id, Name: filterName}}, nil).Once() + orm.On("SelectSeqNums", mock.Anything).Return(map[int64]int64{}, nil).Once() orm.On("MarkFilterDeleted", mock.Anything, id).Return(errors.New("db query failed")).Once() err := fs.UnregisterFilter(tests.Context(t), filterName) require.EqualError(t, err, "failed to mark filter deleted: db query failed") @@ -199,6 +210,7 @@ func TestFilters_UnregisterFilter(t *testing.T) { const filterName = "Filter" const id int64 = 10 orm.On("SelectFilters", mock.Anything).Return([]Filter{{ID: id, Name: filterName}}, nil).Once() + orm.On("SelectSeqNums", mock.Anything).Return(map[int64]int64{}, nil).Once() orm.On("MarkFilterDeleted", mock.Anything, id).Return(nil).Once() err := fs.UnregisterFilter(tests.Context(t), filterName) require.NoError(t, err) @@ -226,6 +238,9 @@ func TestFilters_PruneFilters(t *testing.T) { Name: "To keep", }, }, nil).Once() + orm.On("SelectSeqNums", mock.Anything).Return(map[int64]int64{ + 2: 25, + }, nil).Once() orm.On("DeleteFilters", mock.Anything, map[int64]Filter{toDelete.ID: toDelete}).Return(nil).Once() err := fs.PruneFilters(tests.Context(t)) require.NoError(t, err) @@ -246,6 +261,10 @@ func TestFilters_PruneFilters(t *testing.T) { Name: "To keep", }, }, nil).Once() + orm.EXPECT().SelectSeqNums(mock.Anything).Return(map[int64]int64{ + 1: 18, + 2: 25, + }, nil).Once() newToDelete := Filter{ ID: 3, Name: "To delete 2", @@ -291,6 +310,12 @@ func TestFilters_MatchingFilters(t *testing.T) { EventSig: expectedFilter1.EventSig, } orm.On("SelectFilters", mock.Anything).Return([]Filter{expectedFilter1, expectedFilter2, sameAddress, sameEventSig}, nil).Once() + orm.On("SelectSeqNums", mock.Anything).Return(map[int64]int64{ + 1: 18, + 2: 25, + 3: 14, + 4: 0, + }, nil) filters := newFilters(lggr, orm) err := filters.LoadFilters(tests.Context(t)) require.NoError(t, err) @@ -319,6 +344,10 @@ func TestFilters_GetFiltersToBackfill(t *testing.T) { Name: "notBackfilled", } orm.EXPECT().SelectFilters(mock.Anything).Return([]Filter{backfilledFilter, notBackfilled}, nil).Once() + orm.EXPECT().SelectSeqNums(mock.Anything).Return(map[int64]int64{ + 1: 18, + 2: 25, + }, nil) filters := newFilters(lggr, orm) err := filters.LoadFilters(tests.Context(t)) require.NoError(t, err) diff --git a/pkg/solana/logpoller/job.go b/pkg/solana/logpoller/job.go index 8e24d4165..bb336893c 100644 --- a/pkg/solana/logpoller/job.go +++ b/pkg/solana/logpoller/job.go @@ -122,8 +122,8 @@ func (j *getTransactionsFromBlockJob) Run(ctx context.Context) error { if block.BlockTime == nil { return fmt.Errorf("received block %d from rpc with missing block time", block.BlockHeight) - detail.blockTime = *block.BlockTime } + detail.blockTime = *block.BlockTime if len(block.Transactions) != len(blockSigsOnly.Signatures) { return fmt.Errorf("block %d has %d transactions but %d signatures", block.BlockHeight, len(block.Transactions), len(blockSigsOnly.Signatures)) diff --git a/pkg/solana/logpoller/loader_test.go b/pkg/solana/logpoller/loader_test.go index 4d3dcd8cc..2b057f7d1 100644 --- a/pkg/solana/logpoller/loader_test.go +++ b/pkg/solana/logpoller/loader_test.go @@ -3,7 +3,6 @@ package logpoller_test import ( "context" "crypto/rand" - "reflect" "sync" "sync/atomic" "testing" @@ -78,11 +77,13 @@ func TestEncodedLogCollector_ParseSingleEvent(t *testing.T) { GetBlockWithOpts(mock.Anything, mock.Anything, mock.Anything). RunAndReturn(func(_ context.Context, slot uint64, _ *rpc.GetBlockOpts) (*rpc.GetBlockResult, error) { height := slot - 1 + timeStamp := solana.UnixTimeSeconds(time.Now().Unix()) result := rpc.GetBlockResult{ Transactions: []rpc.TransactionWithMeta{}, Signatures: []solana.Signature{}, BlockHeight: &height, + BlockTime: &timeStamp, } _, _ = rand.Read(result.Blockhash[:]) @@ -132,6 +133,8 @@ func TestEncodedLogCollector_MultipleEventOrdered(t *testing.T) { hashes := make([]solana.Hash, len(slots)) scrambler := &slotUnsync{ch: make(chan struct{})} + timeStamp := solana.UnixTimeSeconds(time.Now().Unix()) + for idx := range len(sigs) { _, _ = rand.Read(sigs[idx][:]) _, _ = rand.Read(hashes[idx][:]) @@ -176,6 +179,7 @@ func TestEncodedLogCollector_MultipleEventOrdered(t *testing.T) { Transactions: []rpc.TransactionWithMeta{}, Signatures: []solana.Signature{}, BlockHeight: &height, + BlockTime: &timeStamp, }, nil } @@ -190,61 +194,72 @@ func TestEncodedLogCollector_MultipleEventOrdered(t *testing.T) { }, Signatures: []solana.Signature{sigs[slotIdx]}, BlockHeight: &height, + BlockTime: &timeStamp, }, nil }) tests.AssertEventually(t, func() bool { - return reflect.DeepEqual(parser.Events(), []logpoller.ProgramEvent{ - { - BlockData: logpoller.BlockData{ - SlotNumber: 41, - BlockHeight: 40, - BlockHash: hashes[3], - TransactionHash: sigs[3], - TransactionIndex: 0, - TransactionLogIndex: 0, - }, - Prefix: ">", - Data: "HDQnaQjSWwkNAAAASGVsbG8sIFdvcmxkISoAAAAAAAAA", + return len(parser.Events()) >= 4 + }) + + assert.Equal(t, []logpoller.ProgramEvent{ + { + BlockData: logpoller.BlockData{ + SlotNumber: 41, + BlockHeight: 40, + BlockTime: timeStamp, + BlockHash: hashes[3], + TransactionHash: sigs[3], + TransactionIndex: 0, + TransactionLogIndex: 0, }, - { - BlockData: logpoller.BlockData{ - SlotNumber: 42, - BlockHeight: 41, - BlockHash: hashes[2], - TransactionHash: sigs[2], - TransactionIndex: 0, - TransactionLogIndex: 0, - }, - Prefix: ">", - Data: "HDQnaQjSWwkNAAAASGVsbG8sIFdvcmxkISoAAAAAAAAA", + Prefix: ">", + Program: "J1zQwrBNBngz26jRPNWsUSZMHJwBwpkoDitXRV95LdK4", + Data: "HDQnaQjSWwkNAAAASGVsbG8sIFdvcmxkISoAAAAAAAAA", + }, + { + BlockData: logpoller.BlockData{ + SlotNumber: 42, + BlockHeight: 41, + BlockTime: timeStamp, + BlockHash: hashes[2], + TransactionHash: sigs[2], + TransactionIndex: 0, + TransactionLogIndex: 0, }, - { - BlockData: logpoller.BlockData{ - SlotNumber: 43, - BlockHeight: 42, - BlockHash: hashes[1], - TransactionHash: sigs[1], - TransactionIndex: 0, - TransactionLogIndex: 0, - }, - Prefix: ">", - Data: "HDQnaQjSWwkNAAAASGVsbG8sIFdvcmxkISoAAAAAAAAA", + Prefix: ">", + Program: "J1zQwrBNBngz26jRPNWsUSZMHJwBwpkoDitXRV95LdK4", + Data: "HDQnaQjSWwkNAAAASGVsbG8sIFdvcmxkISoAAAAAAAAA", + }, + { + BlockData: logpoller.BlockData{ + SlotNumber: 43, + BlockHeight: 42, + BlockTime: timeStamp, + BlockHash: hashes[1], + TransactionHash: sigs[1], + TransactionIndex: 0, + TransactionLogIndex: 0, }, - { - BlockData: logpoller.BlockData{ - SlotNumber: 44, - BlockHeight: 43, - BlockHash: hashes[0], - TransactionHash: sigs[0], - TransactionIndex: 0, - TransactionLogIndex: 0, - }, - Prefix: ">", - Data: "HDQnaQjSWwkNAAAASGVsbG8sIFdvcmxkISoAAAAAAAAA", + Prefix: ">", + Program: "J1zQwrBNBngz26jRPNWsUSZMHJwBwpkoDitXRV95LdK4", + Data: "HDQnaQjSWwkNAAAASGVsbG8sIFdvcmxkISoAAAAAAAAA", + }, + { + BlockData: logpoller.BlockData{ + SlotNumber: 44, + BlockHeight: 43, + BlockTime: timeStamp, + BlockHash: hashes[0], + TransactionHash: sigs[0], + TransactionIndex: 0, + TransactionLogIndex: 0, }, - }) - }) + Prefix: ">", + Program: "J1zQwrBNBngz26jRPNWsUSZMHJwBwpkoDitXRV95LdK4", + Data: "HDQnaQjSWwkNAAAASGVsbG8sIFdvcmxkISoAAAAAAAAA", + }, + }, parser.Events()) client.AssertExpectations(t) } @@ -337,12 +352,14 @@ func TestEncodedLogCollector_BackfillForAddress(t *testing.T) { } height := slot - 1 + timeStamp := solana.UnixTimeSeconds(time.Now().Unix()) if idx == -1 { return &rpc.GetBlockResult{ Transactions: []rpc.TransactionWithMeta{}, Signatures: []solana.Signature{}, BlockHeight: &height, + BlockTime: &timeStamp, }, nil } @@ -361,6 +378,7 @@ func TestEncodedLogCollector_BackfillForAddress(t *testing.T) { }, Signatures: []solana.Signature{sigs[idx*2], sigs[(idx*2)+1]}, BlockHeight: &height, + BlockTime: &timeStamp, }, nil }) diff --git a/pkg/solana/logpoller/models.go b/pkg/solana/logpoller/models.go index b1a1db3ac..8d1ec356a 100644 --- a/pkg/solana/logpoller/models.go +++ b/pkg/solana/logpoller/models.go @@ -1,9 +1,12 @@ package logpoller import ( + "encoding/base64" "time" "github.com/lib/pq" + + "github.com/smartcontractkit/chainlink-solana/pkg/solana/logpoller/utils" ) type Filter struct { @@ -26,6 +29,11 @@ func (f Filter) MatchSameLogs(other Filter) bool { f.EventIdl.Equal(other.EventIdl) && f.SubkeyPaths.Equal(other.SubkeyPaths) } +func (f Filter) Discriminator() string { + d := utils.Discriminator("event", f.Name) + return base64.StdEncoding.EncodeToString(d[:]) +} + type Log struct { ID int64 FilterID int64 From c5b9fe43ade2fb2558632192740ed7de71072b3e Mon Sep 17 00:00:00 2001 From: Domino Valdano <2644901+reductionista@users.noreply.github.com> Date: Tue, 7 Jan 2025 19:20:09 -0800 Subject: [PATCH 18/43] Remove mock_orm.go --- .../pkg/solana/logpoller/mock_orm.go | 442 ------------------ 1 file changed, 442 deletions(-) delete mode 100644 pkg/solana/logpoller/pkg/solana/logpoller/mock_orm.go diff --git a/pkg/solana/logpoller/pkg/solana/logpoller/mock_orm.go b/pkg/solana/logpoller/pkg/solana/logpoller/mock_orm.go deleted file mode 100644 index 1508ba4aa..000000000 --- a/pkg/solana/logpoller/pkg/solana/logpoller/mock_orm.go +++ /dev/null @@ -1,442 +0,0 @@ -// Code generated by mockery v2.43.2. DO NOT EDIT. - -package logpoller - -import ( - context "context" - - mock "github.com/stretchr/testify/mock" -) - -// mockORM is an autogenerated mock type for the ORM type -type mockORM struct { - mock.Mock -} - -type mockORM_Expecter struct { - mock *mock.Mock -} - -func (_m *mockORM) EXPECT() *mockORM_Expecter { - return &mockORM_Expecter{mock: &_m.Mock} -} - -// ChainID provides a mock function with given fields: -func (_m *mockORM) ChainID() string { - ret := _m.Called() - - if len(ret) == 0 { - panic("no return value specified for ChainID") - } - - var r0 string - if rf, ok := ret.Get(0).(func() string); ok { - r0 = rf() - } else { - r0 = ret.Get(0).(string) - } - - return r0 -} - -// mockORM_ChainID_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ChainID' -type mockORM_ChainID_Call struct { - *mock.Call -} - -// ChainID is a helper method to define mock.On call -func (_e *mockORM_Expecter) ChainID() *mockORM_ChainID_Call { - return &mockORM_ChainID_Call{Call: _e.mock.On("ChainID")} -} - -func (_c *mockORM_ChainID_Call) Run(run func()) *mockORM_ChainID_Call { - _c.Call.Run(func(args mock.Arguments) { - run() - }) - return _c -} - -func (_c *mockORM_ChainID_Call) Return(_a0 string) *mockORM_ChainID_Call { - _c.Call.Return(_a0) - return _c -} - -func (_c *mockORM_ChainID_Call) RunAndReturn(run func() string) *mockORM_ChainID_Call { - _c.Call.Return(run) - return _c -} - -// DeleteFilters provides a mock function with given fields: ctx, filters -func (_m *mockORM) DeleteFilters(ctx context.Context, filters map[int64]Filter) error { - ret := _m.Called(ctx, filters) - - if len(ret) == 0 { - panic("no return value specified for DeleteFilters") - } - - var r0 error - if rf, ok := ret.Get(0).(func(context.Context, map[int64]Filter) error); ok { - r0 = rf(ctx, filters) - } else { - r0 = ret.Error(0) - } - - return r0 -} - -// mockORM_DeleteFilters_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'DeleteFilters' -type mockORM_DeleteFilters_Call struct { - *mock.Call -} - -// DeleteFilters is a helper method to define mock.On call -// - ctx context.Context -// - filters map[int64]Filter -func (_e *mockORM_Expecter) DeleteFilters(ctx interface{}, filters interface{}) *mockORM_DeleteFilters_Call { - return &mockORM_DeleteFilters_Call{Call: _e.mock.On("DeleteFilters", ctx, filters)} -} - -func (_c *mockORM_DeleteFilters_Call) Run(run func(ctx context.Context, filters map[int64]Filter)) *mockORM_DeleteFilters_Call { - _c.Call.Run(func(args mock.Arguments) { - run(args[0].(context.Context), args[1].(map[int64]Filter)) - }) - return _c -} - -func (_c *mockORM_DeleteFilters_Call) Return(_a0 error) *mockORM_DeleteFilters_Call { - _c.Call.Return(_a0) - return _c -} - -func (_c *mockORM_DeleteFilters_Call) RunAndReturn(run func(context.Context, map[int64]Filter) error) *mockORM_DeleteFilters_Call { - _c.Call.Return(run) - return _c -} - -// InsertFilter provides a mock function with given fields: ctx, filter -func (_m *mockORM) InsertFilter(ctx context.Context, filter Filter) (int64, error) { - ret := _m.Called(ctx, filter) - - if len(ret) == 0 { - panic("no return value specified for InsertFilter") - } - - var r0 int64 - var r1 error - if rf, ok := ret.Get(0).(func(context.Context, Filter) (int64, error)); ok { - return rf(ctx, filter) - } - if rf, ok := ret.Get(0).(func(context.Context, Filter) int64); ok { - r0 = rf(ctx, filter) - } else { - r0 = ret.Get(0).(int64) - } - - if rf, ok := ret.Get(1).(func(context.Context, Filter) error); ok { - r1 = rf(ctx, filter) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// mockORM_InsertFilter_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'InsertFilter' -type mockORM_InsertFilter_Call struct { - *mock.Call -} - -// InsertFilter is a helper method to define mock.On call -// - ctx context.Context -// - filter Filter -func (_e *mockORM_Expecter) InsertFilter(ctx interface{}, filter interface{}) *mockORM_InsertFilter_Call { - return &mockORM_InsertFilter_Call{Call: _e.mock.On("InsertFilter", ctx, filter)} -} - -func (_c *mockORM_InsertFilter_Call) Run(run func(ctx context.Context, filter Filter)) *mockORM_InsertFilter_Call { - _c.Call.Run(func(args mock.Arguments) { - run(args[0].(context.Context), args[1].(Filter)) - }) - return _c -} - -func (_c *mockORM_InsertFilter_Call) Return(id int64, err error) *mockORM_InsertFilter_Call { - _c.Call.Return(id, err) - return _c -} - -func (_c *mockORM_InsertFilter_Call) RunAndReturn(run func(context.Context, Filter) (int64, error)) *mockORM_InsertFilter_Call { - _c.Call.Return(run) - return _c -} - -// InsertLogs provides a mock function with given fields: _a0, _a1 -func (_m *mockORM) InsertLogs(_a0 context.Context, _a1 []Log) error { - ret := _m.Called(_a0, _a1) - - if len(ret) == 0 { - panic("no return value specified for InsertLogs") - } - - var r0 error - if rf, ok := ret.Get(0).(func(context.Context, []Log) error); ok { - r0 = rf(_a0, _a1) - } else { - r0 = ret.Error(0) - } - - return r0 -} - -// mockORM_InsertLogs_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'InsertLogs' -type mockORM_InsertLogs_Call struct { - *mock.Call -} - -// InsertLogs is a helper method to define mock.On call -// - _a0 context.Context -// - _a1 []Log -func (_e *mockORM_Expecter) InsertLogs(_a0 interface{}, _a1 interface{}) *mockORM_InsertLogs_Call { - return &mockORM_InsertLogs_Call{Call: _e.mock.On("InsertLogs", _a0, _a1)} -} - -func (_c *mockORM_InsertLogs_Call) Run(run func(_a0 context.Context, _a1 []Log)) *mockORM_InsertLogs_Call { - _c.Call.Run(func(args mock.Arguments) { - run(args[0].(context.Context), args[1].([]Log)) - }) - return _c -} - -func (_c *mockORM_InsertLogs_Call) Return(err error) *mockORM_InsertLogs_Call { - _c.Call.Return(err) - return _c -} - -func (_c *mockORM_InsertLogs_Call) RunAndReturn(run func(context.Context, []Log) error) *mockORM_InsertLogs_Call { - _c.Call.Return(run) - return _c -} - -// MarkFilterBackfilled provides a mock function with given fields: ctx, id -func (_m *mockORM) MarkFilterBackfilled(ctx context.Context, id int64) error { - ret := _m.Called(ctx, id) - - if len(ret) == 0 { - panic("no return value specified for MarkFilterBackfilled") - } - - var r0 error - if rf, ok := ret.Get(0).(func(context.Context, int64) error); ok { - r0 = rf(ctx, id) - } else { - r0 = ret.Error(0) - } - - return r0 -} - -// mockORM_MarkFilterBackfilled_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'MarkFilterBackfilled' -type mockORM_MarkFilterBackfilled_Call struct { - *mock.Call -} - -// MarkFilterBackfilled is a helper method to define mock.On call -// - ctx context.Context -// - id int64 -func (_e *mockORM_Expecter) MarkFilterBackfilled(ctx interface{}, id interface{}) *mockORM_MarkFilterBackfilled_Call { - return &mockORM_MarkFilterBackfilled_Call{Call: _e.mock.On("MarkFilterBackfilled", ctx, id)} -} - -func (_c *mockORM_MarkFilterBackfilled_Call) Run(run func(ctx context.Context, id int64)) *mockORM_MarkFilterBackfilled_Call { - _c.Call.Run(func(args mock.Arguments) { - run(args[0].(context.Context), args[1].(int64)) - }) - return _c -} - -func (_c *mockORM_MarkFilterBackfilled_Call) Return(err error) *mockORM_MarkFilterBackfilled_Call { - _c.Call.Return(err) - return _c -} - -func (_c *mockORM_MarkFilterBackfilled_Call) RunAndReturn(run func(context.Context, int64) error) *mockORM_MarkFilterBackfilled_Call { - _c.Call.Return(run) - return _c -} - -// MarkFilterDeleted provides a mock function with given fields: ctx, id -func (_m *mockORM) MarkFilterDeleted(ctx context.Context, id int64) error { - ret := _m.Called(ctx, id) - - if len(ret) == 0 { - panic("no return value specified for MarkFilterDeleted") - } - - var r0 error - if rf, ok := ret.Get(0).(func(context.Context, int64) error); ok { - r0 = rf(ctx, id) - } else { - r0 = ret.Error(0) - } - - return r0 -} - -// mockORM_MarkFilterDeleted_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'MarkFilterDeleted' -type mockORM_MarkFilterDeleted_Call struct { - *mock.Call -} - -// MarkFilterDeleted is a helper method to define mock.On call -// - ctx context.Context -// - id int64 -func (_e *mockORM_Expecter) MarkFilterDeleted(ctx interface{}, id interface{}) *mockORM_MarkFilterDeleted_Call { - return &mockORM_MarkFilterDeleted_Call{Call: _e.mock.On("MarkFilterDeleted", ctx, id)} -} - -func (_c *mockORM_MarkFilterDeleted_Call) Run(run func(ctx context.Context, id int64)) *mockORM_MarkFilterDeleted_Call { - _c.Call.Run(func(args mock.Arguments) { - run(args[0].(context.Context), args[1].(int64)) - }) - return _c -} - -func (_c *mockORM_MarkFilterDeleted_Call) Return(err error) *mockORM_MarkFilterDeleted_Call { - _c.Call.Return(err) - return _c -} - -func (_c *mockORM_MarkFilterDeleted_Call) RunAndReturn(run func(context.Context, int64) error) *mockORM_MarkFilterDeleted_Call { - _c.Call.Return(run) - return _c -} - -// SelectFilters provides a mock function with given fields: ctx -func (_m *mockORM) SelectFilters(ctx context.Context) ([]Filter, error) { - ret := _m.Called(ctx) - - if len(ret) == 0 { - panic("no return value specified for SelectFilters") - } - - var r0 []Filter - var r1 error - if rf, ok := ret.Get(0).(func(context.Context) ([]Filter, error)); ok { - return rf(ctx) - } - if rf, ok := ret.Get(0).(func(context.Context) []Filter); ok { - r0 = rf(ctx) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).([]Filter) - } - } - - if rf, ok := ret.Get(1).(func(context.Context) error); ok { - r1 = rf(ctx) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// mockORM_SelectFilters_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SelectFilters' -type mockORM_SelectFilters_Call struct { - *mock.Call -} - -// SelectFilters is a helper method to define mock.On call -// - ctx context.Context -func (_e *mockORM_Expecter) SelectFilters(ctx interface{}) *mockORM_SelectFilters_Call { - return &mockORM_SelectFilters_Call{Call: _e.mock.On("SelectFilters", ctx)} -} - -func (_c *mockORM_SelectFilters_Call) Run(run func(ctx context.Context)) *mockORM_SelectFilters_Call { - _c.Call.Run(func(args mock.Arguments) { - run(args[0].(context.Context)) - }) - return _c -} - -func (_c *mockORM_SelectFilters_Call) Return(_a0 []Filter, _a1 error) *mockORM_SelectFilters_Call { - _c.Call.Return(_a0, _a1) - return _c -} - -func (_c *mockORM_SelectFilters_Call) RunAndReturn(run func(context.Context) ([]Filter, error)) *mockORM_SelectFilters_Call { - _c.Call.Return(run) - return _c -} - -// SelectSeqNums provides a mock function with given fields: ctx -func (_m *mockORM) SelectSeqNums(ctx context.Context) (map[int64]int64, error) { - ret := _m.Called(ctx) - - if len(ret) == 0 { - panic("no return value specified for SelectSeqNums") - } - - var r0 map[int64]int64 - var r1 error - if rf, ok := ret.Get(0).(func(context.Context) (map[int64]int64, error)); ok { - return rf(ctx) - } - if rf, ok := ret.Get(0).(func(context.Context) map[int64]int64); ok { - r0 = rf(ctx) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(map[int64]int64) - } - } - - if rf, ok := ret.Get(1).(func(context.Context) error); ok { - r1 = rf(ctx) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// mockORM_SelectSeqNums_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SelectSeqNums' -type mockORM_SelectSeqNums_Call struct { - *mock.Call -} - -// SelectSeqNums is a helper method to define mock.On call -// - ctx context.Context -func (_e *mockORM_Expecter) SelectSeqNums(ctx interface{}) *mockORM_SelectSeqNums_Call { - return &mockORM_SelectSeqNums_Call{Call: _e.mock.On("SelectSeqNums", ctx)} -} - -func (_c *mockORM_SelectSeqNums_Call) Run(run func(ctx context.Context)) *mockORM_SelectSeqNums_Call { - _c.Call.Run(func(args mock.Arguments) { - run(args[0].(context.Context)) - }) - return _c -} - -func (_c *mockORM_SelectSeqNums_Call) Return(_a0 map[int64]int64, _a1 error) *mockORM_SelectSeqNums_Call { - _c.Call.Return(_a0, _a1) - return _c -} - -func (_c *mockORM_SelectSeqNums_Call) RunAndReturn(run func(context.Context) (map[int64]int64, error)) *mockORM_SelectSeqNums_Call { - _c.Call.Return(run) - return _c -} - -// newMockORM creates a new instance of mockORM. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. -// The first argument is typically a *testing.T value. -func newMockORM(t interface { - mock.TestingT - Cleanup(func()) -}) *mockORM { - mock := &mockORM{} - mock.Mock.Test(t) - - t.Cleanup(func() { mock.AssertExpectations(t) }) - - return mock -} From 4f7f21a6290abbb638931d7d08062c797bfad6b9 Mon Sep 17 00:00:00 2001 From: Domino Valdano <2644901+reductionista@users.noreply.github.com> Date: Tue, 7 Jan 2025 19:50:48 -0800 Subject: [PATCH 19/43] Use UTC time, and fix lints --- pkg/solana/logpoller/log_poller.go | 23 ++++++++++++++++------- pkg/solana/logpoller/types.go | 11 +++++------ 2 files changed, 21 insertions(+), 13 deletions(-) diff --git a/pkg/solana/logpoller/log_poller.go b/pkg/solana/logpoller/log_poller.go index e454fdc77..516ed417e 100644 --- a/pkg/solana/logpoller/log_poller.go +++ b/pkg/solana/logpoller/log_poller.go @@ -9,6 +9,7 @@ import ( "time" bin "github.com/gagliardetto/binary" + "github.com/lib/pq" "github.com/smartcontractkit/chainlink-common/pkg/logger" "github.com/smartcontractkit/chainlink-common/pkg/services" "github.com/smartcontractkit/chainlink-common/pkg/utils" @@ -120,31 +121,39 @@ func (lp *LogPoller) Process(programEvent ProgramEvent) (err error) { return err } - subKeyValues := make([]IndexedValue, 0, len(filter.SubkeyPaths)) + log.SubkeyValues = make(pq.ByteaArray, 0, len(filter.SubkeyPaths)) for _, path := range filter.SubkeyPaths { var subKeyVal any subKeyVal, err = lp.typeProvider.CreateType(filter.EventIdl.IdlEvent, filter.EventIdl.IdlTypeDefSlice, path) - bin.UnmarshalBorsh(&subKeyVal, log.Data) if err != nil { return err } - indexedVal, err := NewIndexedValue(subKeyVal) + err = bin.UnmarshalBorsh(&subKeyVal, log.Data) + if err != nil { + return err + } + indexedVal, err2 := NewIndexedValue(subKeyVal) + if err2 != nil { + return err2 + } + err = log.SubkeyValues.Scan(indexedVal) if err != nil { return err } - subKeyValues = append(subKeyValues, indexedVal) } log.SequenceNum = lp.filters.IncrementSeqNums(filter.ID) - expiresAt := time.Now() // TODO: account for possible discrepencies in time? Seems like retention should be passed directly to ORM - expiresAt.Add(filter.Retention) + expiresAt := time.Now().Add(filter.Retention).UTC() log.ExpiresAt = &expiresAt logs = append(logs, log) } - lp.orm.InsertLogs(ctx, logs) + err = lp.orm.InsertLogs(ctx, logs) + if err != nil { + return err + } return nil } diff --git a/pkg/solana/logpoller/types.go b/pkg/solana/logpoller/types.go index 938fb12f3..886f9470f 100644 --- a/pkg/solana/logpoller/types.go +++ b/pkg/solana/logpoller/types.go @@ -81,7 +81,7 @@ func (p SubkeyPaths) Value() (driver.Value, error) { } func (p *SubkeyPaths) Scan(src interface{}) error { - return scanJson("SubkeyPaths", p, src) + return scanJSON("SubkeyPaths", p, src) } func (p SubkeyPaths) Equal(o SubkeyPaths) bool { @@ -112,7 +112,7 @@ type EventIdl struct { } func (e *EventIdl) Scan(src interface{}) error { - return scanJson("EventIdl", e, src) + return scanJSON("EventIdl", e, src) } func (e EventIdl) Value() (driver.Value, error) { @@ -122,11 +122,11 @@ func (e EventIdl) Value() (driver.Value, error) { }) } -func (p EventIdl) Equal(o EventIdl) bool { - return reflect.DeepEqual(p, o) +func (e EventIdl) Equal(o EventIdl) bool { + return reflect.DeepEqual(e, o) } -func scanJson(name string, dest, src interface{}) error { +func scanJSON(name string, dest, src interface{}) error { var bSrc []byte switch src := src.(type) { case string: @@ -170,7 +170,6 @@ func (v *IndexedValue) FromFloat64(f float64) { return } v.FromUint64(math.MaxInt64 - math.Float64bits(f)) - return } func NewIndexedValue(typedVal any) (iVal IndexedValue, err error) { From 3443f937407455367689e542ee538e7afbc16047 Mon Sep 17 00:00:00 2001 From: Domino Valdano <2644901+reductionista@users.noreply.github.com> Date: Tue, 7 Jan 2025 20:48:50 -0800 Subject: [PATCH 20/43] Add tests - WIP --- pkg/solana/logpoller/log_poller.go | 1 + pkg/solana/logpoller/log_poller_test.go | 53 +++++++++++++++++++++++++ 2 files changed, 54 insertions(+) create mode 100644 pkg/solana/logpoller/log_poller_test.go diff --git a/pkg/solana/logpoller/log_poller.go b/pkg/solana/logpoller/log_poller.go index 516ed417e..62ac406d5 100644 --- a/pkg/solana/logpoller/log_poller.go +++ b/pkg/solana/logpoller/log_poller.go @@ -39,6 +39,7 @@ type ILogPoller interface { Close() error RegisterFilter(ctx context.Context, filter Filter) error UnregisterFilter(ctx context.Context, name string) error + Process(programEvent ProgramEvent) error } type LogPoller struct { diff --git a/pkg/solana/logpoller/log_poller_test.go b/pkg/solana/logpoller/log_poller_test.go new file mode 100644 index 000000000..f80fdc5aa --- /dev/null +++ b/pkg/solana/logpoller/log_poller_test.go @@ -0,0 +1,53 @@ +package logpoller + +import ( + "testing" + + "github.com/smartcontractkit/chainlink-common/pkg/logger" + "github.com/smartcontractkit/chainlink-common/pkg/utils" + "github.com/smartcontractkit/chainlink-common/pkg/utils/tests" + "github.com/stretchr/testify/mock" + + "github.com/smartcontractkit/chainlink-solana/pkg/solana/client" + clientmocks "github.com/smartcontractkit/chainlink-solana/pkg/solana/client/mocks" + "github.com/smartcontractkit/chainlink-solana/pkg/solana/codec" +) + +type mockTypeProvider struct{} + +func (tp mockTypeProvider) CreateType(eventIdl codec.IdlEvent, typedefSlice codec.IdlTypeDefSlice, subKeyPath []string) (any, error) { + return nil, nil +} + +func TestProcess(t *testing.T) { + ctx := tests.Context(t) + + addr := newRandomPublicKey(t) + sig := newRandomEventSignature(t) + + orm := newMockORM(t) + cl := clientmocks.NewReaderWriter(t) + loader := utils.NewLazyLoad(func() (client.Reader, error) { return cl, nil }) + lggr := logger.Sugared(logger.Test(t)) + tp := mockTypeProvider{} + lp := New(lggr, orm, loader, tp) + + filter := Filter{ + Name: "test filter", + Address: addr, + EventSig: sig, + } + orm.EXPECT().SelectFilters(mock.Anything).Return([]Filter{filter}, nil) + orm.EXPECT().SelectSeqNums(mock.Anything).Return(map[int64]int64{}, nil) + orm.EXPECT().InsertFilter(mock.Anything, mock.Anything).Return(1, nil) + lp.RegisterFilter(ctx, filter) + + ev := ProgramEvent{ + Program: "myprog", + Prefix: "prefix", + BlockData: BlockData{SlotNumber: 3, BlockHeight: 5}, + } + lp.Process(ev) + + lp.UnregisterFilter(ctx, filter.Name) +} From 45623f5823d829e05d8479d3b275a74c3d46d3a8 Mon Sep 17 00:00:00 2001 From: Domino Valdano <2644901+reductionista@users.noreply.github.com> Date: Wed, 8 Jan 2025 21:56:18 -0800 Subject: [PATCH 21/43] Refactor to use new codec api, based on review --- pkg/solana/codec/solana.go | 33 ++++++++++++++++++++++++ pkg/solana/logpoller/filters.go | 12 +++++++++ pkg/solana/logpoller/log_poller.go | 34 ++++++++++++------------- pkg/solana/logpoller/log_poller_test.go | 24 ++++++++--------- pkg/solana/logpoller/models.go | 24 +++++++++++++++-- pkg/solana/logpoller/types.go | 6 +++++ 6 files changed, 101 insertions(+), 32 deletions(-) diff --git a/pkg/solana/codec/solana.go b/pkg/solana/codec/solana.go index 3a19a1683..e372fb67a 100644 --- a/pkg/solana/codec/solana.go +++ b/pkg/solana/codec/solana.go @@ -489,3 +489,36 @@ func saveDependency(refs *codecRefs, parent, child string) { refs.dependencies[parent] = append(deps, child) } +func NewIDLEventCodec(idl IDL, builder commonencodings.Builder) (commontypes.RemoteCodec, error) { + typeCodecs := make(commonencodings.LenientCodecFromTypeCodec) + refs := &codecRefs{ + builder: builder, + codecs: make(map[string]commonencodings.TypeCodec), + typeDefs: idl.Types, + dependencies: make(map[string][]string), + } + + for _, event := range idl.Events { + name, instCodec, err := asStruct(eventFieldsAsStandardFields(event.Fields), refs, event.Name, false, false) + if err != nil { + return nil, err + } + + typeCodecs[name] = instCodec + } + + return typeCodecs, nil +} + +func eventFieldsAsStandardFields(event []IdlEventField) []IdlField { + output := make([]IdlField, len(event)) + + for idx := range output { + output[idx] = IdlField{ + Name: event[idx].Name, + Type: event[idx].Type, + } + } + + return output +} diff --git a/pkg/solana/logpoller/filters.go b/pkg/solana/logpoller/filters.go index 20f971ec2..be69aa96b 100644 --- a/pkg/solana/logpoller/filters.go +++ b/pkg/solana/logpoller/filters.go @@ -11,7 +11,10 @@ import ( "sync/atomic" "github.com/gagliardetto/solana-go" + "github.com/smartcontractkit/chainlink-common/pkg/codec/encodings/binary" "github.com/smartcontractkit/chainlink-common/pkg/logger" + + "github.com/smartcontractkit/chainlink-solana/pkg/solana/codec" ) type filters struct { @@ -110,6 +113,15 @@ func (fl *filters) RegisterFilter(ctx context.Context, filter Filter) error { filter.ID = filterID + idl := codec.IDL{ + Events: []codec.IdlEvent{filter.EventIdl.IdlEvent}, + Types: filter.EventIdl.IdlTypeDefSlice, + } + filter.decoder, err = codec.NewIDLEventCodec(idl, binary.LittleEndian()) + if err != nil { + return fmt.Errorf("failed to create event decoder: %w", err) + } + fl.filtersByName[filter.Name] = filter.ID fl.filtersByID[filter.ID] = &filter filtersForAddress, ok := fl.filtersByAddress[filter.Address] diff --git a/pkg/solana/logpoller/log_poller.go b/pkg/solana/logpoller/log_poller.go index 62ac406d5..183ea9f0e 100644 --- a/pkg/solana/logpoller/log_poller.go +++ b/pkg/solana/logpoller/log_poller.go @@ -8,7 +8,6 @@ import ( "math" "time" - bin "github.com/gagliardetto/binary" "github.com/lib/pq" "github.com/smartcontractkit/chainlink-common/pkg/logger" "github.com/smartcontractkit/chainlink-common/pkg/services" @@ -56,13 +55,12 @@ type LogPoller struct { typeProvider EventTypeProvider } -func New(lggr logger.SugaredLogger, orm ORM, cl internal.Loader[client.Reader], typeProvider EventTypeProvider) ILogPoller { +func New(lggr logger.SugaredLogger, orm ORM, cl internal.Loader[client.Reader]) ILogPoller { lggr = logger.Sugared(logger.Named(lggr, "LogPoller")) lp := &LogPoller{ - orm: orm, - client: cl, - filters: newFilters(lggr, orm), - typeProvider: typeProvider, + orm: orm, + client: cl, + filters: newFilters(lggr, orm), } lp.Service, lp.eng = services.Config{ @@ -103,15 +101,20 @@ func (lp *LogPoller) Process(programEvent ProgramEvent) (err error) { blockData := programEvent.BlockData + matchingFilters := lp.filters.MatchingFiltersForEncodedEvent(programEvent) + if matchingFilters == nil { + return + } + var logs []Log - for filter := range lp.filters.MatchingFiltersForEncodedEvent(programEvent) { + for filter := range matchingFilters { log := Log{ FilterID: filter.ID, ChainID: lp.orm.ChainID(), LogIndex: makeLogIndex(blockData.TransactionIndex, blockData.TransactionLogIndex), BlockHash: Hash(blockData.BlockHash), BlockNumber: int64(blockData.BlockHeight), - BlockTimestamp: blockData.BlockTime.Time(), // TODO: is this a timezone safe conversion? + BlockTimestamp: blockData.BlockTime.Time().UTC(), Address: filter.Address, EventSig: filter.EventSig, TxHash: Signature(blockData.TransactionHash), @@ -124,19 +127,14 @@ func (lp *LogPoller) Process(programEvent ProgramEvent) (err error) { log.SubkeyValues = make(pq.ByteaArray, 0, len(filter.SubkeyPaths)) for _, path := range filter.SubkeyPaths { - var subKeyVal any - subKeyVal, err = lp.typeProvider.CreateType(filter.EventIdl.IdlEvent, filter.EventIdl.IdlTypeDefSlice, path) - if err != nil { - return err - } - err = bin.UnmarshalBorsh(&subKeyVal, log.Data) - if err != nil { - return err - } - indexedVal, err2 := NewIndexedValue(subKeyVal) + subKeyVal, err2 := filter.DecodeSubKey(ctx, log.Data, path) if err2 != nil { return err2 } + indexedVal, err3 := NewIndexedValue(subKeyVal) + if err3 != nil { + return err3 + } err = log.SubkeyValues.Scan(indexedVal) if err != nil { return err diff --git a/pkg/solana/logpoller/log_poller_test.go b/pkg/solana/logpoller/log_poller_test.go index f80fdc5aa..91ae80796 100644 --- a/pkg/solana/logpoller/log_poller_test.go +++ b/pkg/solana/logpoller/log_poller_test.go @@ -1,24 +1,19 @@ package logpoller import ( + "context" "testing" "github.com/smartcontractkit/chainlink-common/pkg/logger" "github.com/smartcontractkit/chainlink-common/pkg/utils" "github.com/smartcontractkit/chainlink-common/pkg/utils/tests" "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" "github.com/smartcontractkit/chainlink-solana/pkg/solana/client" clientmocks "github.com/smartcontractkit/chainlink-solana/pkg/solana/client/mocks" - "github.com/smartcontractkit/chainlink-solana/pkg/solana/codec" ) -type mockTypeProvider struct{} - -func (tp mockTypeProvider) CreateType(eventIdl codec.IdlEvent, typedefSlice codec.IdlTypeDefSlice, subKeyPath []string) (any, error) { - return nil, nil -} - func TestProcess(t *testing.T) { ctx := tests.Context(t) @@ -29,8 +24,7 @@ func TestProcess(t *testing.T) { cl := clientmocks.NewReaderWriter(t) loader := utils.NewLazyLoad(func() (client.Reader, error) { return cl, nil }) lggr := logger.Sugared(logger.Test(t)) - tp := mockTypeProvider{} - lp := New(lggr, orm, loader, tp) + lp := New(lggr, orm, loader) filter := Filter{ Name: "test filter", @@ -39,15 +33,21 @@ func TestProcess(t *testing.T) { } orm.EXPECT().SelectFilters(mock.Anything).Return([]Filter{filter}, nil) orm.EXPECT().SelectSeqNums(mock.Anything).Return(map[int64]int64{}, nil) - orm.EXPECT().InsertFilter(mock.Anything, mock.Anything).Return(1, nil) + orm.EXPECT().InsertFilter(mock.Anything, mock.Anything).RunAndReturn(func(ctx context.Context, f Filter) (int64, error) { + require.Equal(t, f, filter) + return 1, nil + }).Once() + // TODO orm.EXPECT().InsertLogs(mock.Anything, mock.Anything).RunAndReturn(nil) validate logs written properly lp.RegisterFilter(ctx, filter) ev := ProgramEvent{ - Program: "myprog", + Program: "myprog", // TODO: fix program address, so filters match Prefix: "prefix", BlockData: BlockData{SlotNumber: 3, BlockHeight: 5}, } - lp.Process(ev) + err := lp.Process(ev) + require.NoError(t, err) + orm.EXPECT().MarkFilterDeleted(mock.Anything, mock.Anything).Return(nil).Once() lp.UnregisterFilter(ctx, filter.Name) } diff --git a/pkg/solana/logpoller/models.go b/pkg/solana/logpoller/models.go index 8d1ec356a..a9597304e 100644 --- a/pkg/solana/logpoller/models.go +++ b/pkg/solana/logpoller/models.go @@ -1,7 +1,10 @@ package logpoller import ( + "context" "encoding/base64" + "slices" + "strings" "time" "github.com/lib/pq" @@ -20,8 +23,9 @@ type Filter struct { SubkeyPaths SubkeyPaths Retention time.Duration MaxLogsKept int64 - IsDeleted bool // only for internal usage. Values set externally are ignored. - IsBackfilled bool // only for internal usage. Values set externally are ignored. + IsDeleted bool // only for internal usage. Values set externally are ignored. + IsBackfilled bool // only for internal usage. Values set externally are ignored. + decoder Decoder // only for internal usage. } func (f Filter) MatchSameLogs(other Filter) bool { @@ -34,6 +38,22 @@ func (f Filter) Discriminator() string { return base64.StdEncoding.EncodeToString(d[:]) } +func (f *Filter) CreateType(subKeyPath string) (any, error) { + itemType := strings.Join([]string{f.Name, subKeyPath}, ".") + return f.decoder.CreateType(itemType, false) // TODO: what does bool represent? pass true or false? +} + +func (f *Filter) DecodeSubKey(ctx context.Context, raw []byte, subKeyPath []string) (any, error) { + itemType := strings.Join(slices.Concat([]string{f.Name}, subKeyPath), ".") + + val, err := f.decoder.CreateType(itemType, false) + if err != nil { + return nil, err + } + err = f.decoder.Decode(ctx, raw, val, itemType) + return val, err +} + type Log struct { ID int64 FilterID int64 diff --git a/pkg/solana/logpoller/types.go b/pkg/solana/logpoller/types.go index 886f9470f..7c7e2ed1a 100644 --- a/pkg/solana/logpoller/types.go +++ b/pkg/solana/logpoller/types.go @@ -1,6 +1,7 @@ package logpoller import ( + "context" "database/sql/driver" "encoding/binary" "encoding/json" @@ -106,6 +107,11 @@ type EventTypeProvider interface { CreateType(eventIdl codec.IdlEvent, typedefSlice codec.IdlTypeDefSlice, subKeyPath []string) (any, error) } +type Decoder interface { + CreateType(itemType string, _ bool) (any, error) + Decode(_ context.Context, raw []byte, into any, itemType string) error +} + type EventIdl struct { codec.IdlEvent codec.IdlTypeDefSlice From 0cc5cfb1643777b928f91ac5c0ecf2cebe2f37cd Mon Sep 17 00:00:00 2001 From: Domino Valdano <2644901+reductionista@users.noreply.github.com> Date: Thu, 9 Jan 2025 18:54:09 -0800 Subject: [PATCH 22/43] Add SeqNum test and fix other tests Decoder had to be moved from Filter into a separate filters map, otherwise it caused a lot of issues with assert.Equal for comparing filters. --- pkg/solana/logpoller/filters.go | 23 +++++++++++++++++++--- pkg/solana/logpoller/log_poller.go | 2 +- pkg/solana/logpoller/models.go | 24 ++--------------------- pkg/solana/logpoller/orm.go | 23 ++++++++++++++++++---- pkg/solana/logpoller/orm_test.go | 31 ++++++++++++++++++++++-------- 5 files changed, 65 insertions(+), 38 deletions(-) diff --git a/pkg/solana/logpoller/filters.go b/pkg/solana/logpoller/filters.go index be69aa96b..0d6629a39 100644 --- a/pkg/solana/logpoller/filters.go +++ b/pkg/solana/logpoller/filters.go @@ -7,6 +7,8 @@ import ( "fmt" "iter" "maps" + "slices" + "strings" "sync" "sync/atomic" @@ -31,12 +33,14 @@ type filters struct { knownPrograms map[string]uint // fast lookup to see if a base58-encoded ProgramID matches any registered filters knownDiscriminators map[string]uint // fast lookup by first 10 characters (60-bits) of a base64-encoded discriminator seqNums map[int64]int64 + decoders map[int64]Decoder } func newFilters(lggr logger.SugaredLogger, orm ORM) *filters { return &filters{ - orm: orm, - lggr: lggr, + orm: orm, + lggr: lggr, + decoders: make(map[int64]Decoder), } } @@ -117,7 +121,7 @@ func (fl *filters) RegisterFilter(ctx context.Context, filter Filter) error { Events: []codec.IdlEvent{filter.EventIdl.IdlEvent}, Types: filter.EventIdl.IdlTypeDefSlice, } - filter.decoder, err = codec.NewIDLEventCodec(idl, binary.LittleEndian()) + fl.decoders[filter.ID], err = codec.NewIDLEventCodec(idl, binary.LittleEndian()) if err != nil { return fmt.Errorf("failed to create event decoder: %w", err) } @@ -410,3 +414,16 @@ func (fl *filters) LoadFilters(ctx context.Context) error { return nil } + +func (fl *filters) DecodeSubKey(ctx context.Context, raw []byte, ID int64, subKeyPath []string) (any, error) { + eventName := fl.filtersByID[ID].EventName + decoder := fl.decoders[ID] + itemType := strings.Join(slices.Concat([]string{eventName}, subKeyPath), ".") + + val, err := decoder.CreateType(itemType, false) + if err != nil { + return nil, err + } + err = decoder.Decode(ctx, raw, val, itemType) + return val, err +} diff --git a/pkg/solana/logpoller/log_poller.go b/pkg/solana/logpoller/log_poller.go index 183ea9f0e..65b6c40ff 100644 --- a/pkg/solana/logpoller/log_poller.go +++ b/pkg/solana/logpoller/log_poller.go @@ -127,7 +127,7 @@ func (lp *LogPoller) Process(programEvent ProgramEvent) (err error) { log.SubkeyValues = make(pq.ByteaArray, 0, len(filter.SubkeyPaths)) for _, path := range filter.SubkeyPaths { - subKeyVal, err2 := filter.DecodeSubKey(ctx, log.Data, path) + subKeyVal, err2 := lp.filters.DecodeSubKey(ctx, log.Data, filter.ID, path) if err2 != nil { return err2 } diff --git a/pkg/solana/logpoller/models.go b/pkg/solana/logpoller/models.go index a9597304e..8d1ec356a 100644 --- a/pkg/solana/logpoller/models.go +++ b/pkg/solana/logpoller/models.go @@ -1,10 +1,7 @@ package logpoller import ( - "context" "encoding/base64" - "slices" - "strings" "time" "github.com/lib/pq" @@ -23,9 +20,8 @@ type Filter struct { SubkeyPaths SubkeyPaths Retention time.Duration MaxLogsKept int64 - IsDeleted bool // only for internal usage. Values set externally are ignored. - IsBackfilled bool // only for internal usage. Values set externally are ignored. - decoder Decoder // only for internal usage. + IsDeleted bool // only for internal usage. Values set externally are ignored. + IsBackfilled bool // only for internal usage. Values set externally are ignored. } func (f Filter) MatchSameLogs(other Filter) bool { @@ -38,22 +34,6 @@ func (f Filter) Discriminator() string { return base64.StdEncoding.EncodeToString(d[:]) } -func (f *Filter) CreateType(subKeyPath string) (any, error) { - itemType := strings.Join([]string{f.Name, subKeyPath}, ".") - return f.decoder.CreateType(itemType, false) // TODO: what does bool represent? pass true or false? -} - -func (f *Filter) DecodeSubKey(ctx context.Context, raw []byte, subKeyPath []string) (any, error) { - itemType := strings.Join(slices.Concat([]string{f.Name}, subKeyPath), ".") - - val, err := f.decoder.CreateType(itemType, false) - if err != nil { - return nil, err - } - err = f.decoder.Decode(ctx, raw, val, itemType) - return val, err -} - type Log struct { ID int64 FilterID int64 diff --git a/pkg/solana/logpoller/orm.go b/pkg/solana/logpoller/orm.go index 1504beea7..ae6e9118e 100644 --- a/pkg/solana/logpoller/orm.go +++ b/pkg/solana/logpoller/orm.go @@ -152,7 +152,7 @@ func (o *DSORM) insertLogsWithinTx(ctx context.Context, logs []Log, tx sqlutil.D (:filter_id, :chain_id, :log_index, :block_hash, :block_number, :block_timestamp, :address, :event_sig, :subkey_values, :tx_hash, :data, NOW(), :expires_at, :sequence_num) ON CONFLICT DO NOTHING` - _, err := tx.NamedExecContext(ctx, query, logs[start:end]) + res, err := tx.NamedExecContext(ctx, query, logs[start:end]) if err != nil { if errors.Is(err, context.DeadlineExceeded) && batchInsertSize > 500 { // In case of DB timeouts, try to insert again with a smaller batch upto a limit @@ -162,6 +162,14 @@ func (o *DSORM) insertLogsWithinTx(ctx context.Context, logs []Log, tx sqlutil.D } return err } + numRows, err := res.RowsAffected() + if err == nil { + if numRows != int64(len(logs)) { + // This probably just means we're trying to insert the same log twice, but could also be an indication + // of other constraint violations + o.lggr.Debugf("attempted to insert %d logs, but could only insert %d", len(logs), numRows) + } + } } return nil } @@ -231,11 +239,18 @@ func (o *DSORM) FilteredLogs(ctx context.Context, filter []query.Expression, lim } func (o *DSORM) SelectSeqNums(ctx context.Context) (map[int64]int64, error) { - seqNums := make(map[int64]int64) - query := "SELECT id, MAX(sequence_num) FROM solana.logs WHERE chain_id=%s GROUP BY id" - err := o.ds.SelectContext(ctx, &seqNums, query, o.chainID) + results := make([]struct { + FilterID int64 + SequenceNum int64 + }, 0) + query := "SELECT filter_id, MAX(sequence_num) AS sequence_num FROM solana.logs WHERE chain_id=$1 GROUP BY filter_id" + err := o.ds.SelectContext(ctx, &results, query, o.chainID) if err != nil { return nil, err } + seqNums := make(map[int64]int64) + for _, row := range results { + seqNums[row.FilterID] = row.SequenceNum + } return seqNums, nil } diff --git a/pkg/solana/logpoller/orm_test.go b/pkg/solana/logpoller/orm_test.go index 272abc10e..7fab36467 100644 --- a/pkg/solana/logpoller/orm_test.go +++ b/pkg/solana/logpoller/orm_test.go @@ -3,6 +3,7 @@ package logpoller import ( + "math/rand" "testing" "time" @@ -32,7 +33,7 @@ func TestLogPollerFilters(t *testing.T) { EventName: "event", EventSig: EventSignature{1, 2, 3}, StartingBlock: 1, - EventIDL: "{}", + EventIdl: EventIdl{}, SubkeyPaths: SubkeyPaths([][]string{{"a", "b"}, {"c"}}), Retention: 1000, MaxLogsKept: 3, @@ -43,7 +44,6 @@ func TestLogPollerFilters(t *testing.T) { EventName: "event", EventSig: EventSignature{1, 2, 3}, StartingBlock: 1, - EventIDL: "{}", SubkeyPaths: SubkeyPaths([][]string{}), Retention: 1000, MaxLogsKept: 3, @@ -54,7 +54,6 @@ func TestLogPollerFilters(t *testing.T) { EventName: "event", EventSig: EventSignature{1, 2, 3}, StartingBlock: 1, - EventIDL: "{}", SubkeyPaths: nil, Retention: 1000, MaxLogsKept: 3, @@ -190,19 +189,35 @@ func TestLogPollerLogs(t *testing.T) { ctx := tests.Context(t) // create filter as it's required for a log filterID, err := orm.InsertFilter(ctx, newRandomFilter(t)) + filterID2, err := orm.InsertFilter(ctx, newRandomFilter(t)) require.NoError(t, err) log := newRandomLog(t, filterID, chainID) - err = orm.InsertLogs(ctx, []Log{log}) + log2 := newRandomLog(t, filterID2, chainID) + err = orm.InsertLogs(ctx, []Log{log, log2}) require.NoError(t, err) // insert of the same Log should not produce two instances err = orm.InsertLogs(ctx, []Log{log}) require.NoError(t, err) - dbLogs, err := orm.SelectLogs(ctx, 0, 100, log.Address, log.EventSig) + + dbLogs, err := orm.SelectLogs(ctx, 0, 1000000, log.Address, log.EventSig) require.NoError(t, err) require.Len(t, dbLogs, 1) log.ID = dbLogs[0].ID log.CreatedAt = dbLogs[0].CreatedAt require.Equal(t, log, dbLogs[0]) + + dbLogs, err = orm.SelectLogs(ctx, 0, 1000000, log2.Address, log2.EventSig) + require.NoError(t, err) + require.Len(t, dbLogs, 1) + log2.ID = dbLogs[0].ID + log2.CreatedAt = dbLogs[0].CreatedAt + require.Equal(t, log2, dbLogs[0]) + + t.Run("SelectSequenceNums", func(t *testing.T) { + seqNums, err := orm.SelectSeqNums(tests.Context(t)) + require.NoError(t, err) + require.Len(t, seqNums, 2) + }) } func newRandomFilter(t *testing.T) Filter { @@ -212,7 +227,6 @@ func newRandomFilter(t *testing.T) Filter { EventName: "event", EventSig: newRandomEventSignature(t), StartingBlock: 1, - EventIDL: "{}", SubkeyPaths: [][]string{{"a", "b"}, {"c"}}, Retention: 1000, MaxLogsKept: 3, @@ -229,14 +243,15 @@ func newRandomLog(t *testing.T, filterID int64, chainID string) Log { return Log{ FilterID: filterID, ChainID: chainID, - LogIndex: 1, + LogIndex: rand.Int63n(1000), BlockHash: Hash(pubKey), - BlockNumber: 10, + BlockNumber: rand.Int63n(1000000), BlockTimestamp: time.Unix(1731590113, 0), Address: PublicKey(pubKey), EventSig: EventSignature{3, 2, 1}, SubkeyValues: [][]byte{{3, 2, 1}, {1}, {1, 2}, pubKey.Bytes()}, TxHash: Signature(signature), Data: data, + SequenceNum: rand.Int63n(500), } } From 8498b80cc2fd0516da4914a31c84f77d197eeb74 Mon Sep 17 00:00:00 2001 From: Domino Valdano <2644901+reductionista@users.noreply.github.com> Date: Thu, 9 Jan 2025 18:56:52 -0800 Subject: [PATCH 23/43] Don't set ExpireAt unless Retention is set --- pkg/solana/logpoller/log_poller.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/pkg/solana/logpoller/log_poller.go b/pkg/solana/logpoller/log_poller.go index 65b6c40ff..8a49253c2 100644 --- a/pkg/solana/logpoller/log_poller.go +++ b/pkg/solana/logpoller/log_poller.go @@ -143,8 +143,10 @@ func (lp *LogPoller) Process(programEvent ProgramEvent) (err error) { log.SequenceNum = lp.filters.IncrementSeqNums(filter.ID) - expiresAt := time.Now().Add(filter.Retention).UTC() - log.ExpiresAt = &expiresAt + if filter.Retention > 0 { + expiresAt := time.Now().Add(filter.Retention).UTC() + log.ExpiresAt = &expiresAt + } logs = append(logs, log) } From 255d493f43515d2dd23a7a64e615216271c771a2 Mon Sep 17 00:00:00 2001 From: Domino Valdano <2644901+reductionista@users.noreply.github.com> Date: Thu, 9 Jan 2025 18:59:57 -0800 Subject: [PATCH 24/43] Move decoder creation before InsertFilter --- pkg/solana/logpoller/filters.go | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/pkg/solana/logpoller/filters.go b/pkg/solana/logpoller/filters.go index 0d6629a39..63daf07f2 100644 --- a/pkg/solana/logpoller/filters.go +++ b/pkg/solana/logpoller/filters.go @@ -110,6 +110,15 @@ func (fl *filters) RegisterFilter(ctx context.Context, filter Filter) error { fl.removeFilterFromIndexes(*existingFilter) } + idl := codec.IDL{ + Events: []codec.IdlEvent{filter.EventIdl.IdlEvent}, + Types: filter.EventIdl.IdlTypeDefSlice, + } + fl.decoders[filter.ID], err = codec.NewIDLEventCodec(idl, binary.LittleEndian()) + if err != nil { + return fmt.Errorf("failed to create event decoder: %w", err) + } + filterID, err := fl.orm.InsertFilter(ctx, filter) if err != nil { return fmt.Errorf("failed to insert filter: %w", err) From 49516463690deb8a124802f9d917823d01017eea Mon Sep 17 00:00:00 2001 From: Domino Valdano <2644901+reductionista@users.noreply.github.com> Date: Thu, 9 Jan 2025 21:16:57 -0800 Subject: [PATCH 25/43] Remove unused functions in anchor.go, add validation to TestProess test --- go.mod | 1 + pkg/solana/logpoller/filters.go | 11 +- pkg/solana/logpoller/log_poller_test.go | 64 +++++- pkg/solana/logpoller/types_test.go | 13 -- pkg/solana/logpoller/utils/anchor.go | 249 ------------------------ 5 files changed, 56 insertions(+), 282 deletions(-) diff --git a/go.mod b/go.mod index 6b58adcfe..70a31342a 100644 --- a/go.mod +++ b/go.mod @@ -20,6 +20,7 @@ require ( github.com/smartcontractkit/chainlink-common v0.4.1-0.20241223143929-db7919d60550 github.com/smartcontractkit/libocr v0.0.0-20241007185508-adbe57025f12 github.com/stretchr/testify v1.9.0 + github.com/test-go/testify v1.1.4 go.uber.org/zap v1.27.0 golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 golang.org/x/sync v0.10.0 diff --git a/pkg/solana/logpoller/filters.go b/pkg/solana/logpoller/filters.go index 63daf07f2..30405f9e9 100644 --- a/pkg/solana/logpoller/filters.go +++ b/pkg/solana/logpoller/filters.go @@ -126,15 +126,6 @@ func (fl *filters) RegisterFilter(ctx context.Context, filter Filter) error { filter.ID = filterID - idl := codec.IDL{ - Events: []codec.IdlEvent{filter.EventIdl.IdlEvent}, - Types: filter.EventIdl.IdlTypeDefSlice, - } - fl.decoders[filter.ID], err = codec.NewIDLEventCodec(idl, binary.LittleEndian()) - if err != nil { - return fmt.Errorf("failed to create event decoder: %w", err) - } - fl.filtersByName[filter.Name] = filter.ID fl.filtersByID[filter.ID] = &filter filtersForAddress, ok := fl.filtersByAddress[filter.Address] @@ -162,7 +153,7 @@ func (fl *filters) RegisterFilter(ctx context.Context, filter Filter) error { } discriminatorHead := filter.Discriminator()[:10] - if _, ok := fl.knownPrograms[programID]; !ok { + if _, ok := fl.knownDiscriminators[discriminatorHead]; !ok { fl.knownDiscriminators[discriminatorHead] = 1 } else { fl.knownDiscriminators[discriminatorHead]++ diff --git a/pkg/solana/logpoller/log_poller_test.go b/pkg/solana/logpoller/log_poller_test.go index 91ae80796..06ebfcbd7 100644 --- a/pkg/solana/logpoller/log_poller_test.go +++ b/pkg/solana/logpoller/log_poller_test.go @@ -2,50 +2,94 @@ package logpoller import ( "context" + "encoding/base64" + "math/rand" "testing" + bin "github.com/gagliardetto/binary" + "github.com/gagliardetto/solana-go" + "github.com/google/uuid" "github.com/smartcontractkit/chainlink-common/pkg/logger" - "github.com/smartcontractkit/chainlink-common/pkg/utils" + commonutils "github.com/smartcontractkit/chainlink-common/pkg/utils" "github.com/smartcontractkit/chainlink-common/pkg/utils/tests" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" "github.com/smartcontractkit/chainlink-solana/pkg/solana/client" clientmocks "github.com/smartcontractkit/chainlink-solana/pkg/solana/client/mocks" + "github.com/smartcontractkit/chainlink-solana/pkg/solana/logpoller/utils" ) func TestProcess(t *testing.T) { ctx := tests.Context(t) addr := newRandomPublicKey(t) - sig := newRandomEventSignature(t) + eventName := "myEvent" + eventSig := utils.Discriminator("event", eventName) + + filterID := rand.Int63() + chainID := uuid.NewString() + + txIndex := int(rand.Int31()) + txLogIndex := uint(rand.Uint32()) + + expectedLog := newRandomLog(t, filterID, chainID, eventName) + + expectedLog.LogIndex = makeLogIndex(txIndex, txLogIndex) orm := newMockORM(t) cl := clientmocks.NewReaderWriter(t) - loader := utils.NewLazyLoad(func() (client.Reader, error) { return cl, nil }) + loader := commonutils.NewLazyLoad(func() (client.Reader, error) { return cl, nil }) lggr := logger.Sugared(logger.Test(t)) lp := New(lggr, orm, loader) filter := Filter{ Name: "test filter", Address: addr, - EventSig: sig, + EventSig: eventSig, } orm.EXPECT().SelectFilters(mock.Anything).Return([]Filter{filter}, nil) orm.EXPECT().SelectSeqNums(mock.Anything).Return(map[int64]int64{}, nil) orm.EXPECT().InsertFilter(mock.Anything, mock.Anything).RunAndReturn(func(ctx context.Context, f Filter) (int64, error) { require.Equal(t, f, filter) - return 1, nil + return filterID, nil }).Once() - // TODO orm.EXPECT().InsertLogs(mock.Anything, mock.Anything).RunAndReturn(nil) validate logs written properly + + orm.EXPECT().InsertLogs(mock.Anything, mock.Anything).RunAndReturn(func(ctx context.Context, logs []Log) error { + require.Len(t, logs, 1) + log := logs[0] + assert.Equal(t, log, expectedLog) + return nil + }) lp.RegisterFilter(ctx, filter) + event := struct { + a int + b string + }{55, "hello"} + + data, err := bin.MarshalBorsh(&event) + require.NoError(t, err) + data = append(eventSig[:], data...) + + require.NoError(t, err) + ev := ProgramEvent{ - Program: "myprog", // TODO: fix program address, so filters match - Prefix: "prefix", - BlockData: BlockData{SlotNumber: 3, BlockHeight: 5}, + Program: addr.ToSolana().String(), + Prefix: ">", + BlockData: BlockData{ + SlotNumber: 3, + BlockHeight: 5, + BlockHash: solana.HashFromBytes([]byte{1, 2, 3}), + BlockTime: solana.UnixTimeSeconds(expectedLog.BlockTimestamp.Unix()), + TransactionHash: expectedLog.TxHash.ToSolana(), + TransactionIndex: txIndex, + TransactionLogIndex: txLogIndex, + }, + Data: base64.StdEncoding.EncodeToString(data), } - err := lp.Process(ev) + err = lp.Process(ev) require.NoError(t, err) orm.EXPECT().MarkFilterDeleted(mock.Anything, mock.Anything).Return(nil).Once() diff --git a/pkg/solana/logpoller/types_test.go b/pkg/solana/logpoller/types_test.go index c60ca4794..f21a8d61d 100644 --- a/pkg/solana/logpoller/types_test.go +++ b/pkg/solana/logpoller/types_test.go @@ -3,23 +3,10 @@ package logpoller import ( "testing" - "github.com/gagliardetto/solana-go" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) -func newRandomPublicKey(t *testing.T) PublicKey { - privateKey, err := solana.NewRandomPrivateKey() - require.NoError(t, err) - pubKey := privateKey.PublicKey() - return PublicKey(pubKey) -} - -func newRandomEventSignature(t *testing.T) EventSignature { - pubKey := newRandomPublicKey(t) - return EventSignature(pubKey[:8]) -} - func TestIndexedValue(t *testing.T) { cases := []struct { typeName string diff --git a/pkg/solana/logpoller/utils/anchor.go b/pkg/solana/logpoller/utils/anchor.go index b042fb67d..5dc954337 100644 --- a/pkg/solana/logpoller/utils/anchor.go +++ b/pkg/solana/logpoller/utils/anchor.go @@ -3,54 +3,13 @@ package utils import ( "bytes" "context" - "crypto/rand" "crypto/sha256" - "encoding/base64" - "encoding/binary" "fmt" - "regexp" - "strconv" - "strings" - "testing" - "time" - bin "github.com/gagliardetto/binary" "github.com/gagliardetto/solana-go" "github.com/gagliardetto/solana-go/rpc" - - "github.com/stretchr/testify/require" ) -var ZeroAddress = [32]byte{} - -func MakeRandom32ByteArray() [32]byte { - a := make([]byte, 32) - if _, err := rand.Read(a); err != nil { - panic(err) // should never panic but check in case - } - return [32]byte(a) -} - -func Uint64ToLE(chain uint64) []byte { - chainLE := make([]byte, 8) - binary.LittleEndian.PutUint64(chainLE, chain) - return chainLE -} - -func To28BytesLE(value uint64) [28]byte { - le := make([]byte, 28) - binary.LittleEndian.PutUint64(le, value) - return [28]byte(le) -} - -func Map[T, V any](ts []T, fn func(T) V) []V { - result := make([]V, len(ts)) - for i, t := range ts { - result[i] = fn(t) - } - return result -} - const DiscriminatorLength = 8 func Discriminator(namespace, name string) [DiscriminatorLength]byte { @@ -59,39 +18,6 @@ func Discriminator(namespace, name string) [DiscriminatorLength]byte { return [DiscriminatorLength]byte(h.Sum(nil)[:DiscriminatorLength]) } -func FundAccounts(ctx context.Context, accounts []solana.PrivateKey, solanaGoClient *rpc.Client, t *testing.T) { - sigs := []solana.Signature{} - for _, v := range accounts { - sig, err := solanaGoClient.RequestAirdrop(ctx, v.PublicKey(), 1000*solana.LAMPORTS_PER_SOL, rpc.CommitmentFinalized) - require.NoError(t, err) - sigs = append(sigs, sig) - } - - // wait for confirmation so later transactions don't fail - remaining := len(sigs) - count := 0 - for remaining > 0 { - count++ - statusRes, sigErr := solanaGoClient.GetSignatureStatuses(ctx, true, sigs...) - require.NoError(t, sigErr) - require.NotNil(t, statusRes) - require.NotNil(t, statusRes.Value) - - unconfirmedTxCount := 0 - for _, res := range statusRes.Value { - if res == nil || res.ConfirmationStatus == rpc.ConfirmationStatusProcessed || res.ConfirmationStatus == rpc.ConfirmationStatusConfirmed { - unconfirmedTxCount++ - } - } - remaining = unconfirmedTxCount - - time.Sleep(500 * time.Millisecond) - if count > 60 { - require.NoError(t, fmt.Errorf("unable to find transaction within timeout")) - } - } -} - func IsEvent(event string, data []byte) bool { if len(data) < 8 { return false @@ -100,181 +26,6 @@ func IsEvent(event string, data []byte) bool { return bytes.Equal(d[:], data[:8]) } -func ParseEvent(logs []string, event string, obj interface{}, print ...bool) error { - for _, v := range logs { - if strings.Contains(v, "Program data:") { - encodedData := strings.TrimSpace(strings.TrimPrefix(v, "Program data:")) - data, err := base64.StdEncoding.DecodeString(encodedData) - if err != nil { - return err - } - if IsEvent(event, data) { - if err := bin.UnmarshalBorsh(obj, data); err != nil { - return err - } - - if len(print) > 0 && print[0] { - fmt.Printf("%s: %+v\n", event, obj) - } - return nil - } - } - } - return fmt.Errorf("%s: event not found", event) -} - -func ParseMultipleEvents[T any](logs []string, event string, print bool) ([]T, error) { - var results []T - for _, v := range logs { - if strings.Contains(v, "Program data:") { - encodedData := strings.TrimSpace(strings.TrimPrefix(v, "Program data:")) - data, err := base64.StdEncoding.DecodeString(encodedData) - if err != nil { - return nil, err - } - if IsEvent(event, data) { - var obj T - if err := bin.UnmarshalBorsh(&obj, data); err != nil { - return nil, err - } - - if print { - fmt.Printf("%s: %+v\n", event, obj) - } - - results = append(results, obj) - } - } - } - if len(results) == 0 { - return nil, fmt.Errorf("%s: event not found", event) - } - - return results, nil -} - -type AnchorInstruction struct { - Name string - ProgramID string - Logs []string - ComputeUnits int - InnerCalls []*AnchorInstruction -} - -// Parses the log messages from an Anchor program and returns a list of AnchorInstructions. -func ParseLogMessages(logMessages []string) []*AnchorInstruction { - var instructions []*AnchorInstruction - var stack []*AnchorInstruction - var currentInstruction *AnchorInstruction - - programInvokeRegex := regexp.MustCompile(`Program (\w+) invoke`) - programSuccessRegex := regexp.MustCompile(`Program (\w+) success`) - computeUnitsRegex := regexp.MustCompile(`Program (\w+) consumed (\d+) of \d+ compute units`) - - for _, line := range logMessages { - line = strings.TrimSpace(line) - - // Program invocation - push to stack - if match := programInvokeRegex.FindStringSubmatch(line); len(match) > 1 { - newInstruction := &AnchorInstruction{ - ProgramID: match[1], - Name: "", - Logs: []string{}, - ComputeUnits: 0, - InnerCalls: []*AnchorInstruction{}, - } - - if len(stack) == 0 { - instructions = append(instructions, newInstruction) - } else { - stack[len(stack)-1].InnerCalls = append(stack[len(stack)-1].InnerCalls, newInstruction) - } - - stack = append(stack, newInstruction) - currentInstruction = newInstruction - continue - } - - // Program success - pop from stack - if match := programSuccessRegex.FindStringSubmatch(line); len(match) > 1 { - if len(stack) > 0 { - stack = stack[:len(stack)-1] // pop - if len(stack) > 0 { - currentInstruction = stack[len(stack)-1] - } else { - currentInstruction = nil - } - } - continue - } - - // Instruction name - if strings.Contains(line, "Instruction:") { - if currentInstruction != nil { - currentInstruction.Name = strings.TrimSpace(strings.Split(line, "Instruction:")[1]) - } - continue - } - - // Program logs - if strings.HasPrefix(line, "Program log:") { - if currentInstruction != nil { - logMessage := strings.TrimSpace(strings.TrimPrefix(line, "Program log:")) - currentInstruction.Logs = append(currentInstruction.Logs, logMessage) - } - continue - } - - // Compute units - if match := computeUnitsRegex.FindStringSubmatch(line); len(match) > 1 { - programID := match[1] - computeUnits, _ := strconv.Atoi(match[2]) - - // Find the instruction in the stack that matches this program ID - for i := len(stack) - 1; i >= 0; i-- { - if stack[i].ProgramID == programID { - stack[i].ComputeUnits = computeUnits - break - } - } - } - } - - return instructions -} - -// Pretty prints the given Anchor instructions. -// Example usage: -// parsed := utils.ParseLogMessages(result.Meta.LogMessages) -// output := utils.PrintInstructions(parsed) -// t.Logf("Parsed Instructions: %s", output) -func PrintInstructions(instructions []*AnchorInstruction) string { - var output strings.Builder - - var printInstruction func(*AnchorInstruction, int, string) - printInstruction = func(instruction *AnchorInstruction, index int, indent string) { - output.WriteString(fmt.Sprintf("%sInstruction %d: %s\n", indent, index, instruction.Name)) - output.WriteString(fmt.Sprintf("%s Program ID: %s\n", indent, instruction.ProgramID)) - output.WriteString(fmt.Sprintf("%s Compute Units: %d\n", indent, instruction.ComputeUnits)) - output.WriteString(fmt.Sprintf("%s Logs:\n", indent)) - for _, log := range instruction.Logs { - output.WriteString(fmt.Sprintf("%s %s\n", indent, log)) - } - if len(instruction.InnerCalls) > 0 { - output.WriteString(fmt.Sprintf("%s Inner Calls:\n", indent)) - for i, innerCall := range instruction.InnerCalls { - printInstruction(innerCall, i+1, indent+" ") - } - } - } - - for i, instruction := range instructions { - printInstruction(instruction, i+1, "") - } - - return output.String() -} - func GetBlockTime(ctx context.Context, client *rpc.Client, commitment rpc.CommitmentType) (*solana.UnixTimeSeconds, error) { block, err := client.GetBlockHeight(ctx, commitment) if err != nil { From a6d70850cb04de30205f612bc9903fe4433f84a5 Mon Sep 17 00:00:00 2001 From: Domino Valdano <2644901+reductionista@users.noreply.github.com> Date: Fri, 10 Jan 2025 09:32:20 -0800 Subject: [PATCH 26/43] Fix lints --- pkg/solana/chain.go | 2 +- pkg/solana/logpoller/log_poller_test.go | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/pkg/solana/chain.go b/pkg/solana/chain.go index e8c321d0d..6f9850449 100644 --- a/pkg/solana/chain.go +++ b/pkg/solana/chain.go @@ -318,7 +318,7 @@ func newChain(id string, cfg *config.TOMLConfig, ks core.Keystore, lggr logger.L } // TODO: import typeProvider function from codec package and pass to constructor - ch.lp = logpoller.New(logger.Sugared(logger.Named(lggr, "LogPoller")), logpoller.NewORM(ch.ID(), ds, lggr), lc, nil) + ch.lp = logpoller.New(logger.Sugared(logger.Named(lggr, "LogPoller")), logpoller.NewORM(ch.ID(), ds, lggr), lc) ch.txm = txm.NewTxm(ch.id, tc, sendTx, cfg, ks, lggr) ch.balanceMonitor = monitor.NewBalanceMonitor(ch.id, cfg, lggr, ks, bc) return &ch, nil diff --git a/pkg/solana/logpoller/log_poller_test.go b/pkg/solana/logpoller/log_poller_test.go index 06ebfcbd7..28e099d31 100644 --- a/pkg/solana/logpoller/log_poller_test.go +++ b/pkg/solana/logpoller/log_poller_test.go @@ -62,7 +62,8 @@ func TestProcess(t *testing.T) { assert.Equal(t, log, expectedLog) return nil }) - lp.RegisterFilter(ctx, filter) + err := lp.RegisterFilter(ctx, filter) + require.NoError(t, err) event := struct { a int @@ -93,5 +94,6 @@ func TestProcess(t *testing.T) { require.NoError(t, err) orm.EXPECT().MarkFilterDeleted(mock.Anything, mock.Anything).Return(nil).Once() - lp.UnregisterFilter(ctx, filter.Name) + err = lp.UnregisterFilter(ctx, filter.Name) + require.NoError(t, err) } From ab24226fe2a23c839cff710c70d0808f4d53a0cd Mon Sep 17 00:00:00 2001 From: Domino Valdano <2644901+reductionista@users.noreply.github.com> Date: Fri, 10 Jan 2025 10:45:41 -0800 Subject: [PATCH 27/43] Remove lp.typeProvider, generate EventIdl for test event --- pkg/solana/logpoller/log_poller.go | 9 ++--- pkg/solana/logpoller/log_poller_test.go | 39 ++++++++++++++++---- pkg/solana/logpoller/models.go | 2 +- pkg/solana/logpoller/test_helpers.go | 47 +++++++++++++++++++++++++ pkg/solana/logpoller/types.go | 4 --- 5 files changed, 83 insertions(+), 18 deletions(-) create mode 100644 pkg/solana/logpoller/test_helpers.go diff --git a/pkg/solana/logpoller/log_poller.go b/pkg/solana/logpoller/log_poller.go index 8a49253c2..7cc4986b2 100644 --- a/pkg/solana/logpoller/log_poller.go +++ b/pkg/solana/logpoller/log_poller.go @@ -18,8 +18,7 @@ import ( ) var ( - ErrFilterNameConflict = errors.New("filter with such name already exists") - ErrMissingEventTypeProvider = errors.New("cannot start LogPoller without EventTypeProvider") + ErrFilterNameConflict = errors.New("filter with such name already exists") ) type ORM interface { @@ -51,8 +50,7 @@ type LogPoller struct { client internal.Loader[client.Reader] collector *EncodedLogCollector - filters *filters - typeProvider EventTypeProvider + filters *filters } func New(lggr logger.SugaredLogger, orm ORM, cl internal.Loader[client.Reader]) ILogPoller { @@ -73,9 +71,6 @@ func New(lggr logger.SugaredLogger, orm ORM, cl internal.Loader[client.Reader]) } func (lp *LogPoller) start(context.Context) error { - if lp.typeProvider == nil { - return ErrMissingEventTypeProvider - } cl, err := lp.client.Get() if err != nil { return err diff --git a/pkg/solana/logpoller/log_poller_test.go b/pkg/solana/logpoller/log_poller_test.go index 28e099d31..2e8a23360 100644 --- a/pkg/solana/logpoller/log_poller_test.go +++ b/pkg/solana/logpoller/log_poller_test.go @@ -3,6 +3,7 @@ package logpoller import ( "context" "encoding/base64" + "encoding/json" "math/rand" "testing" @@ -18,6 +19,7 @@ import ( "github.com/smartcontractkit/chainlink-solana/pkg/solana/client" clientmocks "github.com/smartcontractkit/chainlink-solana/pkg/solana/client/mocks" + "github.com/smartcontractkit/chainlink-solana/pkg/solana/codec" "github.com/smartcontractkit/chainlink-solana/pkg/solana/logpoller/utils" ) @@ -44,10 +46,35 @@ func TestProcess(t *testing.T) { lggr := logger.Sugared(logger.Test(t)) lp := New(lggr, orm, loader) + var idlTypeInt64 codec.IdlType + var idlTypeString codec.IdlType + + err := json.Unmarshal([]byte("\"i64\""), &idlTypeInt64) + require.NoError(t, err) + err = json.Unmarshal([]byte("\"string\""), &idlTypeString) + require.NoError(t, err) + + idl := EventIdl{ + codec.IdlEvent{ + Name: "myEvent", + Fields: []codec.IdlEventField{{ + Name: "A", + Type: idlTypeInt64, + }, { + Name: "B", + Type: idlTypeString, + }}, + }, + []codec.IdlTypeDef{}, + } + filter := Filter{ - Name: "test filter", - Address: addr, - EventSig: eventSig, + Name: "test filter", + EventName: eventName, + Address: addr, + EventSig: eventSig, + EventIdl: idl, + SubkeyPaths: [][]string{{"A"}, {"B"}}, } orm.EXPECT().SelectFilters(mock.Anything).Return([]Filter{filter}, nil) orm.EXPECT().SelectSeqNums(mock.Anything).Return(map[int64]int64{}, nil) @@ -62,12 +89,12 @@ func TestProcess(t *testing.T) { assert.Equal(t, log, expectedLog) return nil }) - err := lp.RegisterFilter(ctx, filter) + err = lp.RegisterFilter(ctx, filter) require.NoError(t, err) event := struct { - a int - b string + A int64 + B string }{55, "hello"} data, err := bin.MarshalBorsh(&event) diff --git a/pkg/solana/logpoller/models.go b/pkg/solana/logpoller/models.go index 8d1ec356a..a8cf45034 100644 --- a/pkg/solana/logpoller/models.go +++ b/pkg/solana/logpoller/models.go @@ -30,7 +30,7 @@ func (f Filter) MatchSameLogs(other Filter) bool { } func (f Filter) Discriminator() string { - d := utils.Discriminator("event", f.Name) + d := utils.Discriminator("event", f.EventName) return base64.StdEncoding.EncodeToString(d[:]) } diff --git a/pkg/solana/logpoller/test_helpers.go b/pkg/solana/logpoller/test_helpers.go new file mode 100644 index 000000000..b1689a601 --- /dev/null +++ b/pkg/solana/logpoller/test_helpers.go @@ -0,0 +1,47 @@ +package logpoller + +import ( + "math/rand" + "testing" + "time" + + "github.com/gagliardetto/solana-go" + "github.com/stretchr/testify/require" + + "github.com/smartcontractkit/chainlink-solana/pkg/solana/logpoller/utils" +) + +func newRandomPublicKey(t *testing.T) PublicKey { + privateKey, err := solana.NewRandomPrivateKey() + require.NoError(t, err) + pubKey := privateKey.PublicKey() + return PublicKey(pubKey) +} + +func newRandomEventSignature(t *testing.T) EventSignature { + pubKey := newRandomPublicKey(t) + return EventSignature(pubKey[:8]) +} + +func newRandomLog(t *testing.T, filterID int64, chainID string, eventName string) Log { + privateKey, err := solana.NewRandomPrivateKey() + require.NoError(t, err) + pubKey := privateKey.PublicKey() + data := []byte("solana is fun") + signature, err := privateKey.Sign(data) + require.NoError(t, err) + return Log{ + FilterID: filterID, + ChainID: chainID, + LogIndex: rand.Int63n(1000), + BlockHash: Hash(pubKey), + BlockNumber: rand.Int63n(1000000), + BlockTimestamp: time.Unix(1731590113, 0), + Address: PublicKey(pubKey), + EventSig: utils.Discriminator("event", eventName), + SubkeyValues: [][]byte{{3, 2, 1}, {1}, {1, 2}, pubKey.Bytes()}, + TxHash: Signature(signature), + Data: data, + SequenceNum: rand.Int63n(500), + } +} diff --git a/pkg/solana/logpoller/types.go b/pkg/solana/logpoller/types.go index 7c7e2ed1a..7517c3ae8 100644 --- a/pkg/solana/logpoller/types.go +++ b/pkg/solana/logpoller/types.go @@ -103,10 +103,6 @@ func (s EventSignature) Value() (driver.Value, error) { return s[:], nil } -type EventTypeProvider interface { - CreateType(eventIdl codec.IdlEvent, typedefSlice codec.IdlTypeDefSlice, subKeyPath []string) (any, error) -} - type Decoder interface { CreateType(itemType string, _ bool) (any, error) Decode(_ context.Context, raw []byte, into any, itemType string) error From 40d80045fffd6f7d645b08147cb0d7d489944ee9 Mon Sep 17 00:00:00 2001 From: Domino Valdano <2644901+reductionista@users.noreply.github.com> Date: Mon, 13 Jan 2025 13:46:32 -0800 Subject: [PATCH 28/43] Remove unused Prefix field from ProgramEvent --- pkg/solana/logpoller/loader_test.go | 4 ---- pkg/solana/logpoller/log_data_parser.go | 4 +--- pkg/solana/logpoller/log_poller_test.go | 1 - 3 files changed, 1 insertion(+), 8 deletions(-) diff --git a/pkg/solana/logpoller/loader_test.go b/pkg/solana/logpoller/loader_test.go index 2b057f7d1..9eb2482bd 100644 --- a/pkg/solana/logpoller/loader_test.go +++ b/pkg/solana/logpoller/loader_test.go @@ -213,7 +213,6 @@ func TestEncodedLogCollector_MultipleEventOrdered(t *testing.T) { TransactionIndex: 0, TransactionLogIndex: 0, }, - Prefix: ">", Program: "J1zQwrBNBngz26jRPNWsUSZMHJwBwpkoDitXRV95LdK4", Data: "HDQnaQjSWwkNAAAASGVsbG8sIFdvcmxkISoAAAAAAAAA", }, @@ -227,7 +226,6 @@ func TestEncodedLogCollector_MultipleEventOrdered(t *testing.T) { TransactionIndex: 0, TransactionLogIndex: 0, }, - Prefix: ">", Program: "J1zQwrBNBngz26jRPNWsUSZMHJwBwpkoDitXRV95LdK4", Data: "HDQnaQjSWwkNAAAASGVsbG8sIFdvcmxkISoAAAAAAAAA", }, @@ -241,7 +239,6 @@ func TestEncodedLogCollector_MultipleEventOrdered(t *testing.T) { TransactionIndex: 0, TransactionLogIndex: 0, }, - Prefix: ">", Program: "J1zQwrBNBngz26jRPNWsUSZMHJwBwpkoDitXRV95LdK4", Data: "HDQnaQjSWwkNAAAASGVsbG8sIFdvcmxkISoAAAAAAAAA", }, @@ -255,7 +252,6 @@ func TestEncodedLogCollector_MultipleEventOrdered(t *testing.T) { TransactionIndex: 0, TransactionLogIndex: 0, }, - Prefix: ">", Program: "J1zQwrBNBngz26jRPNWsUSZMHJwBwpkoDitXRV95LdK4", Data: "HDQnaQjSWwkNAAAASGVsbG8sIFdvcmxkISoAAAAAAAAA", }, diff --git a/pkg/solana/logpoller/log_data_parser.go b/pkg/solana/logpoller/log_data_parser.go index 2549c40bc..ca3fb79e9 100644 --- a/pkg/solana/logpoller/log_data_parser.go +++ b/pkg/solana/logpoller/log_data_parser.go @@ -34,8 +34,7 @@ type ProgramLog struct { type ProgramEvent struct { Program string BlockData - Prefix string - Data string + Data string } type ProgramOutput struct { @@ -81,7 +80,6 @@ func parseProgramLogs(logs []string) []ProgramOutput { if len(dataMatches) > 1 { instLogs[lastLogIdx].Events = append(instLogs[lastLogIdx].Events, ProgramEvent{ Program: instLogs[lastLogIdx].Program, - Prefix: prefixBuilder(depth), Data: dataMatches[1], }) } diff --git a/pkg/solana/logpoller/log_poller_test.go b/pkg/solana/logpoller/log_poller_test.go index 2e8a23360..fbbbf94c4 100644 --- a/pkg/solana/logpoller/log_poller_test.go +++ b/pkg/solana/logpoller/log_poller_test.go @@ -105,7 +105,6 @@ func TestProcess(t *testing.T) { ev := ProgramEvent{ Program: addr.ToSolana().String(), - Prefix: ">", BlockData: BlockData{ SlotNumber: 3, BlockHeight: 5, From b552a226808664e3b421a32eebf8e317d2ffe614 Mon Sep 17 00:00:00 2001 From: Domino Valdano <2644901+reductionista@users.noreply.github.com> Date: Mon, 13 Jan 2025 11:03:08 -0800 Subject: [PATCH 29/43] Fix decoding issues --- go.mod | 3 +- pkg/solana/logpoller/filters.go | 39 ++++++++++++++++--------- pkg/solana/logpoller/log_poller.go | 19 +++++++----- pkg/solana/logpoller/log_poller_test.go | 7 ++--- pkg/solana/logpoller/models.go | 7 +++-- pkg/solana/logpoller/test_helpers.go | 2 +- pkg/solana/logpoller/types.go | 24 ++++++++------- 7 files changed, 60 insertions(+), 41 deletions(-) diff --git a/go.mod b/go.mod index 70a31342a..a068a37e0 100644 --- a/go.mod +++ b/go.mod @@ -14,13 +14,11 @@ require ( github.com/hashicorp/go-plugin v1.6.2 github.com/jackc/pgx/v4 v4.18.3 github.com/jpillora/backoff v1.0.0 - github.com/lib/pq v1.10.9 github.com/pelletier/go-toml/v2 v2.2.0 github.com/prometheus/client_golang v1.17.0 github.com/smartcontractkit/chainlink-common v0.4.1-0.20241223143929-db7919d60550 github.com/smartcontractkit/libocr v0.0.0-20241007185508-adbe57025f12 github.com/stretchr/testify v1.9.0 - github.com/test-go/testify v1.1.4 go.uber.org/zap v1.27.0 golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 golang.org/x/sync v0.10.0 @@ -77,6 +75,7 @@ require ( github.com/kr/pretty v0.3.1 // indirect github.com/kr/text v0.2.0 // indirect github.com/leodido/go-urn v1.2.0 // indirect + github.com/lib/pq v1.10.9 // indirect github.com/linkedin/goavro/v2 v2.12.0 // indirect github.com/logrusorgru/aurora v2.0.3+incompatible // indirect github.com/mailru/easyjson v0.7.7 // indirect diff --git a/pkg/solana/logpoller/filters.go b/pkg/solana/logpoller/filters.go index 30405f9e9..b16865f57 100644 --- a/pkg/solana/logpoller/filters.go +++ b/pkg/solana/logpoller/filters.go @@ -110,6 +110,13 @@ func (fl *filters) RegisterFilter(ctx context.Context, filter Filter) error { fl.removeFilterFromIndexes(*existingFilter) } + filterID, err := fl.orm.InsertFilter(ctx, filter) + if err != nil { + return fmt.Errorf("failed to insert filter: %w", err) + } + + filter.ID = filterID + idl := codec.IDL{ Events: []codec.IdlEvent{filter.EventIdl.IdlEvent}, Types: filter.EventIdl.IdlTypeDefSlice, @@ -119,13 +126,6 @@ func (fl *filters) RegisterFilter(ctx context.Context, filter Filter) error { return fmt.Errorf("failed to create event decoder: %w", err) } - filterID, err := fl.orm.InsertFilter(ctx, filter) - if err != nil { - return fmt.Errorf("failed to insert filter: %w", err) - } - - filter.ID = filterID - fl.filtersByName[filter.Name] = filter.ID fl.filtersByID[filter.ID] = &filter filtersForAddress, ok := fl.filtersByAddress[filter.Address] @@ -279,6 +279,10 @@ func (fl *filters) MatchingFiltersForEncodedEvent(event ProgramEvent) iter.Seq[F return nil } + if len(event.Data) < 12 { + return nil + } + // The first 64-bits of the event data is the event sig. Because it's base64 encoded, this corresponds to // the first 10 characters plus 4 bits of the 11th character. We can quickly rule it out as not matching any known // discriminators if the first 10 characters don't match. If it passes that initial test, we base64-decode the @@ -293,8 +297,11 @@ func (fl *filters) MatchingFiltersForEncodedEvent(event ProgramEvent) iter.Seq[F fl.lggr.Errorw("failed to parse Program ID for event", "EventProgram", event) return nil } - decoded, err := base64.StdEncoding.DecodeString(event.Data[:11]) - if err != nil { + + // Decoding first 12 characters will give us the first 9 bytes of binary data + // The first 8 of those is the discriminator + decoded, err := base64.StdEncoding.DecodeString(event.Data[:12]) + if err != nil || len(decoded) < 8 { fl.lggr.Errorw("failed to decode event data", "EventProgram", event) return nil } @@ -416,12 +423,18 @@ func (fl *filters) LoadFilters(ctx context.Context) error { } func (fl *filters) DecodeSubKey(ctx context.Context, raw []byte, ID int64, subKeyPath []string) (any, error) { - eventName := fl.filtersByID[ID].EventName - decoder := fl.decoders[ID] - itemType := strings.Join(slices.Concat([]string{eventName}, subKeyPath), ".") + filter, ok := fl.filtersByID[ID] + if !ok { + return nil, fmt.Errorf("filter %d not found", ID) + } + decoder, ok := fl.decoders[ID] + if !ok { + return nil, fmt.Errorf("decoder %d not found", ID) + } + itemType := strings.Join(slices.Concat([]string{filter.EventName}, subKeyPath), ".") val, err := decoder.CreateType(itemType, false) - if err != nil { + if err != nil || val == nil { return nil, err } err = decoder.Decode(ctx, raw, val, itemType) diff --git a/pkg/solana/logpoller/log_poller.go b/pkg/solana/logpoller/log_poller.go index 7cc4986b2..6c145b13c 100644 --- a/pkg/solana/logpoller/log_poller.go +++ b/pkg/solana/logpoller/log_poller.go @@ -8,7 +8,6 @@ import ( "math" "time" - "github.com/lib/pq" "github.com/smartcontractkit/chainlink-common/pkg/logger" "github.com/smartcontractkit/chainlink-common/pkg/services" "github.com/smartcontractkit/chainlink-common/pkg/utils" @@ -18,7 +17,8 @@ import ( ) var ( - ErrFilterNameConflict = errors.New("filter with such name already exists") + ErrFilterNameConflict = errors.New("filter with such name already exists") + ErrMissingDiscriminator = errors.New("Solana log is missing discriminator") ) type ORM interface { @@ -115,12 +115,18 @@ func (lp *LogPoller) Process(programEvent ProgramEvent) (err error) { TxHash: Signature(blockData.TransactionHash), } - log.Data, err = base64.StdEncoding.DecodeString(programEvent.Data) + eventData, err := base64.StdEncoding.DecodeString(programEvent.Data) if err != nil { return err } + if len(eventData) < 8 { + err = fmt.Errorf("Assumption violation: %w, log.Data=%s", ErrMissingDiscriminator, log.Data) + lp.lggr.Criticalw(err.Error()) + return err + } + log.Data = eventData[8:] - log.SubkeyValues = make(pq.ByteaArray, 0, len(filter.SubkeyPaths)) + log.SubkeyValues = make([]IndexedValue, 0, len(filter.SubkeyPaths)) for _, path := range filter.SubkeyPaths { subKeyVal, err2 := lp.filters.DecodeSubKey(ctx, log.Data, filter.ID, path) if err2 != nil { @@ -130,10 +136,7 @@ func (lp *LogPoller) Process(programEvent ProgramEvent) (err error) { if err3 != nil { return err3 } - err = log.SubkeyValues.Scan(indexedVal) - if err != nil { - return err - } + log.SubkeyValues = append(log.SubkeyValues, indexedVal) } log.SequenceNum = lp.filters.IncrementSeqNums(filter.ID) diff --git a/pkg/solana/logpoller/log_poller_test.go b/pkg/solana/logpoller/log_poller_test.go index fbbbf94c4..1c8b07eb1 100644 --- a/pkg/solana/logpoller/log_poller_test.go +++ b/pkg/solana/logpoller/log_poller_test.go @@ -76,8 +76,9 @@ func TestProcess(t *testing.T) { EventIdl: idl, SubkeyPaths: [][]string{{"A"}, {"B"}}, } - orm.EXPECT().SelectFilters(mock.Anything).Return([]Filter{filter}, nil) - orm.EXPECT().SelectSeqNums(mock.Anything).Return(map[int64]int64{}, nil) + orm.EXPECT().SelectFilters(mock.Anything).Return([]Filter{filter}, nil).Once() + orm.EXPECT().SelectSeqNums(mock.Anything).Return(map[int64]int64{}, nil).Once() + orm.EXPECT().ChainID().Return(chainID).Once() orm.EXPECT().InsertFilter(mock.Anything, mock.Anything).RunAndReturn(func(ctx context.Context, f Filter) (int64, error) { require.Equal(t, f, filter) return filterID, nil @@ -101,8 +102,6 @@ func TestProcess(t *testing.T) { require.NoError(t, err) data = append(eventSig[:], data...) - require.NoError(t, err) - ev := ProgramEvent{ Program: addr.ToSolana().String(), BlockData: BlockData{ diff --git a/pkg/solana/logpoller/models.go b/pkg/solana/logpoller/models.go index a8cf45034..f07ec667c 100644 --- a/pkg/solana/logpoller/models.go +++ b/pkg/solana/logpoller/models.go @@ -4,8 +4,6 @@ import ( "encoding/base64" "time" - "github.com/lib/pq" - "github.com/smartcontractkit/chainlink-solana/pkg/solana/logpoller/utils" ) @@ -29,6 +27,9 @@ func (f Filter) MatchSameLogs(other Filter) bool { f.EventIdl.Equal(other.EventIdl) && f.SubkeyPaths.Equal(other.SubkeyPaths) } +// Discriminator returns a 12 character base64-encoded string +// +// This is the base64 encoding of the [8]byte discriminator returned by utils.Discriminator func (f Filter) Discriminator() string { d := utils.Discriminator("event", f.EventName) return base64.StdEncoding.EncodeToString(d[:]) @@ -44,7 +45,7 @@ type Log struct { BlockTimestamp time.Time Address PublicKey EventSig EventSignature - SubkeyValues pq.ByteaArray + SubkeyValues []IndexedValue TxHash Signature Data []byte CreatedAt time.Time diff --git a/pkg/solana/logpoller/test_helpers.go b/pkg/solana/logpoller/test_helpers.go index b1689a601..ba1807dbc 100644 --- a/pkg/solana/logpoller/test_helpers.go +++ b/pkg/solana/logpoller/test_helpers.go @@ -39,7 +39,7 @@ func newRandomLog(t *testing.T, filterID int64, chainID string, eventName string BlockTimestamp: time.Unix(1731590113, 0), Address: PublicKey(pubKey), EventSig: utils.Discriminator("event", eventName), - SubkeyValues: [][]byte{{3, 2, 1}, {1}, {1, 2}, pubKey.Bytes()}, + SubkeyValues: []IndexedValue{{3, 2, 1}, {1}, {1, 2}, pubKey.Bytes()}, TxHash: Signature(signature), Data: data, SequenceNum: rand.Int63n(500), diff --git a/pkg/solana/logpoller/types.go b/pkg/solana/logpoller/types.go index 7517c3ae8..b3aefd57c 100644 --- a/pkg/solana/logpoller/types.go +++ b/pkg/solana/logpoller/types.go @@ -157,34 +157,38 @@ func scanJSON(name string, dest, src interface{}) error { // way. type IndexedValue []byte -func (v *IndexedValue) FromInt64(i int64) { - v.FromUint64(uint64(i + math.MaxInt64)) -} - func (v *IndexedValue) FromUint64(u uint64) { *v = make([]byte, 8) binary.BigEndian.PutUint64(*v, u) } +func (v *IndexedValue) FromInt64(i int64) { + v.FromUint64(uint64(i) + math.MaxInt64 + 1) +} + func (v *IndexedValue) FromFloat64(f float64) { if f > 0 { - v.FromUint64(math.Float64bits(f) + math.MaxInt64) + v.FromUint64(math.Float64bits(f) + math.MaxInt64 + 1) return } - v.FromUint64(math.MaxInt64 - math.Float64bits(f)) + v.FromUint64(math.MaxInt64 + 1 - math.Float64bits(f)) } func NewIndexedValue(typedVal any) (iVal IndexedValue, err error) { + if typedVal == nil { + return nil, fmt.Errorf("nil pointer passed to NewIndexedValue") + } + // handle 2 simplest cases first switch t := typedVal.(type) { case []byte: return t, nil - case string: - return []byte(t), nil + case *string: + return []byte(*t), nil } // handle numeric types - v := reflect.ValueOf(typedVal) + v := reflect.ValueOf(typedVal).Elem() if v.CanUint() { iVal.FromUint64(v.Uint()) return iVal, nil @@ -199,7 +203,7 @@ func NewIndexedValue(typedVal any) (iVal IndexedValue, err error) { } // any length array is fine as long as the element type is byte - if t := reflect.TypeOf(typedVal); t.Kind() == reflect.Array { + if t := v.Type(); t.Kind() == reflect.Array { if t.Elem().Kind() == reflect.Uint8 { return v.Bytes(), nil } From 19f47d6580a515ad2c3f988930d2dcda60b64a0b Mon Sep 17 00:00:00 2001 From: Domino Valdano <2644901+reductionista@users.noreply.github.com> Date: Mon, 13 Jan 2025 20:24:50 -0800 Subject: [PATCH 30/43] Add ExtractField function, and fix DecodeSubkeyValues --- pkg/solana/logpoller/filters.go | 70 ++++++++++++++++++++++++---- pkg/solana/logpoller/filters_test.go | 65 ++++++++++++++++++++++++++ pkg/solana/logpoller/types.go | 10 ++-- 3 files changed, 130 insertions(+), 15 deletions(-) diff --git a/pkg/solana/logpoller/filters.go b/pkg/solana/logpoller/filters.go index b16865f57..dafb89bcf 100644 --- a/pkg/solana/logpoller/filters.go +++ b/pkg/solana/logpoller/filters.go @@ -7,8 +7,8 @@ import ( "fmt" "iter" "maps" - "slices" - "strings" + "reflect" + "strconv" "sync" "sync/atomic" @@ -431,12 +431,66 @@ func (fl *filters) DecodeSubKey(ctx context.Context, raw []byte, ID int64, subKe if !ok { return nil, fmt.Errorf("decoder %d not found", ID) } - itemType := strings.Join(slices.Concat([]string{filter.EventName}, subKeyPath), ".") - - val, err := decoder.CreateType(itemType, false) - if err != nil || val == nil { + decodedEvent, err := decoder.CreateType(filter.EventName, false) + if err != nil || decodedEvent == nil { return nil, err } - err = decoder.Decode(ctx, raw, val, itemType) - return val, err + err = decoder.Decode(ctx, raw, decodedEvent, filter.EventName) + if err != nil { + return nil, err + } + return ExtractField(decodedEvent, subKeyPath) +} + +// ExtractField extracts the value of a field or nested subfield from a composite datatype composed +// of a series of nested structs and maps. Pointers at any level are automatically dereferenced, as long +// as they aren't nil. path is an ordered list of nested subfield names to traverse. For now, slices and +// arrays are not supported. (If the need arises, we could support them by converting the field to an +// integer to extract a specific element from a slice or array.) +func ExtractField(data any, path []string) (any, error) { + v := reflect.ValueOf(data) + for v.Kind() == reflect.Ptr { + if v.IsNil() { + if len(path) > 0 { + return nil, fmt.Errorf("cannot extract field '%s' from a nil pointer", path[0]) + } + return nil, nil // as long as this is the last field in the path, nil pointer is not a problem + } + v = v.Elem() + } + + if len(path) == 0 { + return v.Interface(), nil + } + field, path := path[0], path[1:] + + switch v.Kind() { + case reflect.Struct: + v = v.FieldByName(field) + if !v.IsValid() { + return nil, fmt.Errorf("field '%s' of struct %v does not exist", field, data) + } + return ExtractField(v.Interface(), path) + case reflect.Map: + var keyVal reflect.Value + if keyType := v.Type().Key(); keyType.Kind() != reflect.String { + // This map does not have string keys, so let's try int (or anything convertible to int) + intKey, err := strconv.Atoi(field) + if err != nil { + return nil, fmt.Errorf("map key '%s' for non-string type '%T' is not convertable to an integer", field, v.Type()) + } + if !keyType.ConvertibleTo(reflect.TypeOf(intKey)) { + return nil, fmt.Errorf("map has type '%T', must be a string or convertable to an integer", v.Type()) + } + keyVal = reflect.ValueOf(intKey) + } else { + keyVal = reflect.ValueOf(field) + } + v = v.MapIndex(keyVal) + if !v.IsValid() { + return nil, fmt.Errorf("key '%s' of map %v does not exist", field, data) + } + return ExtractField(v.Interface(), path) + } + return nil, fmt.Errorf("extracting a field from a %s type is not supported", v.Kind().String()) } diff --git a/pkg/solana/logpoller/filters_test.go b/pkg/solana/logpoller/filters_test.go index 15b14c22b..cf8a22a78 100644 --- a/pkg/solana/logpoller/filters_test.go +++ b/pkg/solana/logpoller/filters_test.go @@ -4,10 +4,12 @@ import ( "errors" "fmt" "slices" + "strings" "testing" "github.com/gagliardetto/solana-go" "github.com/google/uuid" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" @@ -387,3 +389,66 @@ func TestFilters_GetFiltersToBackfill(t *testing.T) { require.NoError(t, filters.RegisterFilter(tests.Context(t), newFilter)) ensureInQueue(notBackfilled, newFilter) } + +func TestExtractField(t *testing.T) { + type innerInner struct { + P string + Q int + } + type innerStruct struct { + PtrString *string + ByteSlice []byte + DoubleNested innerInner + MapStringInt map[string]int + MapIntString map[int]string + } + myString := "string" + myInt32 := int32(16) + + testStruct := struct { + A int + B string + C *int32 + D innerStruct + }{ + 5, + "hello", + &myInt32, + innerStruct{ + &myString, + []byte("bytes"), + innerInner{"goodbye", 8}, + map[string]int{"key1": 1, "key2": 2}, + map[int]string{1: "val1", 2: "val2"}, + }, + } + + cases := []struct { + Name string + Path string + Result any + }{ + {"int from struct", "A", int(5)}, + {"string from struct", "B", "hello"}, + {"*int32 from struct", "C", myInt32}, + {"*string from nested struct", "D.PtrString", myString}, + {"[]byte from nested struct", "D.ByteSlice", []byte("bytes")}, + {"string from double-nested struct", "D.DoubleNested.P", "goodbye"}, + {"map[string]int from nested struct", "D.MapStringInt.key2", 2}, + {"key in map not found", "D.MapIntString.3", nil}, + {"non-integer key for map[int]string", "D.MapIntString.NotAnInt", nil}, + {"invalid field name in nested struct", "D.NoSuchField", nil}, + } + for _, c := range cases { + t.Run(c.Name, func(t *testing.T) { + result, err := ExtractField(&testStruct, strings.Split(c.Path, ".")) + if c.Result == nil { + assert.Error(t, err) + return + } + require.NoError(t, err) + assert.Equal(t, c.Result, result) + }) + } + +} diff --git a/pkg/solana/logpoller/types.go b/pkg/solana/logpoller/types.go index b3aefd57c..e17bb7fad 100644 --- a/pkg/solana/logpoller/types.go +++ b/pkg/solana/logpoller/types.go @@ -175,20 +175,16 @@ func (v *IndexedValue) FromFloat64(f float64) { } func NewIndexedValue(typedVal any) (iVal IndexedValue, err error) { - if typedVal == nil { - return nil, fmt.Errorf("nil pointer passed to NewIndexedValue") - } - // handle 2 simplest cases first switch t := typedVal.(type) { case []byte: return t, nil - case *string: - return []byte(*t), nil + case string: + return []byte(t), nil } // handle numeric types - v := reflect.ValueOf(typedVal).Elem() + v := reflect.ValueOf(typedVal) if v.CanUint() { iVal.FromUint64(v.Uint()) return iVal, nil From 981693fc40da7560ca0d871b5108c608bdfdffe2 Mon Sep 17 00:00:00 2001 From: Domino Valdano <2644901+reductionista@users.noreply.github.com> Date: Tue, 14 Jan 2025 09:22:51 -0800 Subject: [PATCH 31/43] MatchingFilters -> matchingFilters, and fix rest of expectedLog fields in TestProcess --- pkg/solana/logpoller/filters.go | 8 ++-- pkg/solana/logpoller/filters_test.go | 12 +++--- pkg/solana/logpoller/log_poller.go | 4 +- pkg/solana/logpoller/log_poller_test.go | 53 ++++++++++++++----------- pkg/solana/logpoller/test_helpers.go | 2 +- 5 files changed, 43 insertions(+), 36 deletions(-) diff --git a/pkg/solana/logpoller/filters.go b/pkg/solana/logpoller/filters.go index dafb89bcf..742e89c18 100644 --- a/pkg/solana/logpoller/filters.go +++ b/pkg/solana/logpoller/filters.go @@ -44,7 +44,9 @@ func newFilters(lggr logger.SugaredLogger, orm ORM) *filters { } } -func (fl *filters) IncrementSeqNums(filterID int64) int64 { +// IncrementSeqNum increments the sequence number for a filterID and returns the new +// number. This means the sequence number assigned to the first log matched after registration will be 1 +func (fl *filters) IncrementSeqNum(filterID int64) int64 { fl.seqNums[filterID]++ return fl.seqNums[filterID] } @@ -245,7 +247,7 @@ func (fl *filters) removeFilterFromIndexes(filter Filter) { // MatchingFilters - returns iterator to go through all matching filters. // Requires LoadFilters to be called at least once. -func (fl *filters) MatchingFilters(addr PublicKey, eventSignature EventSignature) iter.Seq[Filter] { +func (fl *filters) matchingFilters(addr PublicKey, eventSignature EventSignature) iter.Seq[Filter] { if !fl.loadedFilters.Load() { fl.lggr.Critical("Invariant violation: expected filters to be loaded before call to MatchingFilters") return nil @@ -307,7 +309,7 @@ func (fl *filters) MatchingFiltersForEncodedEvent(event ProgramEvent) iter.Seq[F } eventSig := EventSignature(decoded[:8]) - return fl.MatchingFilters(PublicKey(addr), eventSig) + return fl.matchingFilters(PublicKey(addr), eventSig) } // GetFiltersToBackfill - returns copy of backfill queue diff --git a/pkg/solana/logpoller/filters_test.go b/pkg/solana/logpoller/filters_test.go index cf8a22a78..de61ad934 100644 --- a/pkg/solana/logpoller/filters_test.go +++ b/pkg/solana/logpoller/filters_test.go @@ -149,7 +149,7 @@ func TestFilters_RegisterFilter(t *testing.T) { orm.On("InsertFilter", mock.Anything, mock.Anything).Return(int64(1), nil).Once() err = fs.RegisterFilter(tests.Context(t), filter) require.NoError(t, err) - storedFilters := slices.Collect(fs.MatchingFilters(filter.Address, filter.EventSig)) + storedFilters := slices.Collect(fs.matchingFilters(filter.Address, filter.EventSig)) require.Len(t, storedFilters, 1) filter.ID = 1 require.Equal(t, filter, storedFilters[0]) @@ -283,7 +283,7 @@ func TestFilters_PruneFilters(t *testing.T) { }) } -func TestFilters_MatchingFilters(t *testing.T) { +func TestFilters_matchingFilters(t *testing.T) { orm := newMockORM(t) lggr := logger.Sugared(logger.Test(t)) expectedFilter1 := Filter{ @@ -321,14 +321,14 @@ func TestFilters_MatchingFilters(t *testing.T) { filters := newFilters(lggr, orm) err := filters.LoadFilters(tests.Context(t)) require.NoError(t, err) - matchingFilters := slices.Collect(filters.MatchingFilters(expectedFilter1.Address, expectedFilter1.EventSig)) + matchingFilters := slices.Collect(filters.matchingFilters(expectedFilter1.Address, expectedFilter1.EventSig)) require.Len(t, matchingFilters, 2) require.Contains(t, matchingFilters, expectedFilter1) require.Contains(t, matchingFilters, expectedFilter2) // if at least one key does not match - returns empty iterator - require.Empty(t, slices.Collect(filters.MatchingFilters(newRandomPublicKey(t), expectedFilter1.EventSig))) - require.Empty(t, slices.Collect(filters.MatchingFilters(expectedFilter1.Address, newRandomEventSignature(t)))) - require.Empty(t, slices.Collect(filters.MatchingFilters(newRandomPublicKey(t), newRandomEventSignature(t)))) + require.Empty(t, slices.Collect(filters.matchingFilters(newRandomPublicKey(t), expectedFilter1.EventSig))) + require.Empty(t, slices.Collect(filters.matchingFilters(expectedFilter1.Address, newRandomEventSignature(t)))) + require.Empty(t, slices.Collect(filters.matchingFilters(newRandomPublicKey(t), newRandomEventSignature(t)))) } func TestFilters_GetFiltersToBackfill(t *testing.T) { diff --git a/pkg/solana/logpoller/log_poller.go b/pkg/solana/logpoller/log_poller.go index 6c145b13c..41e51df5c 100644 --- a/pkg/solana/logpoller/log_poller.go +++ b/pkg/solana/logpoller/log_poller.go @@ -108,7 +108,7 @@ func (lp *LogPoller) Process(programEvent ProgramEvent) (err error) { ChainID: lp.orm.ChainID(), LogIndex: makeLogIndex(blockData.TransactionIndex, blockData.TransactionLogIndex), BlockHash: Hash(blockData.BlockHash), - BlockNumber: int64(blockData.BlockHeight), + BlockNumber: int64(blockData.SlotNumber), BlockTimestamp: blockData.BlockTime.Time().UTC(), Address: filter.Address, EventSig: filter.EventSig, @@ -139,7 +139,7 @@ func (lp *LogPoller) Process(programEvent ProgramEvent) (err error) { log.SubkeyValues = append(log.SubkeyValues, indexedVal) } - log.SequenceNum = lp.filters.IncrementSeqNums(filter.ID) + log.SequenceNum = lp.filters.IncrementSeqNum(filter.ID) if filter.Retention > 0 { expiresAt := time.Now().Add(filter.Retention).UTC() diff --git a/pkg/solana/logpoller/log_poller_test.go b/pkg/solana/logpoller/log_poller_test.go index 1c8b07eb1..5682d7def 100644 --- a/pkg/solana/logpoller/log_poller_test.go +++ b/pkg/solana/logpoller/log_poller_test.go @@ -29,6 +29,14 @@ func TestProcess(t *testing.T) { addr := newRandomPublicKey(t) eventName := "myEvent" eventSig := utils.Discriminator("event", eventName) + event := struct { + A int64 + B string + }{55, "hello"} + subkeyValA, err := NewIndexedValue(event.A) + require.NoError(t, err) + subkeyValB, err := NewIndexedValue(event.B) + require.NoError(t, err) filterID := rand.Int63() chainID := uuid.NewString() @@ -37,8 +45,27 @@ func TestProcess(t *testing.T) { txLogIndex := uint(rand.Uint32()) expectedLog := newRandomLog(t, filterID, chainID, eventName) - + expectedLog.Address = addr expectedLog.LogIndex = makeLogIndex(txIndex, txLogIndex) + expectedLog.SequenceNum = 1 + expectedLog.SubkeyValues = []IndexedValue{subkeyValA, subkeyValB} + + expectedLog.Data, err = bin.MarshalBorsh(&event) + require.NoError(t, err) + + ev := ProgramEvent{ + Program: addr.ToSolana().String(), + BlockData: BlockData{ + SlotNumber: uint64(expectedLog.BlockNumber), + BlockHeight: 3, + BlockHash: expectedLog.BlockHash.ToSolana(), + BlockTime: solana.UnixTimeSeconds(expectedLog.BlockTimestamp.Unix()), + TransactionHash: expectedLog.TxHash.ToSolana(), + TransactionIndex: txIndex, + TransactionLogIndex: txLogIndex, + }, + Data: base64.StdEncoding.EncodeToString(append(eventSig[:], expectedLog.Data...)), + } orm := newMockORM(t) cl := clientmocks.NewReaderWriter(t) @@ -49,7 +76,7 @@ func TestProcess(t *testing.T) { var idlTypeInt64 codec.IdlType var idlTypeString codec.IdlType - err := json.Unmarshal([]byte("\"i64\""), &idlTypeInt64) + err = json.Unmarshal([]byte("\"i64\""), &idlTypeInt64) require.NoError(t, err) err = json.Unmarshal([]byte("\"string\""), &idlTypeString) require.NoError(t, err) @@ -93,28 +120,6 @@ func TestProcess(t *testing.T) { err = lp.RegisterFilter(ctx, filter) require.NoError(t, err) - event := struct { - A int64 - B string - }{55, "hello"} - - data, err := bin.MarshalBorsh(&event) - require.NoError(t, err) - data = append(eventSig[:], data...) - - ev := ProgramEvent{ - Program: addr.ToSolana().String(), - BlockData: BlockData{ - SlotNumber: 3, - BlockHeight: 5, - BlockHash: solana.HashFromBytes([]byte{1, 2, 3}), - BlockTime: solana.UnixTimeSeconds(expectedLog.BlockTimestamp.Unix()), - TransactionHash: expectedLog.TxHash.ToSolana(), - TransactionIndex: txIndex, - TransactionLogIndex: txLogIndex, - }, - Data: base64.StdEncoding.EncodeToString(data), - } err = lp.Process(ev) require.NoError(t, err) diff --git a/pkg/solana/logpoller/test_helpers.go b/pkg/solana/logpoller/test_helpers.go index ba1807dbc..987ad9f75 100644 --- a/pkg/solana/logpoller/test_helpers.go +++ b/pkg/solana/logpoller/test_helpers.go @@ -36,7 +36,7 @@ func newRandomLog(t *testing.T, filterID int64, chainID string, eventName string LogIndex: rand.Int63n(1000), BlockHash: Hash(pubKey), BlockNumber: rand.Int63n(1000000), - BlockTimestamp: time.Unix(1731590113, 0), + BlockTimestamp: time.Unix(1731590113, 0).UTC(), Address: PublicKey(pubKey), EventSig: utils.Discriminator("event", eventName), SubkeyValues: []IndexedValue{{3, 2, 1}, {1}, {1, 2}, pubKey.Bytes()}, From 98bd0933f0886ae6751e1295a47eafd6b8c5bdeb Mon Sep 17 00:00:00 2001 From: Domino Valdano <2644901+reductionista@users.noreply.github.com> Date: Tue, 14 Jan 2025 11:30:19 -0800 Subject: [PATCH 32/43] Fix lint errors, rename err2 & err3 --- pkg/solana/logpoller/filters.go | 3 ++- pkg/solana/logpoller/filters_test.go | 1 - pkg/solana/logpoller/log_poller.go | 20 ++++++++++---------- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/pkg/solana/logpoller/filters.go b/pkg/solana/logpoller/filters.go index 742e89c18..39111f220 100644 --- a/pkg/solana/logpoller/filters.go +++ b/pkg/solana/logpoller/filters.go @@ -493,6 +493,7 @@ func ExtractField(data any, path []string) (any, error) { return nil, fmt.Errorf("key '%s' of map %v does not exist", field, data) } return ExtractField(v.Interface(), path) + default: + return nil, fmt.Errorf("extracting a field from a %s type is not supported", v.Kind().String()) } - return nil, fmt.Errorf("extracting a field from a %s type is not supported", v.Kind().String()) } diff --git a/pkg/solana/logpoller/filters_test.go b/pkg/solana/logpoller/filters_test.go index de61ad934..b83b71385 100644 --- a/pkg/solana/logpoller/filters_test.go +++ b/pkg/solana/logpoller/filters_test.go @@ -450,5 +450,4 @@ func TestExtractField(t *testing.T) { assert.Equal(t, c.Result, result) }) } - } diff --git a/pkg/solana/logpoller/log_poller.go b/pkg/solana/logpoller/log_poller.go index 41e51df5c..bad0e392c 100644 --- a/pkg/solana/logpoller/log_poller.go +++ b/pkg/solana/logpoller/log_poller.go @@ -98,7 +98,7 @@ func (lp *LogPoller) Process(programEvent ProgramEvent) (err error) { matchingFilters := lp.filters.MatchingFiltersForEncodedEvent(programEvent) if matchingFilters == nil { - return + return nil } var logs []Log @@ -115,9 +115,9 @@ func (lp *LogPoller) Process(programEvent ProgramEvent) (err error) { TxHash: Signature(blockData.TransactionHash), } - eventData, err := base64.StdEncoding.DecodeString(programEvent.Data) - if err != nil { - return err + eventData, decodeErr := base64.StdEncoding.DecodeString(programEvent.Data) + if decodeErr != nil { + return decodeErr } if len(eventData) < 8 { err = fmt.Errorf("Assumption violation: %w, log.Data=%s", ErrMissingDiscriminator, log.Data) @@ -128,13 +128,13 @@ func (lp *LogPoller) Process(programEvent ProgramEvent) (err error) { log.SubkeyValues = make([]IndexedValue, 0, len(filter.SubkeyPaths)) for _, path := range filter.SubkeyPaths { - subKeyVal, err2 := lp.filters.DecodeSubKey(ctx, log.Data, filter.ID, path) - if err2 != nil { - return err2 + subKeyVal, decodeSubKeyErr := lp.filters.DecodeSubKey(ctx, log.Data, filter.ID, path) + if decodeSubKeyErr != nil { + return decodeSubKeyErr } - indexedVal, err3 := NewIndexedValue(subKeyVal) - if err3 != nil { - return err3 + indexedVal, newIndexedValErr := NewIndexedValue(subKeyVal) + if newIndexedValErr != nil { + return newIndexedValErr } log.SubkeyValues = append(log.SubkeyValues, indexedVal) } From b70cd69380c4597d59f1d1c00e3614e7cbff98f6 Mon Sep 17 00:00:00 2001 From: Domino Valdano <2644901+reductionista@users.noreply.github.com> Date: Tue, 14 Jan 2025 17:25:11 -0800 Subject: [PATCH 33/43] Fix overflows, lints, use default map vals, & add early return to Process --- .tool-versions | 2 +- pkg/solana/logpoller/filters.go | 12 ++---------- pkg/solana/logpoller/log_poller.go | 25 +++++++++++++++++++------ pkg/solana/logpoller/log_poller_test.go | 3 ++- pkg/solana/logpoller/types.go | 2 +- pkg/solana/logpoller/types_test.go | 7 +++++-- 6 files changed, 30 insertions(+), 21 deletions(-) diff --git a/.tool-versions b/.tool-versions index c4111ddca..04823174d 100644 --- a/.tool-versions +++ b/.tool-versions @@ -2,7 +2,7 @@ nodejs 18.20.2 yarn 1.22.19 rust 1.59.0 golang 1.23.3 -golangci-lint 1.60.1 +golangci-lint 1.61.0 actionlint 1.6.22 shellcheck 0.8.0 helm 3.9.4 diff --git a/pkg/solana/logpoller/filters.go b/pkg/solana/logpoller/filters.go index 39111f220..7903c994f 100644 --- a/pkg/solana/logpoller/filters.go +++ b/pkg/solana/logpoller/filters.go @@ -148,18 +148,10 @@ func (fl *filters) RegisterFilter(ctx context.Context, filter Filter) error { } programID := filter.Address.ToSolana().String() - if _, ok = fl.knownPrograms[programID]; !ok { - fl.knownPrograms[programID] = 1 - } else { - fl.knownPrograms[programID]++ - } + fl.knownPrograms[programID]++ discriminatorHead := filter.Discriminator()[:10] - if _, ok := fl.knownDiscriminators[discriminatorHead]; !ok { - fl.knownDiscriminators[discriminatorHead] = 1 - } else { - fl.knownDiscriminators[discriminatorHead]++ - } + fl.knownDiscriminators[discriminatorHead]++ return nil } diff --git a/pkg/solana/logpoller/log_poller.go b/pkg/solana/logpoller/log_poller.go index bad0e392c..5eb8b5396 100644 --- a/pkg/solana/logpoller/log_poller.go +++ b/pkg/solana/logpoller/log_poller.go @@ -82,11 +82,11 @@ func (lp *LogPoller) start(context.Context) error { return nil } -func makeLogIndex(txIndex int, txLogIndex uint) int64 { - if txIndex < 0 || txIndex > math.MaxUint32 || txLogIndex > math.MaxUint32 { - panic(fmt.Sprintf("txIndex or txLogIndex out of range: txIndex=%d, txLogIndex=%d", txIndex, txLogIndex)) +func makeLogIndex(txIndex int, txLogIndex uint) (int64, error) { + if txIndex > 0 && txIndex < math.MaxInt32 && txLogIndex < math.MaxUint32 { + return int64(txIndex<<32) | int64(txLogIndex), nil } - return int64(math.MaxUint32*uint32(txIndex) + uint32(txLogIndex)) + return 0, fmt.Errorf("txIndex or txLogIndex out of range: txIndex=%d, txLogIndex=%d", txIndex, txLogIndex) } // Process - process stream of events coming from log ingester @@ -103,12 +103,22 @@ func (lp *LogPoller) Process(programEvent ProgramEvent) (err error) { var logs []Log for filter := range matchingFilters { + logIndex, logIndexErr := makeLogIndex(blockData.TransactionIndex, blockData.TransactionLogIndex) + if logIndexErr != nil { + lp.lggr.Critical(err) + return err + } + if blockData.SlotNumber == math.MaxInt64 { + errSlot := fmt.Errorf("slot number %d out of range", blockData.SlotNumber) + lp.lggr.Critical(err.Error()) + return errSlot + } log := Log{ FilterID: filter.ID, ChainID: lp.orm.ChainID(), - LogIndex: makeLogIndex(blockData.TransactionIndex, blockData.TransactionLogIndex), + LogIndex: logIndex, BlockHash: Hash(blockData.BlockHash), - BlockNumber: int64(blockData.SlotNumber), + BlockNumber: int64(blockData.SlotNumber), //nolint:gosec BlockTimestamp: blockData.BlockTime.Time().UTC(), Address: filter.Address, EventSig: filter.EventSig, @@ -148,6 +158,9 @@ func (lp *LogPoller) Process(programEvent ProgramEvent) (err error) { logs = append(logs, log) } + if len(logs) == 0 { + return nil + } err = lp.orm.InsertLogs(ctx, logs) if err != nil { diff --git a/pkg/solana/logpoller/log_poller_test.go b/pkg/solana/logpoller/log_poller_test.go index 5682d7def..84f7e8496 100644 --- a/pkg/solana/logpoller/log_poller_test.go +++ b/pkg/solana/logpoller/log_poller_test.go @@ -46,7 +46,8 @@ func TestProcess(t *testing.T) { expectedLog := newRandomLog(t, filterID, chainID, eventName) expectedLog.Address = addr - expectedLog.LogIndex = makeLogIndex(txIndex, txLogIndex) + expectedLog.LogIndex, err = makeLogIndex(txIndex, txLogIndex) + require.NoError(t, err) expectedLog.SequenceNum = 1 expectedLog.SubkeyValues = []IndexedValue{subkeyValA, subkeyValB} diff --git a/pkg/solana/logpoller/types.go b/pkg/solana/logpoller/types.go index e17bb7fad..dc8b614bc 100644 --- a/pkg/solana/logpoller/types.go +++ b/pkg/solana/logpoller/types.go @@ -163,7 +163,7 @@ func (v *IndexedValue) FromUint64(u uint64) { } func (v *IndexedValue) FromInt64(i int64) { - v.FromUint64(uint64(i) + math.MaxInt64 + 1) + v.FromUint64(uint64(i + math.MaxInt64 + 1)) //nolint gosec passing i=math.MaxInt64 and i=math.MinInt64 are proven safe in TestIndexedValue } func (v *IndexedValue) FromFloat64(f float64) { diff --git a/pkg/solana/logpoller/types_test.go b/pkg/solana/logpoller/types_test.go index f21a8d61d..b7ed36c6d 100644 --- a/pkg/solana/logpoller/types_test.go +++ b/pkg/solana/logpoller/types_test.go @@ -1,6 +1,7 @@ package logpoller import ( + "math" "testing" "github.com/stretchr/testify/assert" @@ -13,10 +14,12 @@ func TestIndexedValue(t *testing.T) { lower any higher any }{ - {"int32", int32(-5), int32(5)}, + + {"uint64", uint64(math.MaxUint32), uint64(math.MaxUint64)}, + {"int32", int32(math.MinInt32), int32(math.MaxInt32)}, {"int32", int32(-8), int32(-5)}, {"int32", int32(5), int32(8)}, - {"int64", int64(-5), int64(5)}, + {"int64", int64(math.MinInt64), int64(math.MaxInt64)}, {"int64", int64(-8), int64(-5)}, {"int64", int64(5), int64(8)}, {"float32", float32(-5), float32(5)}, From bc853cd2a302600a0bfb34914955783717c9d89c Mon Sep 17 00:00:00 2001 From: Domino Valdano <2644901+reductionista@users.noreply.github.com> Date: Wed, 15 Jan 2025 11:41:29 -0800 Subject: [PATCH 34/43] Remove utils package, add 12-character assertion to Discriminator() This could only fail if someone updates to a new version of encoding/base64 which is buggy or breaks backward compatibility with the current version. Using panic instead of returning an error, since this can only happen during development and would need to be addressed immediately. --- pkg/solana/logpoller/discriminator.go | 14 +++++++++ pkg/solana/logpoller/log_poller_test.go | 3 +- pkg/solana/logpoller/models.go | 11 ++++--- pkg/solana/logpoller/test_helpers.go | 4 +-- pkg/solana/logpoller/utils/anchor.go | 41 ------------------------- 5 files changed, 23 insertions(+), 50 deletions(-) create mode 100644 pkg/solana/logpoller/discriminator.go delete mode 100644 pkg/solana/logpoller/utils/anchor.go diff --git a/pkg/solana/logpoller/discriminator.go b/pkg/solana/logpoller/discriminator.go new file mode 100644 index 000000000..812057a1c --- /dev/null +++ b/pkg/solana/logpoller/discriminator.go @@ -0,0 +1,14 @@ +package logpoller + +import ( + "crypto/sha256" + "fmt" +) + +const DiscriminatorLength = 8 + +func Discriminator(namespace, name string) [DiscriminatorLength]byte { + h := sha256.New() + h.Write([]byte(fmt.Sprintf("%s:%s", namespace, name))) + return [DiscriminatorLength]byte(h.Sum(nil)[:DiscriminatorLength]) +} diff --git a/pkg/solana/logpoller/log_poller_test.go b/pkg/solana/logpoller/log_poller_test.go index 84f7e8496..bd24ecd29 100644 --- a/pkg/solana/logpoller/log_poller_test.go +++ b/pkg/solana/logpoller/log_poller_test.go @@ -20,7 +20,6 @@ import ( "github.com/smartcontractkit/chainlink-solana/pkg/solana/client" clientmocks "github.com/smartcontractkit/chainlink-solana/pkg/solana/client/mocks" "github.com/smartcontractkit/chainlink-solana/pkg/solana/codec" - "github.com/smartcontractkit/chainlink-solana/pkg/solana/logpoller/utils" ) func TestProcess(t *testing.T) { @@ -28,7 +27,7 @@ func TestProcess(t *testing.T) { addr := newRandomPublicKey(t) eventName := "myEvent" - eventSig := utils.Discriminator("event", eventName) + eventSig := Discriminator("event", eventName) event := struct { A int64 B string diff --git a/pkg/solana/logpoller/models.go b/pkg/solana/logpoller/models.go index f07ec667c..2fe406d74 100644 --- a/pkg/solana/logpoller/models.go +++ b/pkg/solana/logpoller/models.go @@ -2,9 +2,8 @@ package logpoller import ( "encoding/base64" + "fmt" "time" - - "github.com/smartcontractkit/chainlink-solana/pkg/solana/logpoller/utils" ) type Filter struct { @@ -31,8 +30,12 @@ func (f Filter) MatchSameLogs(other Filter) bool { // // This is the base64 encoding of the [8]byte discriminator returned by utils.Discriminator func (f Filter) Discriminator() string { - d := utils.Discriminator("event", f.EventName) - return base64.StdEncoding.EncodeToString(d[:]) + d := Discriminator("event", f.EventName) + b64encoded := base64.StdEncoding.EncodeToString(d[:]) + if len(b64encoded) != 12 { + panic(fmt.Sprintf("Assumption Violation: expected encoding/base64 to return 12 character base64-encoding, got %d characters", len(b64encoded))) + } + return b64encoded } type Log struct { diff --git a/pkg/solana/logpoller/test_helpers.go b/pkg/solana/logpoller/test_helpers.go index 987ad9f75..8511ae1ac 100644 --- a/pkg/solana/logpoller/test_helpers.go +++ b/pkg/solana/logpoller/test_helpers.go @@ -7,8 +7,6 @@ import ( "github.com/gagliardetto/solana-go" "github.com/stretchr/testify/require" - - "github.com/smartcontractkit/chainlink-solana/pkg/solana/logpoller/utils" ) func newRandomPublicKey(t *testing.T) PublicKey { @@ -38,7 +36,7 @@ func newRandomLog(t *testing.T, filterID int64, chainID string, eventName string BlockNumber: rand.Int63n(1000000), BlockTimestamp: time.Unix(1731590113, 0).UTC(), Address: PublicKey(pubKey), - EventSig: utils.Discriminator("event", eventName), + EventSig: Discriminator("event", eventName), SubkeyValues: []IndexedValue{{3, 2, 1}, {1}, {1, 2}, pubKey.Bytes()}, TxHash: Signature(signature), Data: data, diff --git a/pkg/solana/logpoller/utils/anchor.go b/pkg/solana/logpoller/utils/anchor.go deleted file mode 100644 index 5dc954337..000000000 --- a/pkg/solana/logpoller/utils/anchor.go +++ /dev/null @@ -1,41 +0,0 @@ -package utils - -import ( - "bytes" - "context" - "crypto/sha256" - "fmt" - - "github.com/gagliardetto/solana-go" - "github.com/gagliardetto/solana-go/rpc" -) - -const DiscriminatorLength = 8 - -func Discriminator(namespace, name string) [DiscriminatorLength]byte { - h := sha256.New() - h.Write([]byte(fmt.Sprintf("%s:%s", namespace, name))) - return [DiscriminatorLength]byte(h.Sum(nil)[:DiscriminatorLength]) -} - -func IsEvent(event string, data []byte) bool { - if len(data) < 8 { - return false - } - d := Discriminator("event", event) - return bytes.Equal(d[:], data[:8]) -} - -func GetBlockTime(ctx context.Context, client *rpc.Client, commitment rpc.CommitmentType) (*solana.UnixTimeSeconds, error) { - block, err := client.GetBlockHeight(ctx, commitment) - if err != nil { - return nil, fmt.Errorf("failed to get block height: %w", err) - } - - blockTime, err := client.GetBlockTime(ctx, block) - if err != nil { - return nil, fmt.Errorf("failed to get block time: %w", err) - } - - return blockTime, nil -} From 4f82cece98762fb06dca193848c0f3c8d1b378d1 Mon Sep 17 00:00:00 2001 From: Domino Valdano <2644901+reductionista@users.noreply.github.com> Date: Wed, 15 Jan 2025 11:45:25 -0800 Subject: [PATCH 35/43] Remove TODO, uncomment internal loaders --- pkg/solana/chain.go | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/pkg/solana/chain.go b/pkg/solana/chain.go index 6f9850449..60ac30755 100644 --- a/pkg/solana/chain.go +++ b/pkg/solana/chain.go @@ -311,10 +311,9 @@ func newChain(id string, cfg *config.TOMLConfig, ks core.Keystore, lggr logger.L return result.Signature(), result.Error() } - // TODO: Can we just remove these? They nullify the lazy loaders initialized earlier, don't they? - //lc = internal.NewLoader[client.Reader](func() (client.Reader, error) { return ch.multiNode.SelectRPC() }) - //tc = internal.NewLoader[client.ReaderWriter](func() (client.ReaderWriter, error) { return ch.multiNode.SelectRPC() }) - //bc = internal.NewLoader[monitor.BalanceClient](func() (monitor.BalanceClient, error) { return ch.multiNode.SelectRPC() }) + lc = internal.NewLoader[client.Reader](func() (client.Reader, error) { return ch.multiNode.SelectRPC() }) + tc = internal.NewLoader[client.ReaderWriter](func() (client.ReaderWriter, error) { return ch.multiNode.SelectRPC() }) + bc = internal.NewLoader[monitor.BalanceClient](func() (monitor.BalanceClient, error) { return ch.multiNode.SelectRPC() }) } // TODO: import typeProvider function from codec package and pass to constructor From f18898f6c8439460afca310f86f755780fc69a79 Mon Sep 17 00:00:00 2001 From: Domino Valdano <2644901+reductionista@users.noreply.github.com> Date: Wed, 15 Jan 2025 12:29:57 -0800 Subject: [PATCH 36/43] Remove ILogPoller interface, add LogPoller interface to chain.go Also: add GetBlockWithOpts to MultiClient --- pkg/solana/chain.go | 13 ++++++++++--- pkg/solana/client/multi_client.go | 9 +++++++++ pkg/solana/logpoller/log_poller.go | 10 +--------- 3 files changed, 20 insertions(+), 12 deletions(-) diff --git a/pkg/solana/chain.go b/pkg/solana/chain.go index 60ac30755..a49e218f8 100644 --- a/pkg/solana/chain.go +++ b/pkg/solana/chain.go @@ -34,12 +34,19 @@ import ( txmutils "github.com/smartcontractkit/chainlink-solana/pkg/solana/txm/utils" ) +type LogPoller interface { + Start(context.Context) error + Close() error + RegisterFilter(ctx context.Context, filter logpoller.Filter) error + UnregisterFilter(ctx context.Context, name string) error +} + type Chain interface { types.ChainService ID() string Config() config.Config - LogPoller() logpoller.ILogPoller + LogPoller() LogPoller TxManager() TxManager // Reader returns a new Reader from the available list of nodes (if there are multiple, it will randomly select one) Reader() (client.Reader, error) @@ -92,7 +99,7 @@ type chain struct { services.StateMachine id string cfg *config.TOMLConfig - lp logpoller.ILogPoller + lp logpoller.LogPoller txm *txm.Txm balanceMonitor services.Service lggr logger.Logger @@ -407,7 +414,7 @@ func (c *chain) Config() config.Config { return c.cfg } -func (c *chain) LogPoller() logpoller.ILogPoller { +func (c *chain) LogPoller() LogPoller { return c.lp } diff --git a/pkg/solana/client/multi_client.go b/pkg/solana/client/multi_client.go index eb159e114..c55e2c9b9 100644 --- a/pkg/solana/client/multi_client.go +++ b/pkg/solana/client/multi_client.go @@ -166,3 +166,12 @@ func (m *MultiClient) GetSignaturesForAddressWithOpts(ctx context.Context, addr return r.GetSignaturesForAddressWithOpts(ctx, addr, opts) } + +func (m *MultiClient) GetBlockWithOpts(ctx context.Context, slot uint64, opts *rpc.GetBlockOpts) (*rpc.GetBlockResult, error) { + r, err := m.getClient() + if err != nil { + return nil, err + } + + return r.GetBlockWithOpts(ctx, slot, opts) +} diff --git a/pkg/solana/logpoller/log_poller.go b/pkg/solana/logpoller/log_poller.go index 5eb8b5396..f601c034d 100644 --- a/pkg/solana/logpoller/log_poller.go +++ b/pkg/solana/logpoller/log_poller.go @@ -32,14 +32,6 @@ type ORM interface { SelectSeqNums(ctx context.Context) (map[int64]int64, error) } -type ILogPoller interface { - Start(context.Context) error - Close() error - RegisterFilter(ctx context.Context, filter Filter) error - UnregisterFilter(ctx context.Context, name string) error - Process(programEvent ProgramEvent) error -} - type LogPoller struct { services.Service eng *services.Engine @@ -53,7 +45,7 @@ type LogPoller struct { filters *filters } -func New(lggr logger.SugaredLogger, orm ORM, cl internal.Loader[client.Reader]) ILogPoller { +func New(lggr logger.SugaredLogger, orm ORM, cl internal.Loader[client.Reader]) *LogPoller { lggr = logger.Sugared(logger.Named(lggr, "LogPoller")) lp := &LogPoller{ orm: orm, From c8d5e62e34af6e7e7cf386dd015cef22421a59c3 Mon Sep 17 00:00:00 2001 From: Domino Valdano <2644901+reductionista@users.noreply.github.com> Date: Wed, 15 Jan 2025 13:21:38 -0800 Subject: [PATCH 37/43] Start EncodedLogCollector as soon as filters are loaded Also: - Double check that filters are loaded at beginning of Process() - Add missing return from lp.loadFilters --- pkg/solana/logpoller/filters.go | 2 +- pkg/solana/logpoller/log_poller.go | 15 ++++++++++++--- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/pkg/solana/logpoller/filters.go b/pkg/solana/logpoller/filters.go index 7903c994f..595bfa9ae 100644 --- a/pkg/solana/logpoller/filters.go +++ b/pkg/solana/logpoller/filters.go @@ -241,7 +241,7 @@ func (fl *filters) removeFilterFromIndexes(filter Filter) { // Requires LoadFilters to be called at least once. func (fl *filters) matchingFilters(addr PublicKey, eventSignature EventSignature) iter.Seq[Filter] { if !fl.loadedFilters.Load() { - fl.lggr.Critical("Invariant violation: expected filters to be loaded before call to MatchingFilters") + fl.lggr.Critical("Invariant violation: expected filters to be loaded before call to matchingFilters") return nil } return func(yield func(Filter) bool) { diff --git a/pkg/solana/logpoller/log_poller.go b/pkg/solana/logpoller/log_poller.go index f601c034d..742665a50 100644 --- a/pkg/solana/logpoller/log_poller.go +++ b/pkg/solana/logpoller/log_poller.go @@ -86,6 +86,12 @@ func (lp *LogPoller) Process(programEvent ProgramEvent) (err error) { ctx, cancel := utils.ContextFromChan(lp.eng.StopChan) defer cancel() + // This should never happen, since the log collector isn't started until after the filters + // get loaded. But just in case, return an error if they aren't so the collector knows to retry later. + if err = lp.filters.LoadFilters(ctx); err != nil { + return err + } + blockData := programEvent.BlockData matchingFilters := lp.filters.MatchingFiltersForEncodedEvent(programEvent) @@ -186,10 +192,10 @@ func (lp *LogPoller) loadFilters(ctx context.Context) error { case <-retryTicker.C: } err := lp.filters.LoadFilters(ctx) - if err != nil { - lp.lggr.Errorw("Failed loading filters in init logpoller loop, retrying later", "err", err) - continue + if err == nil { + return nil } + lp.lggr.Errorw("Failed loading filters in init logpoller loop, retrying later", "err", err) } // unreachable } @@ -201,6 +207,9 @@ func (lp *LogPoller) run(ctx context.Context) { return } + // safe to start fetching logs, now that filters are loaded + lp.collector.Start(ctx) + var blocks chan struct { BlockNumber int64 Logs any // to be defined From e9983a37860d7f1429e98cca5e0e2ebd1038ec12 Mon Sep 17 00:00:00 2001 From: Domino Valdano <2644901+reductionista@users.noreply.github.com> Date: Wed, 15 Jan 2025 14:13:36 -0800 Subject: [PATCH 38/43] Add comment explaining < 12 validation --- pkg/solana/logpoller/filters.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/pkg/solana/logpoller/filters.go b/pkg/solana/logpoller/filters.go index 595bfa9ae..e3b6fed52 100644 --- a/pkg/solana/logpoller/filters.go +++ b/pkg/solana/logpoller/filters.go @@ -273,6 +273,10 @@ func (fl *filters) MatchingFiltersForEncodedEvent(event ProgramEvent) iter.Seq[F return nil } + // If this log message corresponds to an anchor event, then it must begin with an 8 byte discriminator, + // which will appear as the first 11 bytes of base64-encoded data. Standard base64 encoding RFC requires + // that any base64-encoded string must be padding with the = char to make its length a multiple of 4, so + // 12 is the minimum length for a valid anchor event. if len(event.Data) < 12 { return nil } @@ -280,7 +284,7 @@ func (fl *filters) MatchingFiltersForEncodedEvent(event ProgramEvent) iter.Seq[F // The first 64-bits of the event data is the event sig. Because it's base64 encoded, this corresponds to // the first 10 characters plus 4 bits of the 11th character. We can quickly rule it out as not matching any known // discriminators if the first 10 characters don't match. If it passes that initial test, we base64-decode the - // first 11 characters, and use the first 8 bytes of that as the event sig to call MatchingFilters. The address + // first 12 characters, and use the first 8 bytes of that as the event sig to call MatchingFilters. The address // also needs to be base58-decoded to pass to MatchingFilters if _, ok := fl.knownDiscriminators[event.Data[:10]]; !ok { return nil From e84fbe5b062f0e5c9d2170f9606f072cb24b44c6 Mon Sep 17 00:00:00 2001 From: Domino Valdano <2644901+reductionista@users.noreply.github.com> Date: Wed, 15 Jan 2025 14:53:21 -0800 Subject: [PATCH 39/43] Address 2 more PR comments, and remove accidentally added file --- pkg/solana/ccip-router-idl.json | 3260 ------------------------------- pkg/solana/logpoller/filters.go | 4 + pkg/solana/logpoller/job.go | 2 +- 3 files changed, 5 insertions(+), 3261 deletions(-) delete mode 100644 pkg/solana/ccip-router-idl.json diff --git a/pkg/solana/ccip-router-idl.json b/pkg/solana/ccip-router-idl.json deleted file mode 100644 index 26436941e..000000000 --- a/pkg/solana/ccip-router-idl.json +++ /dev/null @@ -1,3260 +0,0 @@ -{ - "version": "0.1.0", - "name": "ccip_router", - "docs": [ - "The `ccip_router` module contains the implementation of the Cross-Chain Interoperability Protocol (CCIP) Router.", - "", - "This is the Collapsed Router Program for CCIP.", - "As it's upgradable persisting the same program id, there is no need to have an indirection of a Proxy Program.", - "This Router handles both the OnRamp and OffRamp flow of the CCIP Messages." - ], - "constants": [ - { - "name": "MAX_ORACLES", - "type": { - "defined": "usize" - }, - "value": "16" - } - ], - "instructions": [ - { - "name": "initialize", - "docs": [ - "Initializes the CCIP Router.", - "", - "The initialization of the Router is responsibility of Admin, nothing more than calling this method should be done first.", - "", - "# Arguments", - "", - "* `ctx` - The context containing the accounts required for initialization.", - "* `solana_chain_selector` - The chain selector for Solana.", - "* `default_gas_limit` - The default gas limit for other destination chains.", - "* `default_allow_out_of_order_execution` - Whether out-of-order execution is allowed by default for other destination chains.", - "* `enable_execution_after` - The minimum amount of time required between a message has been committed and can be manually executed." - ], - "accounts": [ - { - "name": "config", - "isMut": true, - "isSigner": false - }, - { - "name": "state", - "isMut": true, - "isSigner": false - }, - { - "name": "authority", - "isMut": true, - "isSigner": true - }, - { - "name": "systemProgram", - "isMut": false, - "isSigner": false - }, - { - "name": "program", - "isMut": false, - "isSigner": false - }, - { - "name": "programData", - "isMut": false, - "isSigner": false - }, - { - "name": "externalExecutionConfig", - "isMut": true, - "isSigner": false - }, - { - "name": "tokenPoolsSigner", - "isMut": true, - "isSigner": false - } - ], - "args": [ - { - "name": "solanaChainSelector", - "type": "u64" - }, - { - "name": "defaultGasLimit", - "type": "u128" - }, - { - "name": "defaultAllowOutOfOrderExecution", - "type": "bool" - }, - { - "name": "enableExecutionAfter", - "type": "i64" - } - ] - }, - { - "name": "transferOwnership", - "docs": [ - "Transfers the ownership of the router to a new proposed owner.", - "", - "Shared func signature with other programs", - "", - "# Arguments", - "", - "* `ctx` - The context containing the accounts required for the transfer.", - "* `proposed_owner` - The public key of the new proposed owner." - ], - "accounts": [ - { - "name": "config", - "isMut": true, - "isSigner": false - }, - { - "name": "authority", - "isMut": false, - "isSigner": true - } - ], - "args": [ - { - "name": "proposedOwner", - "type": "publicKey" - } - ] - }, - { - "name": "acceptOwnership", - "docs": [ - "Accepts the ownership of the router by the proposed owner.", - "", - "Shared func signature with other programs", - "", - "# Arguments", - "", - "* `ctx` - The context containing the accounts required for accepting ownership.", - "The new owner must be a signer of the transaction." - ], - "accounts": [ - { - "name": "config", - "isMut": true, - "isSigner": false - }, - { - "name": "authority", - "isMut": false, - "isSigner": true - } - ], - "args": [] - }, - { - "name": "addChainSelector", - "docs": [ - "Adds a new chain selector to the router.", - "", - "The Admin needs to add any new chain supported (this means both OnRamp and OffRamp).", - "When adding a new chain, the Admin needs to specify if it's enabled or not.", - "They may enable only source, or only destination, or neither, or both.", - "", - "# Arguments", - "", - "* `ctx` - The context containing the accounts required for adding the chain selector.", - "* `new_chain_selector` - The new chain selector to be added.", - "* `source_chain_config` - The configuration for the chain as source.", - "* `dest_chain_config` - The configuration for the chain as destination." - ], - "accounts": [ - { - "name": "chainState", - "isMut": true, - "isSigner": false - }, - { - "name": "config", - "isMut": false, - "isSigner": false - }, - { - "name": "authority", - "isMut": true, - "isSigner": true - }, - { - "name": "systemProgram", - "isMut": false, - "isSigner": false - } - ], - "args": [ - { - "name": "newChainSelector", - "type": "u64" - }, - { - "name": "sourceChainConfig", - "type": { - "defined": "SourceChainConfig" - } - }, - { - "name": "destChainConfig", - "type": { - "defined": "DestChainConfig" - } - } - ] - }, - { - "name": "disableSourceChainSelector", - "docs": [ - "Disables the source chain selector.", - "", - "The Admin is the only one able to disable the chain selector as source. This method is thought of as an emergency kill-switch.", - "", - "# Arguments", - "", - "* `ctx` - The context containing the accounts required for disabling the chain selector.", - "* `source_chain_selector` - The source chain selector to be disabled." - ], - "accounts": [ - { - "name": "chainState", - "isMut": true, - "isSigner": false - }, - { - "name": "config", - "isMut": false, - "isSigner": false - }, - { - "name": "authority", - "isMut": true, - "isSigner": true - } - ], - "args": [ - { - "name": "sourceChainSelector", - "type": "u64" - } - ] - }, - { - "name": "disableDestChainSelector", - "docs": [ - "Disables the destination chain selector.", - "", - "The Admin is the only one able to disable the chain selector as destination. This method is thought of as an emergency kill-switch.", - "", - "# Arguments", - "", - "* `ctx` - The context containing the accounts required for disabling the chain selector.", - "* `dest_chain_selector` - The destination chain selector to be disabled." - ], - "accounts": [ - { - "name": "chainState", - "isMut": true, - "isSigner": false - }, - { - "name": "config", - "isMut": false, - "isSigner": false - }, - { - "name": "authority", - "isMut": true, - "isSigner": true - } - ], - "args": [ - { - "name": "destChainSelector", - "type": "u64" - } - ] - }, - { - "name": "updateSourceChainConfig", - "docs": [ - "Updates the configuration of the source chain selector.", - "", - "The Admin is the only one able to update the source chain config.", - "", - "# Arguments", - "", - "* `ctx` - The context containing the accounts required for updating the chain selector.", - "* `source_chain_selector` - The source chain selector to be updated.", - "* `source_chain_config` - The new configuration for the source chain." - ], - "accounts": [ - { - "name": "chainState", - "isMut": true, - "isSigner": false - }, - { - "name": "config", - "isMut": false, - "isSigner": false - }, - { - "name": "authority", - "isMut": true, - "isSigner": true - } - ], - "args": [ - { - "name": "sourceChainSelector", - "type": "u64" - }, - { - "name": "sourceChainConfig", - "type": { - "defined": "SourceChainConfig" - } - } - ] - }, - { - "name": "updateDestChainConfig", - "docs": [ - "Updates the configuration of the destination chain selector.", - "", - "The Admin is the only one able to update the destination chain config.", - "", - "# Arguments", - "", - "* `ctx` - The context containing the accounts required for updating the chain selector.", - "* `dest_chain_selector` - The destination chain selector to be updated.", - "* `dest_chain_config` - The new configuration for the destination chain." - ], - "accounts": [ - { - "name": "chainState", - "isMut": true, - "isSigner": false - }, - { - "name": "config", - "isMut": false, - "isSigner": false - }, - { - "name": "authority", - "isMut": true, - "isSigner": true - } - ], - "args": [ - { - "name": "destChainSelector", - "type": "u64" - }, - { - "name": "destChainConfig", - "type": { - "defined": "DestChainConfig" - } - } - ] - }, - { - "name": "updateSolanaChainSelector", - "docs": [ - "Updates the Solana chain selector in the router configuration.", - "", - "This method should only be used if there was an error with the initial configuration or if the solana chain selector changes.", - "", - "# Arguments", - "", - "* `ctx` - The context containing the accounts required for updating the configuration.", - "* `new_chain_selector` - The new chain selector for Solana." - ], - "accounts": [ - { - "name": "config", - "isMut": true, - "isSigner": false - }, - { - "name": "authority", - "isMut": false, - "isSigner": true - }, - { - "name": "systemProgram", - "isMut": false, - "isSigner": false - } - ], - "args": [ - { - "name": "newChainSelector", - "type": "u64" - } - ] - }, - { - "name": "updateDefaultGasLimit", - "docs": [ - "Updates the default gas limit in the router configuration.", - "", - "This change affects the default value for gas limit on every other destination chain.", - "The Admin is the only one able to update the default gas limit.", - "", - "# Arguments", - "", - "* `ctx` - The context containing the accounts required for updating the configuration.", - "* `new_gas_limit` - The new default gas limit." - ], - "accounts": [ - { - "name": "config", - "isMut": true, - "isSigner": false - }, - { - "name": "authority", - "isMut": false, - "isSigner": true - }, - { - "name": "systemProgram", - "isMut": false, - "isSigner": false - } - ], - "args": [ - { - "name": "newGasLimit", - "type": "u128" - } - ] - }, - { - "name": "updateDefaultAllowOutOfOrderExecution", - "docs": [ - "Updates the default setting for allowing out-of-order execution for other destination chains.", - "The Admin is the only one able to update this config.", - "", - "# Arguments", - "", - "* `ctx` - The context containing the accounts required for updating the configuration.", - "* `new_allow_out_of_order_execution` - The new setting for allowing out-of-order execution." - ], - "accounts": [ - { - "name": "config", - "isMut": true, - "isSigner": false - }, - { - "name": "authority", - "isMut": false, - "isSigner": true - }, - { - "name": "systemProgram", - "isMut": false, - "isSigner": false - } - ], - "args": [ - { - "name": "newAllowOutOfOrderExecution", - "type": "bool" - } - ] - }, - { - "name": "updateEnableManualExecutionAfter", - "docs": [ - "Updates the minimum amount of time required between a message being committed and when it can be manually executed.", - "", - "This is part of the OffRamp Configuration for Solana.", - "The Admin is the only one able to update this config.", - "", - "# Arguments", - "", - "* `ctx` - The context containing the accounts required for updating the configuration.", - "* `new_enable_manual_execution_after` - The new minimum amount of time required." - ], - "accounts": [ - { - "name": "config", - "isMut": true, - "isSigner": false - }, - { - "name": "authority", - "isMut": false, - "isSigner": true - }, - { - "name": "systemProgram", - "isMut": false, - "isSigner": false - } - ], - "args": [ - { - "name": "newEnableManualExecutionAfter", - "type": "i64" - } - ] - }, - { - "name": "registerTokenAdminRegistryViaGetCcipAdmin", - "docs": [ - "Registers the Token Admin Registry via the CCIP Admin", - "", - "# Arguments", - "", - "* `ctx` - The context containing the accounts required for registration.", - "* `mint` - The public key of the token mint.", - "* `token_admin_registry_admin` - The public key of the token admin registry admin." - ], - "accounts": [ - { - "name": "config", - "isMut": false, - "isSigner": false - }, - { - "name": "tokenAdminRegistry", - "isMut": true, - "isSigner": false - }, - { - "name": "authority", - "isMut": true, - "isSigner": true - }, - { - "name": "systemProgram", - "isMut": false, - "isSigner": false - } - ], - "args": [ - { - "name": "mint", - "type": "publicKey" - }, - { - "name": "tokenAdminRegistryAdmin", - "type": "publicKey" - } - ] - }, - { - "name": "registerTokenAdminRegistryViaOwner", - "docs": [ - "Registers the Token Admin Registry via the token owner.", - "", - "The Authority of the Mint Token can claim the registry of the token.", - "", - "# Arguments", - "", - "* `ctx` - The context containing the accounts required for registration." - ], - "accounts": [ - { - "name": "tokenAdminRegistry", - "isMut": true, - "isSigner": false - }, - { - "name": "mint", - "isMut": true, - "isSigner": false - }, - { - "name": "authority", - "isMut": true, - "isSigner": true - }, - { - "name": "systemProgram", - "isMut": false, - "isSigner": false - } - ], - "args": [] - }, - { - "name": "setPool", - "docs": [ - "Sets the pool lookup table for a given token mint.", - "", - "The administrator of the token admin registry can set the pool lookup table for a given token mint.", - "", - "# Arguments", - "", - "* `ctx` - The context containing the accounts required for setting the pool.", - "* `mint` - The public key of the token mint.", - "* `pool_lookup_table` - The public key of the pool lookup table, this address will be used for validations when interacting with the pool." - ], - "accounts": [ - { - "name": "tokenAdminRegistry", - "isMut": true, - "isSigner": false - }, - { - "name": "authority", - "isMut": true, - "isSigner": true - } - ], - "args": [ - { - "name": "mint", - "type": "publicKey" - }, - { - "name": "poolLookupTable", - "type": "publicKey" - } - ] - }, - { - "name": "transferAdminRoleTokenAdminRegistry", - "docs": [ - "Transfers the admin role of the token admin registry to a new admin.", - "", - "Only the Admin can transfer the Admin Role of the Token Admin Registry, this setups the Pending Admin and then it's their responsibility to accept the role.", - "", - "# Arguments", - "", - "* `ctx` - The context containing the accounts required for the transfer.", - "* `mint` - The public key of the token mint.", - "* `new_admin` - The public key of the new admin." - ], - "accounts": [ - { - "name": "tokenAdminRegistry", - "isMut": true, - "isSigner": false - }, - { - "name": "authority", - "isMut": true, - "isSigner": true - } - ], - "args": [ - { - "name": "mint", - "type": "publicKey" - }, - { - "name": "newAdmin", - "type": "publicKey" - } - ] - }, - { - "name": "acceptAdminRoleTokenAdminRegistry", - "docs": [ - "Accepts the admin role of the token admin registry.", - "", - "The Pending Admin must call this function to accept the admin role of the Token Admin Registry.", - "", - "# Arguments", - "", - "* `ctx` - The context containing the accounts required for accepting the admin role.", - "* `mint` - The public key of the token mint." - ], - "accounts": [ - { - "name": "tokenAdminRegistry", - "isMut": true, - "isSigner": false - }, - { - "name": "authority", - "isMut": true, - "isSigner": true - } - ], - "args": [ - { - "name": "mint", - "type": "publicKey" - } - ] - }, - { - "name": "setTokenBilling", - "docs": [ - "Sets the token billing configuration.", - "", - "Only CCIP Admin can set the token billing configuration.", - "", - "# Arguments", - "", - "* `ctx` - The context containing the accounts required for setting the token billing configuration.", - "* `_chain_selector` - The chain selector.", - "* `_mint` - The public key of the token mint.", - "* `cfg` - The token billing configuration." - ], - "accounts": [ - { - "name": "config", - "isMut": false, - "isSigner": false - }, - { - "name": "perChainPerTokenConfig", - "isMut": true, - "isSigner": false - }, - { - "name": "authority", - "isMut": true, - "isSigner": true - }, - { - "name": "systemProgram", - "isMut": false, - "isSigner": false - } - ], - "args": [ - { - "name": "chainSelector", - "type": "u64" - }, - { - "name": "mint", - "type": "publicKey" - }, - { - "name": "cfg", - "type": { - "defined": "TokenBilling" - } - } - ] - }, - { - "name": "setOcrConfig", - "docs": [ - "Sets the OCR configuration.", - "Only CCIP Admin can set the OCR configuration.", - "", - "# Arguments", - "", - "* `ctx` - The context containing the accounts required for setting the OCR configuration.", - "* `plugin_type` - The type of OCR plugin [0: Commit, 1: Execution].", - "* `config_info` - The OCR configuration information.", - "* `signers` - The list of signers.", - "* `transmitters` - The list of transmitters." - ], - "accounts": [ - { - "name": "config", - "isMut": true, - "isSigner": false - }, - { - "name": "state", - "isMut": true, - "isSigner": false - }, - { - "name": "authority", - "isMut": false, - "isSigner": true - } - ], - "args": [ - { - "name": "pluginType", - "type": "u8" - }, - { - "name": "configInfo", - "type": { - "defined": "Ocr3ConfigInfo" - } - }, - { - "name": "signers", - "type": { - "vec": { - "array": [ - "u8", - 20 - ] - } - } - }, - { - "name": "transmitters", - "type": { - "vec": "publicKey" - } - } - ] - }, - { - "name": "addBillingTokenConfig", - "docs": [ - "Adds a billing token configuration.", - "Only CCIP Admin can add a billing token configuration.", - "", - "# Arguments", - "", - "* `ctx` - The context containing the accounts required for adding the billing token configuration.", - "* `config` - The billing token configuration to be added." - ], - "accounts": [ - { - "name": "config", - "isMut": false, - "isSigner": false - }, - { - "name": "billingTokenConfig", - "isMut": true, - "isSigner": false - }, - { - "name": "tokenProgram", - "isMut": false, - "isSigner": false, - "docs": [ - "type of a specific program (which would enforce its ID). Thus, it's an UncheckedAccount", - "with a constraint enforcing that it is one of the two allowed programs." - ] - }, - { - "name": "feeTokenMint", - "isMut": false, - "isSigner": false - }, - { - "name": "feeTokenReceiver", - "isMut": true, - "isSigner": false - }, - { - "name": "authority", - "isMut": true, - "isSigner": true - }, - { - "name": "feeBillingSigner", - "isMut": false, - "isSigner": false - }, - { - "name": "associatedTokenProgram", - "isMut": false, - "isSigner": false - }, - { - "name": "systemProgram", - "isMut": false, - "isSigner": false - } - ], - "args": [ - { - "name": "config", - "type": { - "defined": "BillingTokenConfig" - } - } - ] - }, - { - "name": "updateBillingTokenConfig", - "docs": [ - "Updates the billing token configuration.", - "Only CCIP Admin can update a billing token configuration.", - "", - "# Arguments", - "", - "* `ctx` - The context containing the accounts required for updating the billing token configuration.", - "* `config` - The new billing token configuration." - ], - "accounts": [ - { - "name": "config", - "isMut": false, - "isSigner": false - }, - { - "name": "billingTokenConfig", - "isMut": true, - "isSigner": false - }, - { - "name": "authority", - "isMut": false, - "isSigner": true - } - ], - "args": [ - { - "name": "config", - "type": { - "defined": "BillingTokenConfig" - } - } - ] - }, - { - "name": "removeBillingTokenConfig", - "docs": [ - "Removes the billing token configuration.", - "Only CCIP Admin can remove a billing token configuration.", - "", - "# Arguments", - "", - "* `ctx` - The context containing the accounts required for removing the billing token configuration." - ], - "accounts": [ - { - "name": "config", - "isMut": false, - "isSigner": false - }, - { - "name": "billingTokenConfig", - "isMut": true, - "isSigner": false - }, - { - "name": "tokenProgram", - "isMut": false, - "isSigner": false, - "docs": [ - "type of a specific program (which would enforce its ID). Thus, it's an UncheckedAccount", - "with a constraint enforcing that it is one of the two allowed programs." - ] - }, - { - "name": "feeTokenMint", - "isMut": false, - "isSigner": false - }, - { - "name": "feeTokenReceiver", - "isMut": true, - "isSigner": false - }, - { - "name": "feeBillingSigner", - "isMut": true, - "isSigner": false - }, - { - "name": "authority", - "isMut": true, - "isSigner": true - }, - { - "name": "systemProgram", - "isMut": false, - "isSigner": false - } - ], - "args": [] - }, - { - "name": "getFee", - "docs": [ - "Calculates the fee for sending a message to the destination chain.", - "", - "# Arguments", - "", - "* `_ctx` - The context containing the accounts required for the fee calculation.", - "* `dest_chain_selector` - The chain selector for the destination chain.", - "* `message` - The message to be sent.", - "", - "# Returns", - "", - "The fee amount in u64." - ], - "accounts": [ - { - "name": "chainState", - "isMut": false, - "isSigner": false - }, - { - "name": "billingTokenConfig", - "isMut": false, - "isSigner": false - } - ], - "args": [ - { - "name": "destChainSelector", - "type": "u64" - }, - { - "name": "message", - "type": { - "defined": "Solana2AnyMessage" - } - } - ], - "returns": "u64" - }, - { - "name": "ccipSend", - "docs": [ - "ON RAMP FLOW", - "Sends a message to the destination chain.", - "", - "Request a message to be sent to the destination chain.", - "The method name needs to be ccip_send with Anchor encoding.", - "This function is called by the CCIP Sender Contract (or final user) to send a message to the CCIP Router.", - "The message will be sent to the receiver on the destination chain selector.", - "This message emits the event CCIPSendRequested with all the necessary data to be retrieved by the OffChain Code", - "", - "# Arguments", - "", - "* `ctx` - The context containing the accounts required for sending the message.", - "* `dest_chain_selector` - The chain selector for the destination chain.", - "* `message` - The message to be sent. The size limit of data is 256 bytes." - ], - "accounts": [ - { - "name": "config", - "isMut": false, - "isSigner": false - }, - { - "name": "chainState", - "isMut": true, - "isSigner": false - }, - { - "name": "nonce", - "isMut": true, - "isSigner": false - }, - { - "name": "authority", - "isMut": true, - "isSigner": true - }, - { - "name": "systemProgram", - "isMut": false, - "isSigner": false - }, - { - "name": "feeTokenProgram", - "isMut": false, - "isSigner": false, - "docs": [ - "type of a specific program (which would enforce its ID). Thus, it's an UncheckedAccount", - "with a constraint enforcing that it is one of the two allowed programs." - ] - }, - { - "name": "feeTokenMint", - "isMut": false, - "isSigner": false - }, - { - "name": "feeTokenConfig", - "isMut": false, - "isSigner": false - }, - { - "name": "feeTokenUserAssociatedAccount", - "isMut": true, - "isSigner": false - }, - { - "name": "feeTokenReceiver", - "isMut": true, - "isSigner": false - }, - { - "name": "feeBillingSigner", - "isMut": false, - "isSigner": false - }, - { - "name": "tokenPoolsSigner", - "isMut": true, - "isSigner": false - } - ], - "args": [ - { - "name": "destChainSelector", - "type": "u64" - }, - { - "name": "message", - "type": { - "defined": "Solana2AnyMessage" - } - } - ] - }, - { - "name": "commit", - "docs": [ - "OFF RAMP FLOW", - "Commits a report to the router.", - "", - "The method name needs to be commit with Anchor encoding.", - "", - "This function is called by the OffChain when committing one Report to the Solana Router.", - "In this Flow only one report is sent, the Commit Report. This is different as EVM does,", - "this is because here all the chain state is stored in one account per Merkle Tree Root.", - "So, to avoid having to send a dynamic size array of accounts, in this message only one Commit Report Account is sent.", - "This message validates the signatures of the report and stores the Merkle Root in the Commit Report Account.", - "The Report must contain an interval of messages, and the min of them must be the next sequence number expected.", - "The max size of the interval is 64.", - "This message emits two events: CommitReportAccepted and Transmitted.", - "", - "# Arguments", - "", - "* `ctx` - The context containing the accounts required for the commit.", - "* `report_context_byte_words` - consists of:", - "* report_context_byte_words[0]: ConfigDigest", - "* report_context_byte_words[1]: 24 byte padding, 8 byte sequence number", - "* report_context_byte_words[2]: ExtraHash", - "* `report` - The commit input report, single merkle root with RMN signatures and price updates", - "* `signatures` - The list of signatures. v0.29.0 - anchor idl does not build with ocr3base::SIGNATURE_LENGTH" - ], - "accounts": [ - { - "name": "config", - "isMut": false, - "isSigner": false - }, - { - "name": "chainState", - "isMut": true, - "isSigner": false - }, - { - "name": "commitReport", - "isMut": true, - "isSigner": false - }, - { - "name": "authority", - "isMut": true, - "isSigner": true - }, - { - "name": "systemProgram", - "isMut": false, - "isSigner": false - }, - { - "name": "sysvarInstructions", - "isMut": false, - "isSigner": false - } - ], - "args": [ - { - "name": "reportContextByteWords", - "type": { - "array": [ - { - "array": [ - "u8", - 32 - ] - }, - 3 - ] - } - }, - { - "name": "report", - "type": { - "defined": "CommitInput" - } - }, - { - "name": "signatures", - "type": { - "vec": { - "array": [ - "u8", - 65 - ] - } - } - } - ] - }, - { - "name": "execute", - "docs": [ - "OFF RAMP FLOW", - "Executes a message on the destination chain.", - "", - "The method name needs to be execute with Anchor encoding.", - "", - "This function is called by the OffChain when executing one Report to the Solana Router.", - "In this Flow only one message is sent, the Execution Report. This is different as EVM does,", - "this is because there is no try/catch mechanism to allow batch execution.", - "This message validates that the Merkle Tree Proof of the given message is correct and is stored in the Commit Report Account.", - "The message must be untouched to be executed.", - "This message emits the event ExecutionStateChanged with the new state of the message.", - "Finally, executes the CPI instruction to the receiver program in the ccip_receive message.", - "", - "# Arguments", - "", - "* `ctx` - The context containing the accounts required for the execute.", - "* `execution_report` - the execution report containing only one message and proofs", - "* `report_context_byte_words` - report_context after execution_report to match context for manually execute (proper decoding order)", - "* consists of:", - "* report_context_byte_words[0]: ConfigDigest", - "* report_context_byte_words[1]: 24 byte padding, 8 byte sequence number", - "* report_context_byte_words[2]: ExtraHash" - ], - "accounts": [ - { - "name": "config", - "isMut": false, - "isSigner": false - }, - { - "name": "chainState", - "isMut": false, - "isSigner": false - }, - { - "name": "commitReport", - "isMut": true, - "isSigner": false - }, - { - "name": "externalExecutionConfig", - "isMut": false, - "isSigner": false - }, - { - "name": "authority", - "isMut": true, - "isSigner": true - }, - { - "name": "systemProgram", - "isMut": false, - "isSigner": false - }, - { - "name": "sysvarInstructions", - "isMut": false, - "isSigner": false - }, - { - "name": "tokenPoolsSigner", - "isMut": false, - "isSigner": false - } - ], - "args": [ - { - "name": "executionReport", - "type": { - "defined": "ExecutionReportSingleChain" - } - }, - { - "name": "reportContextByteWords", - "type": { - "array": [ - { - "array": [ - "u8", - 32 - ] - }, - 3 - ] - } - } - ] - }, - { - "name": "manuallyExecute", - "docs": [ - "Manually executes a report to the router.", - "", - "When a message is not being executed, then the user can trigger the execution manually.", - "No verification over the transmitter, but the message needs to be in some commit report.", - "It validates that the required time has passed since the commit and then executes the report.", - "", - "# Arguments", - "", - "* `ctx` - The context containing the accounts required for the execution.", - "* `execution_report` - The execution report containing the message and proofs." - ], - "accounts": [ - { - "name": "config", - "isMut": false, - "isSigner": false - }, - { - "name": "chainState", - "isMut": false, - "isSigner": false - }, - { - "name": "commitReport", - "isMut": true, - "isSigner": false - }, - { - "name": "externalExecutionConfig", - "isMut": false, - "isSigner": false - }, - { - "name": "authority", - "isMut": true, - "isSigner": true - }, - { - "name": "systemProgram", - "isMut": false, - "isSigner": false - }, - { - "name": "sysvarInstructions", - "isMut": false, - "isSigner": false - }, - { - "name": "tokenPoolsSigner", - "isMut": false, - "isSigner": false - } - ], - "args": [ - { - "name": "executionReport", - "type": { - "defined": "ExecutionReportSingleChain" - } - } - ] - } - ], - "accounts": [ - { - "name": "Config", - "type": { - "kind": "struct", - "fields": [ - { - "name": "version", - "type": "u8" - }, - { - "name": "defaultAllowOutOfOrderExecution", - "type": "u8" - }, - { - "name": "padding0", - "type": { - "array": [ - "u8", - 6 - ] - } - }, - { - "name": "solanaChainSelector", - "type": "u64" - }, - { - "name": "defaultGasLimit", - "type": "u128" - }, - { - "name": "padding1", - "type": { - "array": [ - "u8", - 8 - ] - } - }, - { - "name": "owner", - "type": "publicKey" - }, - { - "name": "proposedOwner", - "type": "publicKey" - }, - { - "name": "enableManualExecutionAfter", - "type": "i64" - }, - { - "name": "padding2", - "type": { - "array": [ - "u8", - 8 - ] - } - }, - { - "name": "ocr3", - "type": { - "array": [ - { - "defined": "Ocr3Config" - }, - 2 - ] - } - } - ] - } - }, - { - "name": "GlobalState", - "type": { - "kind": "struct", - "fields": [ - { - "name": "latestPriceSequenceNumber", - "type": "u64" - } - ] - } - }, - { - "name": "ChainState", - "type": { - "kind": "struct", - "fields": [ - { - "name": "version", - "type": "u8" - }, - { - "name": "sourceChain", - "type": { - "defined": "SourceChain" - } - }, - { - "name": "destChain", - "type": { - "defined": "DestChain" - } - } - ] - } - }, - { - "name": "Nonce", - "type": { - "kind": "struct", - "fields": [ - { - "name": "version", - "type": "u8" - }, - { - "name": "counter", - "type": "u64" - } - ] - } - }, - { - "name": "ExternalExecutionConfig", - "type": { - "kind": "struct", - "fields": [] - } - }, - { - "name": "CommitReport", - "type": { - "kind": "struct", - "fields": [ - { - "name": "version", - "type": "u8" - }, - { - "name": "timestamp", - "type": "i64" - }, - { - "name": "minMsgNr", - "type": "u64" - }, - { - "name": "maxMsgNr", - "type": "u64" - }, - { - "name": "executionStates", - "type": "u128" - } - ] - } - }, - { - "name": "PerChainPerTokenConfig", - "type": { - "kind": "struct", - "fields": [ - { - "name": "version", - "type": "u8" - }, - { - "name": "chainSelector", - "type": "u64" - }, - { - "name": "mint", - "type": "publicKey" - }, - { - "name": "billing", - "type": { - "defined": "TokenBilling" - } - } - ] - } - }, - { - "name": "BillingTokenConfigWrapper", - "type": { - "kind": "struct", - "fields": [ - { - "name": "version", - "type": "u8" - }, - { - "name": "config", - "type": { - "defined": "BillingTokenConfig" - } - } - ] - } - }, - { - "name": "TokenAdminRegistry", - "type": { - "kind": "struct", - "fields": [ - { - "name": "version", - "type": "u8" - }, - { - "name": "administrator", - "type": "publicKey" - }, - { - "name": "pendingAdministrator", - "type": "publicKey" - }, - { - "name": "lookupTable", - "type": "publicKey" - } - ] - } - } - ], - "types": [ - { - "name": "CommitInput", - "type": { - "kind": "struct", - "fields": [ - { - "name": "priceUpdates", - "type": { - "defined": "PriceUpdates" - } - }, - { - "name": "merkleRoot", - "type": { - "defined": "MerkleRoot" - } - } - ] - } - }, - { - "name": "PriceUpdates", - "type": { - "kind": "struct", - "fields": [ - { - "name": "tokenPriceUpdates", - "type": { - "vec": { - "defined": "TokenPriceUpdate" - } - } - }, - { - "name": "gasPriceUpdates", - "type": { - "vec": { - "defined": "GasPriceUpdate" - } - } - } - ] - } - }, - { - "name": "TokenPriceUpdate", - "type": { - "kind": "struct", - "fields": [ - { - "name": "sourceToken", - "type": "publicKey" - }, - { - "name": "usdPerToken", - "type": { - "array": [ - "u8", - 28 - ] - } - } - ] - } - }, - { - "name": "GasPriceUpdate", - "type": { - "kind": "struct", - "fields": [ - { - "name": "destChainSelector", - "type": "u64" - }, - { - "name": "usdPerUnitGas", - "type": { - "array": [ - "u8", - 28 - ] - } - } - ] - } - }, - { - "name": "MerkleRoot", - "type": { - "kind": "struct", - "fields": [ - { - "name": "sourceChainSelector", - "type": "u64" - }, - { - "name": "onRampAddress", - "type": "bytes" - }, - { - "name": "minSeqNr", - "type": "u64" - }, - { - "name": "maxSeqNr", - "type": "u64" - }, - { - "name": "merkleRoot", - "type": { - "array": [ - "u8", - 32 - ] - } - } - ] - } - }, - { - "name": "RampMessageHeader", - "type": { - "kind": "struct", - "fields": [ - { - "name": "messageId", - "type": { - "array": [ - "u8", - 32 - ] - } - }, - { - "name": "sourceChainSelector", - "type": "u64" - }, - { - "name": "destChainSelector", - "type": "u64" - }, - { - "name": "sequenceNumber", - "type": "u64" - }, - { - "name": "nonce", - "type": "u64" - } - ] - } - }, - { - "name": "ExecutionReportSingleChain", - "docs": [ - "Report that is submitted by the execution DON at the execution phase. (including chain selector data)" - ], - "type": { - "kind": "struct", - "fields": [ - { - "name": "sourceChainSelector", - "type": "u64" - }, - { - "name": "message", - "type": { - "defined": "Any2SolanaRampMessage" - } - }, - { - "name": "offchainTokenData", - "type": { - "vec": "bytes" - } - }, - { - "name": "root", - "type": { - "array": [ - "u8", - 32 - ] - } - }, - { - "name": "proofs", - "type": { - "vec": { - "array": [ - "u8", - 32 - ] - } - } - }, - { - "name": "tokenIndexes", - "type": "bytes" - } - ] - } - }, - { - "name": "SolanaAccountMeta", - "type": { - "kind": "struct", - "fields": [ - { - "name": "pubkey", - "type": "publicKey" - }, - { - "name": "isWritable", - "type": "bool" - } - ] - } - }, - { - "name": "SolanaExtraArgs", - "type": { - "kind": "struct", - "fields": [ - { - "name": "computeUnits", - "type": "u32" - }, - { - "name": "accounts", - "type": { - "vec": { - "defined": "SolanaAccountMeta" - } - } - } - ] - } - }, - { - "name": "EvmExtraArgs", - "type": { - "kind": "struct", - "fields": [ - { - "name": "gasLimit", - "type": "u128" - }, - { - "name": "allowOutOfOrderExecution", - "type": "bool" - } - ] - } - }, - { - "name": "Any2SolanaRampMessage", - "type": { - "kind": "struct", - "fields": [ - { - "name": "header", - "type": { - "defined": "RampMessageHeader" - } - }, - { - "name": "sender", - "type": "bytes" - }, - { - "name": "data", - "type": "bytes" - }, - { - "name": "receiver", - "type": "publicKey" - }, - { - "name": "tokenAmounts", - "type": { - "vec": { - "defined": "Any2SolanaTokenTransfer" - } - } - }, - { - "name": "extraArgs", - "type": { - "defined": "SolanaExtraArgs" - } - } - ] - } - }, - { - "name": "Solana2AnyRampMessage", - "type": { - "kind": "struct", - "fields": [ - { - "name": "header", - "type": { - "defined": "RampMessageHeader" - } - }, - { - "name": "sender", - "type": "publicKey" - }, - { - "name": "data", - "type": "bytes" - }, - { - "name": "receiver", - "type": "bytes" - }, - { - "name": "extraArgs", - "type": { - "defined": "EvmExtraArgs" - } - }, - { - "name": "feeToken", - "type": "publicKey" - }, - { - "name": "tokenAmounts", - "type": { - "vec": { - "defined": "Solana2AnyTokenTransfer" - } - } - } - ] - } - }, - { - "name": "Solana2AnyTokenTransfer", - "type": { - "kind": "struct", - "fields": [ - { - "name": "sourcePoolAddress", - "type": "publicKey" - }, - { - "name": "destTokenAddress", - "type": "bytes" - }, - { - "name": "extraData", - "type": "bytes" - }, - { - "name": "amount", - "type": { - "array": [ - "u8", - 32 - ] - } - }, - { - "name": "destExecData", - "type": "bytes" - } - ] - } - }, - { - "name": "Any2SolanaTokenTransfer", - "type": { - "kind": "struct", - "fields": [ - { - "name": "sourcePoolAddress", - "type": "bytes" - }, - { - "name": "destTokenAddress", - "type": "publicKey" - }, - { - "name": "destGasAmount", - "type": "u32" - }, - { - "name": "extraData", - "type": "bytes" - }, - { - "name": "amount", - "type": { - "array": [ - "u8", - 32 - ] - } - } - ] - } - }, - { - "name": "LockOrBurnInV1", - "type": { - "kind": "struct", - "fields": [ - { - "name": "receiver", - "type": "bytes" - }, - { - "name": "remoteChainSelector", - "type": "u64" - }, - { - "name": "originalSender", - "type": "publicKey" - }, - { - "name": "amount", - "type": "u64" - }, - { - "name": "localToken", - "type": "publicKey" - } - ] - } - }, - { - "name": "ReleaseOrMintInV1", - "type": { - "kind": "struct", - "fields": [ - { - "name": "originalSender", - "type": "bytes" - }, - { - "name": "remoteChainSelector", - "type": "u64" - }, - { - "name": "receiver", - "type": "publicKey" - }, - { - "name": "amount", - "type": { - "array": [ - "u8", - 32 - ] - } - }, - { - "name": "localToken", - "type": "publicKey" - }, - { - "name": "sourcePoolAddress", - "docs": [ - "@dev WARNING: sourcePoolAddress should be checked prior to any processing of funds. Make sure it matches the", - "expected pool address for the given remoteChainSelector." - ], - "type": "bytes" - }, - { - "name": "sourcePoolData", - "type": "bytes" - }, - { - "name": "offchainTokenData", - "docs": [ - "@dev WARNING: offchainTokenData is untrusted data." - ], - "type": "bytes" - } - ] - } - }, - { - "name": "LockOrBurnOutV1", - "type": { - "kind": "struct", - "fields": [ - { - "name": "destTokenAddress", - "type": "bytes" - }, - { - "name": "destPoolData", - "type": "bytes" - } - ] - } - }, - { - "name": "ReleaseOrMintOutV1", - "type": { - "kind": "struct", - "fields": [ - { - "name": "destinationAmount", - "type": "u64" - } - ] - } - }, - { - "name": "Solana2AnyMessage", - "type": { - "kind": "struct", - "fields": [ - { - "name": "receiver", - "type": "bytes" - }, - { - "name": "data", - "type": "bytes" - }, - { - "name": "tokenAmounts", - "type": { - "vec": { - "defined": "SolanaTokenAmount" - } - } - }, - { - "name": "feeToken", - "type": "publicKey" - }, - { - "name": "extraArgs", - "type": { - "defined": "ExtraArgsInput" - } - }, - { - "name": "tokenIndexes", - "type": "bytes" - } - ] - } - }, - { - "name": "SolanaTokenAmount", - "type": { - "kind": "struct", - "fields": [ - { - "name": "token", - "type": "publicKey" - }, - { - "name": "amount", - "type": "u64" - } - ] - } - }, - { - "name": "ExtraArgsInput", - "type": { - "kind": "struct", - "fields": [ - { - "name": "gasLimit", - "type": { - "option": "u128" - } - }, - { - "name": "allowOutOfOrderExecution", - "type": { - "option": "bool" - } - } - ] - } - }, - { - "name": "Any2SolanaMessage", - "type": { - "kind": "struct", - "fields": [ - { - "name": "messageId", - "type": { - "array": [ - "u8", - 32 - ] - } - }, - { - "name": "sourceChainSelector", - "type": "u64" - }, - { - "name": "sender", - "type": "bytes" - }, - { - "name": "data", - "type": "bytes" - }, - { - "name": "tokenAmounts", - "type": { - "vec": { - "defined": "SolanaTokenAmount" - } - } - } - ] - } - }, - { - "name": "ReportContext", - "type": { - "kind": "struct", - "fields": [ - { - "name": "byteWords", - "type": { - "array": [ - { - "array": [ - "u8", - 32 - ] - }, - 3 - ] - } - } - ] - } - }, - { - "name": "Ocr3Config", - "type": { - "kind": "struct", - "fields": [ - { - "name": "pluginType", - "type": "u8" - }, - { - "name": "configInfo", - "type": { - "defined": "Ocr3ConfigInfo" - } - }, - { - "name": "signers", - "type": { - "array": [ - { - "array": [ - "u8", - 20 - ] - }, - 16 - ] - } - }, - { - "name": "transmitters", - "type": { - "array": [ - { - "array": [ - "u8", - 32 - ] - }, - 16 - ] - } - } - ] - } - }, - { - "name": "Ocr3ConfigInfo", - "type": { - "kind": "struct", - "fields": [ - { - "name": "configDigest", - "type": { - "array": [ - "u8", - 32 - ] - } - }, - { - "name": "f", - "type": "u8" - }, - { - "name": "n", - "type": "u8" - }, - { - "name": "isSignatureVerificationEnabled", - "type": "u8" - } - ] - } - }, - { - "name": "SourceChainConfig", - "type": { - "kind": "struct", - "fields": [ - { - "name": "isEnabled", - "type": "bool" - }, - { - "name": "onRamp", - "type": "bytes" - } - ] - } - }, - { - "name": "SourceChainState", - "type": { - "kind": "struct", - "fields": [ - { - "name": "minSeqNr", - "type": "u64" - } - ] - } - }, - { - "name": "SourceChain", - "type": { - "kind": "struct", - "fields": [ - { - "name": "state", - "type": { - "defined": "SourceChainState" - } - }, - { - "name": "config", - "type": { - "defined": "SourceChainConfig" - } - } - ] - } - }, - { - "name": "DestChainState", - "type": { - "kind": "struct", - "fields": [ - { - "name": "sequenceNumber", - "type": "u64" - }, - { - "name": "usdPerUnitGas", - "type": { - "defined": "TimestampedPackedU224" - } - } - ] - } - }, - { - "name": "DestChainConfig", - "type": { - "kind": "struct", - "fields": [ - { - "name": "isEnabled", - "type": "bool" - }, - { - "name": "maxNumberOfTokensPerMsg", - "type": "u16" - }, - { - "name": "maxDataBytes", - "type": "u32" - }, - { - "name": "maxPerMsgGasLimit", - "type": "u32" - }, - { - "name": "destGasOverhead", - "type": "u32" - }, - { - "name": "destGasPerPayloadByte", - "type": "u16" - }, - { - "name": "destDataAvailabilityOverheadGas", - "type": "u32" - }, - { - "name": "destGasPerDataAvailabilityByte", - "type": "u16" - }, - { - "name": "destDataAvailabilityMultiplierBps", - "type": "u16" - }, - { - "name": "defaultTokenFeeUsdcents", - "type": "u16" - }, - { - "name": "defaultTokenDestGasOverhead", - "type": "u32" - }, - { - "name": "defaultTxGasLimit", - "type": "u32" - }, - { - "name": "gasMultiplierWeiPerEth", - "type": "u64" - }, - { - "name": "networkFeeUsdcents", - "type": "u32" - }, - { - "name": "gasPriceStalenessThreshold", - "type": "u32" - }, - { - "name": "enforceOutOfOrder", - "type": "bool" - }, - { - "name": "chainFamilySelector", - "type": { - "array": [ - "u8", - 4 - ] - } - } - ] - } - }, - { - "name": "DestChain", - "type": { - "kind": "struct", - "fields": [ - { - "name": "state", - "type": { - "defined": "DestChainState" - } - }, - { - "name": "config", - "type": { - "defined": "DestChainConfig" - } - } - ] - } - }, - { - "name": "TokenBilling", - "type": { - "kind": "struct", - "fields": [ - { - "name": "minFeeUsdcents", - "type": "u32" - }, - { - "name": "maxFeeUsdcents", - "type": "u32" - }, - { - "name": "deciBps", - "type": "u16" - }, - { - "name": "destGasOverhead", - "type": "u32" - }, - { - "name": "destBytesOverhead", - "type": "u32" - }, - { - "name": "isEnabled", - "type": "bool" - } - ] - } - }, - { - "name": "RateLimitTokenBucket", - "type": { - "kind": "struct", - "fields": [ - { - "name": "tokens", - "type": "u128" - }, - { - "name": "lastUpdated", - "type": "u32" - }, - { - "name": "isEnabled", - "type": "bool" - }, - { - "name": "capacity", - "type": "u128" - }, - { - "name": "rate", - "type": "u128" - } - ] - } - }, - { - "name": "BillingTokenConfig", - "type": { - "kind": "struct", - "fields": [ - { - "name": "enabled", - "type": "bool" - }, - { - "name": "mint", - "type": "publicKey" - }, - { - "name": "usdPerToken", - "type": { - "defined": "TimestampedPackedU224" - } - }, - { - "name": "premiumMultiplierWeiPerEth", - "type": "u64" - } - ] - } - }, - { - "name": "TimestampedPackedU224", - "type": { - "kind": "struct", - "fields": [ - { - "name": "value", - "type": { - "array": [ - "u8", - 28 - ] - } - }, - { - "name": "timestamp", - "type": "i64" - } - ] - } - }, - { - "name": "OcrPluginType", - "type": { - "kind": "enum", - "variants": [ - { - "name": "Commit" - }, - { - "name": "Execution" - } - ] - } - }, - { - "name": "MerkleError", - "type": { - "kind": "enum", - "variants": [ - { - "name": "InvalidProof" - } - ] - } - }, - { - "name": "MessageExecutionState", - "type": { - "kind": "enum", - "variants": [ - { - "name": "Untouched" - }, - { - "name": "InProgress" - }, - { - "name": "Success" - }, - { - "name": "Failure" - } - ] - } - }, - { - "name": "CcipRouterError", - "type": { - "kind": "enum", - "variants": [ - { - "name": "InvalidSequenceInterval" - }, - { - "name": "RootNotCommitted" - }, - { - "name": "ExistingMerkleRoot" - }, - { - "name": "Unauthorized" - }, - { - "name": "InvalidInputs" - }, - { - "name": "UnsupportedSourceChainSelector" - }, - { - "name": "UnsupportedDestinationChainSelector" - }, - { - "name": "InvalidProof" - }, - { - "name": "InvalidMessage" - }, - { - "name": "ReachedMaxSequenceNumber" - }, - { - "name": "ManualExecutionNotAllowed" - }, - { - "name": "InvalidInputsTokenIndices" - }, - { - "name": "InvalidInputsPoolAccounts" - }, - { - "name": "InvalidInputsTokenAccounts" - }, - { - "name": "InvalidInputsConfigAccounts" - }, - { - "name": "InvalidInputsTokenAdminRegistryAccounts" - }, - { - "name": "InvalidInputsLookupTableAccounts" - }, - { - "name": "InvalidInputsTokenAmount" - }, - { - "name": "OfframpReleaseMintBalanceMismatch" - }, - { - "name": "OfframpInvalidDataLength" - }, - { - "name": "StaleCommitReport" - }, - { - "name": "DestinationChainDisabled" - }, - { - "name": "FeeTokenDisabled" - }, - { - "name": "MessageTooLarge" - }, - { - "name": "UnsupportedNumberOfTokens" - }, - { - "name": "UnsupportedChainFamilySelector" - }, - { - "name": "InvalidEVMAddress" - }, - { - "name": "InvalidEncoding" - } - ] - } - } - ], - "events": [ - { - "name": "CCIPMessageSent", - "fields": [ - { - "name": "destChainSelector", - "type": "u64", - "index": false - }, - { - "name": "sequenceNumber", - "type": "u64", - "index": false - }, - { - "name": "message", - "type": { - "defined": "Solana2AnyRampMessage" - }, - "index": false - } - ] - }, - { - "name": "CommitReportAccepted", - "fields": [ - { - "name": "merkleRoot", - "type": { - "defined": "MerkleRoot" - }, - "index": false - }, - { - "name": "priceUpdates", - "type": { - "defined": "PriceUpdates" - }, - "index": false - } - ] - }, - { - "name": "SkippedAlreadyExecutedMessage", - "fields": [ - { - "name": "sourceChainSelector", - "type": "u64", - "index": false - }, - { - "name": "sequenceNumber", - "type": "u64", - "index": false - } - ] - }, - { - "name": "AlreadyAttempted", - "fields": [ - { - "name": "sourceChainSelector", - "type": "u64", - "index": false - }, - { - "name": "sequenceNumber", - "type": "u64", - "index": false - } - ] - }, - { - "name": "ExecutionStateChanged", - "fields": [ - { - "name": "sourceChainSelector", - "type": "u64", - "index": false - }, - { - "name": "sequenceNumber", - "type": "u64", - "index": false - }, - { - "name": "messageId", - "type": { - "array": [ - "u8", - 32 - ] - }, - "index": false - }, - { - "name": "messageHash", - "type": { - "array": [ - "u8", - 32 - ] - }, - "index": false - }, - { - "name": "state", - "type": { - "defined": "MessageExecutionState" - }, - "index": false - } - ] - }, - { - "name": "PoolSet", - "fields": [ - { - "name": "token", - "type": "publicKey", - "index": false - }, - { - "name": "previousPoolLookupTable", - "type": "publicKey", - "index": false - }, - { - "name": "newPoolLookupTable", - "type": "publicKey", - "index": false - } - ] - }, - { - "name": "AdministratorTransferRequested", - "fields": [ - { - "name": "token", - "type": "publicKey", - "index": false - }, - { - "name": "currentAdmin", - "type": "publicKey", - "index": false - }, - { - "name": "newAdmin", - "type": "publicKey", - "index": false - } - ] - }, - { - "name": "AdministratorTransferred", - "fields": [ - { - "name": "token", - "type": "publicKey", - "index": false - }, - { - "name": "newAdmin", - "type": "publicKey", - "index": false - } - ] - }, - { - "name": "FeeTokenAdded", - "fields": [ - { - "name": "feeToken", - "type": "publicKey", - "index": false - }, - { - "name": "enabled", - "type": "bool", - "index": false - } - ] - }, - { - "name": "FeeTokenEnabled", - "fields": [ - { - "name": "feeToken", - "type": "publicKey", - "index": false - } - ] - }, - { - "name": "FeeTokenDisabled", - "fields": [ - { - "name": "feeToken", - "type": "publicKey", - "index": false - } - ] - }, - { - "name": "FeeTokenRemoved", - "fields": [ - { - "name": "feeToken", - "type": "publicKey", - "index": false - } - ] - }, - { - "name": "UsdPerUnitGasUpdated", - "fields": [ - { - "name": "destChain", - "type": "u64", - "index": false - }, - { - "name": "value", - "type": { - "array": [ - "u8", - 28 - ] - }, - "index": false - }, - { - "name": "timestamp", - "type": "i64", - "index": false - } - ] - }, - { - "name": "UsdPerTokenUpdated", - "fields": [ - { - "name": "token", - "type": "publicKey", - "index": false - }, - { - "name": "value", - "type": { - "array": [ - "u8", - 28 - ] - }, - "index": false - }, - { - "name": "timestamp", - "type": "i64", - "index": false - } - ] - }, - { - "name": "TokenTransferFeeConfigUpdated", - "fields": [ - { - "name": "destChainSelector", - "type": "u64", - "index": false - }, - { - "name": "token", - "type": "publicKey", - "index": false - }, - { - "name": "tokenTransferFeeConfig", - "type": { - "defined": "TokenBilling" - }, - "index": false - } - ] - }, - { - "name": "PremiumMultiplierWeiPerEthUpdated", - "fields": [ - { - "name": "token", - "type": "publicKey", - "index": false - }, - { - "name": "premiumMultiplierWeiPerEth", - "type": "u64", - "index": false - } - ] - }, - { - "name": "SourceChainConfigUpdated", - "fields": [ - { - "name": "sourceChainSelector", - "type": "u64", - "index": false - }, - { - "name": "sourceChainConfig", - "type": { - "defined": "SourceChainConfig" - }, - "index": false - } - ] - }, - { - "name": "SourceChainAdded", - "fields": [ - { - "name": "sourceChainSelector", - "type": "u64", - "index": false - }, - { - "name": "sourceChainConfig", - "type": { - "defined": "SourceChainConfig" - }, - "index": false - } - ] - }, - { - "name": "DestChainConfigUpdated", - "fields": [ - { - "name": "destChainSelector", - "type": "u64", - "index": false - }, - { - "name": "destChainConfig", - "type": { - "defined": "DestChainConfig" - }, - "index": false - } - ] - }, - { - "name": "DestChainAdded", - "fields": [ - { - "name": "destChainSelector", - "type": "u64", - "index": false - }, - { - "name": "destChainConfig", - "type": { - "defined": "DestChainConfig" - }, - "index": false - } - ] - }, - { - "name": "AdministratorRegistered", - "fields": [ - { - "name": "tokenMint", - "type": "publicKey", - "index": false - }, - { - "name": "administrator", - "type": "publicKey", - "index": false - } - ] - }, - { - "name": "ConfigSet", - "fields": [ - { - "name": "ocrPluginType", - "type": "u8", - "index": false - }, - { - "name": "configDigest", - "type": { - "array": [ - "u8", - 32 - ] - }, - "index": false - }, - { - "name": "signers", - "type": { - "vec": { - "array": [ - "u8", - 20 - ] - } - }, - "index": false - }, - { - "name": "transmitters", - "type": { - "vec": "publicKey" - }, - "index": false - }, - { - "name": "f", - "type": "u8", - "index": false - } - ] - }, - { - "name": "Transmitted", - "fields": [ - { - "name": "ocrPluginType", - "type": "u8", - "index": false - }, - { - "name": "configDigest", - "type": { - "array": [ - "u8", - 32 - ] - }, - "index": false - }, - { - "name": "sequenceNumber", - "type": "u64", - "index": false - } - ] - } - ], - "errors": [ - { - "code": 6000, - "name": "InvalidConfigFMustBePositive", - "msg": "Invalid config: F must be positive" - }, - { - "code": 6001, - "name": "InvalidConfigTooManyTransmitters", - "msg": "Invalid config: Too many transmitters" - }, - { - "code": 6002, - "name": "InvalidConfigTooManySigners", - "msg": "Invalid config: Too many signers" - }, - { - "code": 6003, - "name": "InvalidConfigFIsTooHigh", - "msg": "Invalid config: F is too high" - }, - { - "code": 6004, - "name": "InvalidConfigRepeatedOracle", - "msg": "Invalid config: Repeated oracle address" - }, - { - "code": 6005, - "name": "WrongMessageLength", - "msg": "Wrong message length" - }, - { - "code": 6006, - "name": "ConfigDigestMismatch", - "msg": "Config digest mismatch" - }, - { - "code": 6007, - "name": "WrongNumberOfSignatures", - "msg": "Wrong number signatures" - }, - { - "code": 6008, - "name": "UnauthorizedTransmitter", - "msg": "Unauthorized transmitter" - }, - { - "code": 6009, - "name": "UnauthorizedSigner", - "msg": "Unauthorized signer" - }, - { - "code": 6010, - "name": "NonUniqueSignatures", - "msg": "Non unique signatures" - }, - { - "code": 6011, - "name": "OracleCannotBeZeroAddress", - "msg": "Oracle cannot be zero address" - }, - { - "code": 6012, - "name": "StaticConfigCannotBeChanged", - "msg": "Static config cannot be changed" - }, - { - "code": 6013, - "name": "InvalidPluginType", - "msg": "Incorrect plugin type" - }, - { - "code": 6014, - "name": "InvalidSignature", - "msg": "Invalid signature" - } - ] -} diff --git a/pkg/solana/logpoller/filters.go b/pkg/solana/logpoller/filters.go index e3b6fed52..618c47fc2 100644 --- a/pkg/solana/logpoller/filters.go +++ b/pkg/solana/logpoller/filters.go @@ -216,6 +216,10 @@ func (fl *filters) removeFilterFromIndexes(filter Filter) { delete(fl.filtersByAddress, filter.Address) } + if _, ok := fl.seqNums[filter.ID]; ok { + delete(fl.seqNums, filter.ID) + } + programID := filter.Address.ToSolana().String() if refcount, ok := fl.knownPrograms[programID]; ok { refcount-- diff --git a/pkg/solana/logpoller/job.go b/pkg/solana/logpoller/job.go index bb336893c..448d800e4 100644 --- a/pkg/solana/logpoller/job.go +++ b/pkg/solana/logpoller/job.go @@ -126,7 +126,7 @@ func (j *getTransactionsFromBlockJob) Run(ctx context.Context) error { detail.blockTime = *block.BlockTime if len(block.Transactions) != len(blockSigsOnly.Signatures) { - return fmt.Errorf("block %d has %d transactions but %d signatures", block.BlockHeight, len(block.Transactions), len(blockSigsOnly.Signatures)) + return fmt.Errorf("block %d has %d transactions but %d signatures", j.slotNumber, len(block.Transactions), len(blockSigsOnly.Signatures)) } j.parser.ExpectTxs(j.slotNumber, len(block.Transactions)) From 82fa8cc4fe2bd26dce649dc30e91d06bee8bd2fa Mon Sep 17 00:00:00 2001 From: Domino Valdano <2644901+reductionista@users.noreply.github.com> Date: Wed, 15 Jan 2025 17:14:42 -0800 Subject: [PATCH 40/43] Use ch.multiClient instead of lazy-loader --- pkg/solana/chain.go | 5 +++-- pkg/solana/logpoller/log_poller.go | 14 +++----------- pkg/solana/logpoller/log_poller_test.go | 5 +---- 3 files changed, 7 insertions(+), 17 deletions(-) diff --git a/pkg/solana/chain.go b/pkg/solana/chain.go index a49e218f8..e4602eb15 100644 --- a/pkg/solana/chain.go +++ b/pkg/solana/chain.go @@ -96,7 +96,8 @@ func NewChain(cfg *config.TOMLConfig, opts ChainOpts) (Chain, error) { var _ Chain = (*chain)(nil) type chain struct { - services.StateMachine + services.Service + engine *services.Engine id string cfg *config.TOMLConfig lp logpoller.LogPoller @@ -324,7 +325,7 @@ func newChain(id string, cfg *config.TOMLConfig, ks core.Keystore, lggr logger.L } // TODO: import typeProvider function from codec package and pass to constructor - ch.lp = logpoller.New(logger.Sugared(logger.Named(lggr, "LogPoller")), logpoller.NewORM(ch.ID(), ds, lggr), lc) + ch.lp = logpoller.New(logger.Sugared(logger.Named(lggr, "LogPoller")), logpoller.NewORM(ch.ID(), ds, lggr), ch.multiClient) ch.txm = txm.NewTxm(ch.id, tc, sendTx, cfg, ks, lggr) ch.balanceMonitor = monitor.NewBalanceMonitor(ch.id, cfg, lggr, ks, bc) return &ch, nil diff --git a/pkg/solana/logpoller/log_poller.go b/pkg/solana/logpoller/log_poller.go index 742665a50..ee0688411 100644 --- a/pkg/solana/logpoller/log_poller.go +++ b/pkg/solana/logpoller/log_poller.go @@ -13,7 +13,6 @@ import ( "github.com/smartcontractkit/chainlink-common/pkg/utils" "github.com/smartcontractkit/chainlink-solana/pkg/solana/client" - "github.com/smartcontractkit/chainlink-solana/pkg/solana/internal" ) var ( @@ -35,17 +34,16 @@ type ORM interface { type LogPoller struct { services.Service eng *services.Engine - services.StateMachine lggr logger.SugaredLogger orm ORM - client internal.Loader[client.Reader] + client client.Reader collector *EncodedLogCollector filters *filters } -func New(lggr logger.SugaredLogger, orm ORM, cl internal.Loader[client.Reader]) *LogPoller { +func New(lggr logger.SugaredLogger, orm ORM, cl client.Reader) *LogPoller { lggr = logger.Sugared(logger.Named(lggr, "LogPoller")) lp := &LogPoller{ orm: orm, @@ -62,13 +60,7 @@ func New(lggr logger.SugaredLogger, orm ORM, cl internal.Loader[client.Reader]) return lp } -func (lp *LogPoller) start(context.Context) error { - cl, err := lp.client.Get() - if err != nil { - return err - } - lp.collector = NewEncodedLogCollector(cl, lp, lp.lggr) - +func (lp *LogPoller) start(_ context.Context) error { lp.eng.Go(lp.run) lp.eng.Go(lp.backgroundWorkerRun) return nil diff --git a/pkg/solana/logpoller/log_poller_test.go b/pkg/solana/logpoller/log_poller_test.go index bd24ecd29..e5006ae7b 100644 --- a/pkg/solana/logpoller/log_poller_test.go +++ b/pkg/solana/logpoller/log_poller_test.go @@ -11,13 +11,11 @@ import ( "github.com/gagliardetto/solana-go" "github.com/google/uuid" "github.com/smartcontractkit/chainlink-common/pkg/logger" - commonutils "github.com/smartcontractkit/chainlink-common/pkg/utils" "github.com/smartcontractkit/chainlink-common/pkg/utils/tests" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" - "github.com/smartcontractkit/chainlink-solana/pkg/solana/client" clientmocks "github.com/smartcontractkit/chainlink-solana/pkg/solana/client/mocks" "github.com/smartcontractkit/chainlink-solana/pkg/solana/codec" ) @@ -69,9 +67,8 @@ func TestProcess(t *testing.T) { orm := newMockORM(t) cl := clientmocks.NewReaderWriter(t) - loader := commonutils.NewLazyLoad(func() (client.Reader, error) { return cl, nil }) lggr := logger.Sugared(logger.Test(t)) - lp := New(lggr, orm, loader) + lp := New(lggr, orm, cl) var idlTypeInt64 codec.IdlType var idlTypeString codec.IdlType From d961eeed06fc21de93b617ec402df61fc8b61222 Mon Sep 17 00:00:00 2001 From: Domino Valdano <2644901+reductionista@users.noreply.github.com> Date: Wed, 15 Jan 2025 19:46:46 -0800 Subject: [PATCH 41/43] revert changes to chain.go --- pkg/solana/chain.go | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/pkg/solana/chain.go b/pkg/solana/chain.go index e4602eb15..04971544a 100644 --- a/pkg/solana/chain.go +++ b/pkg/solana/chain.go @@ -96,8 +96,7 @@ func NewChain(cfg *config.TOMLConfig, opts ChainOpts) (Chain, error) { var _ Chain = (*chain)(nil) type chain struct { - services.Service - engine *services.Engine + services.StateMachine id string cfg *config.TOMLConfig lp logpoller.LogPoller @@ -248,7 +247,6 @@ func newChain(id string, cfg *config.TOMLConfig, ks core.Keystore, lggr logger.L clientCache: map[string]*verifiedCachedClient{}, } - var lc internal.Loader[client.Reader] = utils.NewLazyLoad(func() (client.Reader, error) { return ch.getClient() }) var tc internal.Loader[client.ReaderWriter] = utils.NewLazyLoad(func() (client.ReaderWriter, error) { return ch.getClient() }) var bc internal.Loader[monitor.BalanceClient] = utils.NewLazyLoad(func() (monitor.BalanceClient, error) { return ch.getClient() }) // getClient returns random client or if MultiNodeEnabled RPC picked and controlled by MultiNode @@ -319,7 +317,6 @@ func newChain(id string, cfg *config.TOMLConfig, ks core.Keystore, lggr logger.L return result.Signature(), result.Error() } - lc = internal.NewLoader[client.Reader](func() (client.Reader, error) { return ch.multiNode.SelectRPC() }) tc = internal.NewLoader[client.ReaderWriter](func() (client.ReaderWriter, error) { return ch.multiNode.SelectRPC() }) bc = internal.NewLoader[monitor.BalanceClient](func() (monitor.BalanceClient, error) { return ch.multiNode.SelectRPC() }) } From ec90e37d4f4d71ec11b2c0fd39404e8475de7396 Mon Sep 17 00:00:00 2001 From: Domino Valdano <2644901+reductionista@users.noreply.github.com> Date: Wed, 15 Jan 2025 19:47:06 -0800 Subject: [PATCH 42/43] logpoller.LogPoller -> logpoller.Service Also: keep retrying if LogCollector fails to start --- pkg/solana/chain.go | 2 +- pkg/solana/logpoller/filters.go | 5 +--- pkg/solana/logpoller/log_poller.go | 38 +++++++++++++++++------------- 3 files changed, 24 insertions(+), 21 deletions(-) diff --git a/pkg/solana/chain.go b/pkg/solana/chain.go index 04971544a..c2c31dc2a 100644 --- a/pkg/solana/chain.go +++ b/pkg/solana/chain.go @@ -99,7 +99,7 @@ type chain struct { services.StateMachine id string cfg *config.TOMLConfig - lp logpoller.LogPoller + lp LogPoller txm *txm.Txm balanceMonitor services.Service lggr logger.Logger diff --git a/pkg/solana/logpoller/filters.go b/pkg/solana/logpoller/filters.go index 618c47fc2..b246f79e0 100644 --- a/pkg/solana/logpoller/filters.go +++ b/pkg/solana/logpoller/filters.go @@ -194,6 +194,7 @@ func (fl *filters) removeFilterFromIndexes(filter Filter) { delete(fl.filtersByName, filter.Name) delete(fl.filtersToBackfill, filter.ID) delete(fl.filtersByID, filter.ID) + delete(fl.seqNums, filter.ID) filtersForAddress, ok := fl.filtersByAddress[filter.Address] if !ok { @@ -216,10 +217,6 @@ func (fl *filters) removeFilterFromIndexes(filter Filter) { delete(fl.filtersByAddress, filter.Address) } - if _, ok := fl.seqNums[filter.ID]; ok { - delete(fl.seqNums, filter.ID) - } - programID := filter.Address.ToSolana().String() if refcount, ok := fl.knownPrograms[programID]; ok { refcount-- diff --git a/pkg/solana/logpoller/log_poller.go b/pkg/solana/logpoller/log_poller.go index ee0688411..c82e8eb47 100644 --- a/pkg/solana/logpoller/log_poller.go +++ b/pkg/solana/logpoller/log_poller.go @@ -31,7 +31,8 @@ type ORM interface { SelectSeqNums(ctx context.Context) (map[int64]int64, error) } -type LogPoller struct { +type Service struct { + services.StateMachine services.Service eng *services.Engine @@ -43,9 +44,9 @@ type LogPoller struct { filters *filters } -func New(lggr logger.SugaredLogger, orm ORM, cl client.Reader) *LogPoller { +func New(lggr logger.SugaredLogger, orm ORM, cl client.Reader) *Service { lggr = logger.Sugared(logger.Named(lggr, "LogPoller")) - lp := &LogPoller{ + lp := &Service{ orm: orm, client: cl, filters: newFilters(lggr, orm), @@ -60,7 +61,7 @@ func New(lggr logger.SugaredLogger, orm ORM, cl client.Reader) *LogPoller { return lp } -func (lp *LogPoller) start(_ context.Context) error { +func (lp *Service) start(_ context.Context) error { lp.eng.Go(lp.run) lp.eng.Go(lp.backgroundWorkerRun) return nil @@ -74,7 +75,7 @@ func makeLogIndex(txIndex int, txLogIndex uint) (int64, error) { } // Process - process stream of events coming from log ingester -func (lp *LogPoller) Process(programEvent ProgramEvent) (err error) { +func (lp *Service) Process(programEvent ProgramEvent) (err error) { ctx, cancel := utils.ContextFromChan(lp.eng.StopChan) defer cancel() @@ -160,20 +161,20 @@ func (lp *LogPoller) Process(programEvent ProgramEvent) (err error) { } // RegisterFilter - refer to filters.RegisterFilter for details. -func (lp *LogPoller) RegisterFilter(ctx context.Context, filter Filter) error { +func (lp *Service) RegisterFilter(ctx context.Context, filter Filter) error { ctx, cancel := lp.eng.Ctx(ctx) defer cancel() return lp.filters.RegisterFilter(ctx, filter) } // UnregisterFilter refer to filters.UnregisterFilter for details -func (lp *LogPoller) UnregisterFilter(ctx context.Context, name string) error { +func (lp *Service) UnregisterFilter(ctx context.Context, name string) error { ctx, cancel := lp.eng.Ctx(ctx) defer cancel() return lp.filters.UnregisterFilter(ctx, name) } -func (lp *LogPoller) loadFilters(ctx context.Context) error { +func (lp *Service) retryUntilSuccess(ctx context.Context, failMessage string, fn func(context.Context) error) error { retryTicker := services.TickerConfig{Initial: 0, JitterPct: services.DefaultJitter}.NewTicker(time.Second) defer retryTicker.Stop() @@ -183,24 +184,29 @@ func (lp *LogPoller) loadFilters(ctx context.Context) error { return ctx.Err() case <-retryTicker.C: } - err := lp.filters.LoadFilters(ctx) + err := fn(ctx) if err == nil { return nil } - lp.lggr.Errorw("Failed loading filters in init logpoller loop, retrying later", "err", err) + lp.lggr.Errorw(failMessage, "err", err) } // unreachable } -func (lp *LogPoller) run(ctx context.Context) { - err := lp.loadFilters(ctx) +func (lp *Service) run(ctx context.Context) { + err := lp.retryUntilSuccess(ctx, "failed loading filters in init Service loop, retrying later", lp.filters.LoadFilters) if err != nil { - lp.lggr.Warnw("Failed loading filters", "err", err) + lp.lggr.Warnw("never loaded filters before shutdown", "err", err) return } // safe to start fetching logs, now that filters are loaded - lp.collector.Start(ctx) + err = lp.retryUntilSuccess(ctx, "failed to start EncodedLogCollector, retrying later", lp.collector.Start) + if err != nil { + lp.lggr.Warnw("EncodedLogCollector never started before shutdown", "err", err) + return + } + defer lp.collector.Close() var blocks chan struct { BlockNumber int64 @@ -227,7 +233,7 @@ func (lp *LogPoller) run(ctx context.Context) { } } -func (lp *LogPoller) backgroundWorkerRun(ctx context.Context) { +func (lp *Service) backgroundWorkerRun(ctx context.Context) { pruneFilters := services.NewTicker(time.Minute) defer pruneFilters.Stop() for { @@ -243,7 +249,7 @@ func (lp *LogPoller) backgroundWorkerRun(ctx context.Context) { } } -func (lp *LogPoller) startFilterBackfill(ctx context.Context, filter Filter, toBlock int64) { +func (lp *Service) startFilterBackfill(ctx context.Context, filter Filter, toBlock int64) { // TODO: NONEVM-916 start backfill lp.lggr.Debugw("Starting filter backfill", "filter", filter) err := lp.filters.MarkFilterBackfilled(ctx, filter.ID) From 55938a00fe6ce4c14a0a2424aca113b3c47af697 Mon Sep 17 00:00:00 2001 From: Domino Valdano <2644901+reductionista@users.noreply.github.com> Date: Thu, 16 Jan 2025 10:35:15 -0800 Subject: [PATCH 43/43] Make MatchFiltersForEncodedEvent thread safe Also: add warnings to methods which are not intended to be called concurrently --- pkg/solana/logpoller/filters.go | 33 ++++++++++++++++++++++----------- 1 file changed, 22 insertions(+), 11 deletions(-) diff --git a/pkg/solana/logpoller/filters.go b/pkg/solana/logpoller/filters.go index b246f79e0..990d83432 100644 --- a/pkg/solana/logpoller/filters.go +++ b/pkg/solana/logpoller/filters.go @@ -45,7 +45,8 @@ func newFilters(lggr logger.SugaredLogger, orm ORM) *filters { } // IncrementSeqNum increments the sequence number for a filterID and returns the new -// number. This means the sequence number assigned to the first log matched after registration will be 1 +// number. This means the sequence number assigned to the first log matched after registration will be 1. +// WARNING: not thread safe, should only be called while fl.filtersMutex is locked, and after filters have been loaded. func (fl *filters) IncrementSeqNum(filterID int64) int64 { fl.seqNums[filterID]++ return fl.seqNums[filterID] @@ -270,10 +271,6 @@ func (fl *filters) matchingFilters(addr PublicKey, eventSignature EventSignature // this will be called on every new event that happens on the blockchain, so it's important it returns immediately if it // doesn't match any registered filters. func (fl *filters) MatchingFiltersForEncodedEvent(event ProgramEvent) iter.Seq[Filter] { - if _, ok := fl.knownPrograms[event.Program]; !ok { - return nil - } - // If this log message corresponds to an anchor event, then it must begin with an 8 byte discriminator, // which will appear as the first 11 bytes of base64-encoded data. Standard base64 encoding RFC requires // that any base64-encoded string must be padding with the = char to make its length a multiple of 4, so @@ -281,13 +278,24 @@ func (fl *filters) MatchingFiltersForEncodedEvent(event ProgramEvent) iter.Seq[F if len(event.Data) < 12 { return nil } + isKnown := func() (ok bool) { + fl.filtersMutex.RLock() + defer fl.filtersMutex.RUnlock() + + if _, ok = fl.knownPrograms[event.Program]; !ok { + return ok + } + + // The first 64-bits of the event data is the event sig. Because it's base64 encoded, this corresponds to + // the first 10 characters plus 4 bits of the 11th character. We can quickly rule it out as not matching any known + // discriminators if the first 10 characters don't match. If it passes that initial test, we base64-decode the + // first 12 characters, and use the first 8 bytes of that as the event sig to call MatchingFilters. The address + // also needs to be base58-decoded to pass to MatchingFilters + _, ok = fl.knownDiscriminators[event.Data[:10]] + return ok + } - // The first 64-bits of the event data is the event sig. Because it's base64 encoded, this corresponds to - // the first 10 characters plus 4 bits of the 11th character. We can quickly rule it out as not matching any known - // discriminators if the first 10 characters don't match. If it passes that initial test, we base64-decode the - // first 12 characters, and use the first 8 bytes of that as the event sig to call MatchingFilters. The address - // also needs to be base58-decoded to pass to MatchingFilters - if _, ok := fl.knownDiscriminators[event.Data[:10]]; !ok { + if !isKnown() { return nil } @@ -421,6 +429,9 @@ func (fl *filters) LoadFilters(ctx context.Context) error { return nil } +// DecodeSubKey accepts raw Borsh-encoded event data, a filter ID and a subkeyPath. It uses the decoder +// associated with that filter to decode the event and extract the subkey value from the specified subKeyPath. +// WARNING: not thread safe, should only be called while fl.filtersMutex is held and after filters have been loaded. func (fl *filters) DecodeSubKey(ctx context.Context, raw []byte, ID int64, subKeyPath []string) (any, error) { filter, ok := fl.filtersByID[ID] if !ok {