diff --git a/rpc/chain/cached_client.go b/rpc/chain/cached_client.go new file mode 100644 index 000000000..e3e8b3d3c --- /dev/null +++ b/rpc/chain/cached_client.go @@ -0,0 +1,170 @@ +package chain + +import ( + "context" + "database/sql" + "math/big" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/log" +) + +type CachedClient struct { + *ClientWithFallback + db *DB +} + +func NewCachedClient(ethClients []*EthClient, chainID uint64, db *sql.DB) *CachedClient { + return &CachedClient{ + ClientWithFallback: NewClient(ethClients, chainID), + db: NewDB(db), + } +} + +func (c *CachedClient) HeaderByHash(ctx context.Context, hash common.Hash) (*types.Header, error) { + header, err := c.db.GetBlockHeaderByHash(c.NetworkID(), hash) + if err == nil { + return header, nil + } else if err != sql.ErrNoRows { + // Soft error, we can continue + log.Error("Failed to get header from cache", "error", err) + } + + header, err = c.ClientWithFallback.HeaderByHash(ctx, hash) + if err != nil { + return nil, err + } + + err = c.db.PutBlockHeader(c.NetworkID(), header) + if err != nil { + // Soft error, we can continue + log.Error("Failed to put header into cache", "error", err) + } + + return header, nil +} + +func (c *CachedClient) BlockByHash(ctx context.Context, hash common.Hash) (*types.Block, error) { + block, err := c.db.GetBlockByHash(c.NetworkID(), hash) + if err == nil { + return block, nil + } else if err != sql.ErrNoRows { + // Soft error, we can continue + log.Error("Failed to get block from cache", "error", err) + } + + block, err = c.ClientWithFallback.BlockByHash(ctx, hash) + if err != nil { + return nil, err + } + + if block != nil { + err = c.db.PutBlock(c.NetworkID(), block) + if err != nil { + // Soft error, we can continue + log.Error("Failed to put block into cache", "error", err) + } + err = c.db.PutTransactions(c.NetworkID(), block.Transactions()) + if err != nil { + // Soft error, we can continue + log.Error("Failed to put transactions into cache", "error", err) + } + } + + return block, nil +} + +func (c *CachedClient) BlockByNumber(ctx context.Context, number *big.Int) (*types.Block, error) { + block, err := c.db.GetBlockByNumber(c.NetworkID(), number) + if err == nil { + return block, nil + } else if err != sql.ErrNoRows { + // Soft error, we can continue + log.Error("Failed to get block from cache", "error", err) + } + + block, err = c.ClientWithFallback.BlockByNumber(ctx, number) + if err != nil { + return nil, err + } + + err = c.db.PutBlock(c.NetworkID(), block) + if err != nil { + // Soft error, we can continue + log.Error("Failed to put block into cache", "error", err) + } + + return block, nil +} + +func (c *CachedClient) HeaderByNumber(ctx context.Context, number *big.Int) (*types.Header, error) { + header, err := c.db.GetBlockHeaderByNumber(c.NetworkID(), number) + if err == nil { + return header, nil + } else if err != sql.ErrNoRows { + // Soft error, we can continue + log.Error("Failed to get header from cache", "error", err) + } + + header, err = c.ClientWithFallback.HeaderByNumber(ctx, number) + if err != nil { + return nil, err + } + + err = c.db.PutBlockHeader(c.NetworkID(), header) + if err != nil { + // Soft error, we can continue + log.Error("Failed to put header into cache", "error", err) + } + + return header, nil +} + +func (c *CachedClient) TransactionByHash(ctx context.Context, hash common.Hash) (*types.Transaction, bool, error) { + transaction, err := c.db.GetTransactionByHash(c.NetworkID(), hash) + if err == nil { + return transaction, false, nil + } else if err != sql.ErrNoRows { + // Soft error, we can continue + log.Error("Failed to get transaction from cache", "error", err) + } + + transaction, pending, err := c.ClientWithFallback.TransactionByHash(ctx, hash) + if err != nil { + return nil, pending, err + } + + if !pending { + err = c.db.PutTransactions(c.NetworkID(), types.Transactions{transaction}) + if err != nil { + // Soft error, we can continue + log.Error("Failed to put transaction into cache", "error", err) + } + } + + return transaction, pending, nil +} + +func (c *CachedClient) TransactionReceipt(ctx context.Context, hash common.Hash) (*types.Receipt, error) { + receipt, err := c.db.GetTransactionReceipt(c.NetworkID(), hash) + if err == nil { + return receipt, nil + } else if err != sql.ErrNoRows { + // Soft error, we can continue + log.Error("Failed to get transaction receipt from cache", "error", err) + } + + receipt, err = c.ClientWithFallback.TransactionReceipt(ctx, hash) + if err != nil { + return nil, err + } + + err = c.db.PutTransactionReceipt(c.NetworkID(), receipt) + if err != nil { + // Soft error, we can continue + log.Error("Failed to put transaction receipt into cache", "error", err) + } + + return receipt, nil +} diff --git a/rpc/chain/cached_client_test.go b/rpc/chain/cached_client_test.go new file mode 100644 index 000000000..7ef3a3420 --- /dev/null +++ b/rpc/chain/cached_client_test.go @@ -0,0 +1,56 @@ +package chain + +import ( + "math/big" + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/status-im/status-go/t/helpers" + + "github.com/status-im/status-go/t/helpers" + "github.com/status-im/status-go/walletdatabase" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func setupDBTest(t *testing.T) (*DB, func()) { + db, err := helpers.SetupTestMemorySQLDB(walletdatabase.DbInitializer{}) + require.NoError(t, err) + return NewDB(db), func() { + require.NoError(t, db.Close()) + } +} + +setupCachedClient(t *testing.T) (*CachedClient, func()) { + db, closeDB := setupDBTest(t) + + + + db, err := helpers.SetupTestMemorySQLDB(walletdatabase.DbInitializer{}) + require.NoError(t, err) + return NewDB(db), func() { + require.NoError(t, db.Close()) + } +} + +func TestGetTransactionByHash(t *testing.T) { + db, cleanup := setupDBTest(t) + defer cleanup() + + chainID := uint64(1) + txHash := common.HexToHash("0x123456789abcdef") + tx := types.NewTransaction(0, common.HexToAddress("0x1"), big.NewInt(1), 100000, big.NewInt(1), nil) + receipt := &types.Receipt{ + TxHash: txHash, + GasUsed: 100000, + Status: types.ReceiptStatusSuccessful, + } + + err := db.PutTransaction(chainID, tx) + require.NoError(t, err) + + retrievedTx, err := db.GetTransactionByHash(chainID, txHash) + require.NoError(t, err) + assert.Equal(t, tx, retrievedTx) +} diff --git a/rpc/chain/cached_client_test_data.go b/rpc/chain/cached_client_test_data.go new file mode 100644 index 000000000..127c63638 --- /dev/null +++ b/rpc/chain/cached_client_test_data.go @@ -0,0 +1,94 @@ +package chain + +import ( + "math/big" + "math/rand" + + crypto_rand "crypto/rand" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" +) + +func getRandomTransaction() *types.Transaction { + nonce := rand.Uint64() + gasLimit := rand.Uint64() + gasPrice := rand.Uint64() + to := common.Address{} + crypto_rand.Read(to[:]) + value := rand.Uint64() + data := make([]byte, 32*rand.Intn(10)) + crypto_rand.Read(data) + + tx := types.NewTransaction(nonce, to, big.NewInt(int64(value)), gasLimit, big.NewInt(int64(gasPrice)), data) + + return tx +} + +func getRandomBlockHeader() *types.Header { + header := &types.Header{ + Number: big.NewInt(rand.Int63()), + Time: rand.Uint64(), + Difficulty: big.NewInt(rand.Int63()), + ParentHash: common.Hash{}, + Nonce: types.BlockNonce{}, + MixDigest: common.Hash{}, + } + crypto_rand.Read(header.ParentHash[:]) + crypto_rand.Read(header.Nonce[:]) + crypto_rand.Read(header.MixDigest[:]) + + return header +} + +func getRandomLog() *types.Log { + log := &types.Log{ + Address: common.Address{}, + Topics: []common.Hash{}, + Data: []byte{}, + BlockNumber: rand.Uint64(), + TxHash: common.Hash{}, + TxIndex: uint(rand.Uint64()), + } + crypto_rand.Read(log.Address[:]) + crypto_rand.Read(log.TxHash[:]) + for i := 0; i < rand.Intn(10); i++ { + hash := common.Hash{} + crypto_rand.Read(hash[:]) + log.Topics = append(log.Topics, hash) + } + crypto_rand.Read(log.Data) + + return log +} + +func getRandomReceipt() *types.Receipt { + receipt := &types.Receipt{ + Status: rand.Uint64(), + CumulativeGasUsed: rand.Uint64(), + Bloom: types.Bloom{}, + Logs: []*types.Log{}, + } + crypto_rand.Read(receipt.Bloom[:]) + for i := 0; i < rand.Intn(10); i++ { + receipt.Logs = append(receipt.Logs, getRandomLog()) + } + + return receipt +} + +func getRandomBlock() *types.Block { + header := getRandomBlockHeader() + + txs := []*types.Transaction{} + for i := 0; i < rand.Intn(10); i++ { + txs = append(txs, getRandomTransaction()) + } + + receipts := []*types.Receipt{} + for i := 0; i < rand.Intn(10); i++ { + receipts = append(receipts, getRandomReceipt()) + } + + return types.NewBlock(header, txs, nil, receipts, nil) +} diff --git a/rpc/chain/client.go b/rpc/chain/client.go index da8c24a3e..316261e5f 100644 --- a/rpc/chain/client.go +++ b/rpc/chain/client.go @@ -161,25 +161,6 @@ var propagateErrors = []error{ bind.ErrNoCode, } -func NewSimpleClient(ethClient EthClient, chainID uint64) *ClientWithFallback { - cbConfig := circuitbreaker.Config{ - Timeout: 20000, - MaxConcurrentRequests: 100, - SleepWindow: 300000, - ErrorPercentThreshold: 25, - } - - isConnected := &atomic.Bool{} - isConnected.Store(true) - return &ClientWithFallback{ - ChainID: chainID, - ethClients: []*EthClient{ðClient}, - isConnected: isConnected, - LastCheckedAt: time.Now().Unix(), - circuitbreaker: circuitbreaker.NewCircuitBreaker(cbConfig), - } -} - func NewClient(ethClients []*EthClient, chainID uint64) *ClientWithFallback { cbConfig := circuitbreaker.Config{ Timeout: 20000, diff --git a/rpc/chain/db.go b/rpc/chain/db.go new file mode 100644 index 000000000..5130308f9 --- /dev/null +++ b/rpc/chain/db.go @@ -0,0 +1,177 @@ +package chain + +import ( + "database/sql" + "math/big" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" +) + +type DB struct { + db *sql.DB +} + +func NewDB(db *sql.DB) *DB { + return &DB{db: db} +} + +func (b *DB) GetBlockByNumber(chainID uint64, blockNumber *big.Int) (*types.Block, error) { + row := b.db.QueryRow("SELECT block_json FROM blockchain_data_blocks WHERE chain_id = ? AND block_number = ?", chainID, blockNumber) + var block types.Block + err := row.Scan(&block) + if err != nil { + return nil, err + } + return &block, nil +} + +func (b *DB) GetBlockByHash(chainID uint64, blockHash common.Hash) (*types.Block, error) { + row := b.db.QueryRow("SELECT block_json FROM blockchain_data_blocks WHERE chain_id = ? AND block_hash = ?", chainID, blockHash) + var block types.Block + err := row.Scan(&block) + if err != nil { + return nil, err + } + return &block, nil +} + +func (b *DB) GetBlockHeaderByNumber(chainID uint64, blockNumber *big.Int) (*types.Header, error) { + row := b.db.QueryRow("SELECT block_json FROM blockchain_data_blocks WHERE chain_id = ? AND block_number = ?", chainID, blockNumber) + var blockHeader types.Header + err := row.Scan(&blockHeader) + if err != nil { + return nil, err + } + return &blockHeader, nil +} + +func (b *DB) GetBlockHeaderByHash(chainID uint64, blockHash common.Hash) (*types.Header, error) { + row := b.db.QueryRow("SELECT block_json FROM blockchain_data_blocks WHERE chain_id = ? AND block_hash = ?", chainID, blockHash) + var blockHeader types.Header + err := row.Scan(&blockHeader) + if err != nil { + return nil, err + } + return &blockHeader, nil +} + +func (b *DB) PutBlock(chainID uint64, block *types.Block) error { + _, err := b.db.Exec("INSERT INTO blockchain_data_blocks (chain_id, block_number, block_hash, block_header_json, block_json) VALUES (?, ?, ?, ?, ?)", chainID, block.Number(), block.Hash(), block.Header(), block) + if err != nil { + return err + } + return b.PutTransactions(chainID, block.Transactions()) +} + +func (b *DB) PutBlockHeader(chainID uint64, blockHeader *types.Header) error { + _, err := b.db.Exec("INSERT INTO blockchain_data_blocks (chain_id, block_number, block_hash, block_header_json) VALUES (?, ?, ?, ?)", chainID, blockHeader.Number, blockHeader.Hash(), blockHeader) + if err != nil { + return err + } + return nil +} + +func (t *DB) GetTransactionsByBlockHash(chainID uint64, blockHash common.Hash) (types.Transactions, error) { + rows, err := t.db.Query("SELECT transaction_json FROM blockchain_data_transactions WHERE chain_id = ? AND block_hash = ?", chainID, blockHash) + if err != nil { + return nil, err + } + defer rows.Close() + + var transactions types.Transactions + for rows.Next() { + var transaction types.Transaction + err := rows.Scan(&transaction) + if err != nil { + return nil, err + } + transactions = append(transactions, &transaction) + } + + return transactions, nil +} + +func (t *DB) GetTransactionsByBlockNumber(chainID uint64, blockNumber *big.Int) (types.Transactions, error) { + rows, err := t.db.Query("SELECT transaction_json FROM blockchain_data_transactions WHERE chain_id = ? AND block_number = ?", chainID, blockNumber) + if err != nil { + return nil, err + } + defer rows.Close() + + var transactions types.Transactions + for rows.Next() { + var transaction types.Transaction + err := rows.Scan(&transaction) + if err != nil { + return nil, err + } + transactions = append(transactions, &transaction) + } + + return transactions, nil +} + +func (t *DB) GetTransactionByHash(chainID uint64, transactionHash common.Hash) (*types.Transaction, error) { + row := t.db.QueryRow("SELECT transaction_json FROM blockchain_data_transactions WHERE chain_id = ? AND transaction_hash = ?", chainID, transactionHash) + var transaction types.Transaction + err := row.Scan(&transaction) + if err != nil { + return nil, err + } + return &transaction, nil +} + +func (t *DB) PutTransactions(chainID uint64, transactions types.Transactions) error { + tx, err := t.db.Begin() + if err != nil { + return err + } + defer tx.Rollback() + + stmt, err := tx.Prepare("INSERT INTO blockchain_data_transactions (chain_id, transaction_hash, transaction_json) VALUES (?, ?, ?)") + if err != nil { + return err + } + defer stmt.Close() + + for _, transaction := range transactions { + _, err = stmt.Exec(chainID, transaction.Hash(), transaction) + if err != nil { + return err + } + } + + return tx.Commit() +} + +func (t *DB) GetTransactionReceipt(chainID uint64, transactionHash common.Hash) (*types.Receipt, error) { + row := t.db.QueryRow("SELECT receipt_json FROM blockchain_data_transactions_receipts WHERE chain_id = ? AND transaction_hash = ?", chainID, transactionHash) + var receipt types.Receipt + err := row.Scan(&receipt) + if err != nil { + return nil, err + } + return &receipt, nil +} + +func (t *DB) PutTransactionReceipt(chainID uint64, receipt *types.Receipt) error { + tx, err := t.db.Begin() + if err != nil { + return err + } + defer tx.Rollback() + + stmt, err := tx.Prepare("INSERT INTO blockchain_data_transactions_receipts (chain_id, transaction_hash, receipt_json) VALUES (?, ?, ?)") + if err != nil { + return err + } + defer stmt.Close() + + _, err = stmt.Exec(chainID, receipt.TxHash, receipt) + if err != nil { + return err + } + + return tx.Commit() +} diff --git a/rpc/chain/db_test.go b/rpc/chain/db_test.go new file mode 100644 index 000000000..52c5d521c --- /dev/null +++ b/rpc/chain/db_test.go @@ -0,0 +1,153 @@ +package chain + +import ( + "math/big" + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/status-im/status-go/t/helpers" + "github.com/status-im/status-go/walletdatabase" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func setupDBTest(t *testing.T) (*DB, func()) { + db, err := helpers.SetupTestMemorySQLDB(walletdatabase.DbInitializer{}) + require.NoError(t, err) + return NewDB(db), func() { + require.NoError(t, db.Close()) + } +} + +func TestGetBlockByNumber(t *testing.T) { + db, cleanup := setupDBTest(t) + defer cleanup() + + chainID := uint64(1) + blockNumber := big.NewInt(123) + block := types.NewBlock(&types.Header{Number: blockNumber}, nil, nil, nil, nil) + + err := db.PutBlock(chainID, block) + require.NoError(t, err) + + retrievedBlock, err := db.GetBlockByNumber(chainID, blockNumber) + assert.NoError(t, err) + assert.NotNil(t, retrievedBlock) + assert.Equal(t, block.Hash(), retrievedBlock.Hash()) +} + +func TestGetBlockByHash(t *testing.T) { + db, cleanup := setupDBTest(t) + defer cleanup() + + chainID := uint64(1) + blockNumber := big.NewInt(123) + block := types.NewBlock(&types.Header{Number: blockNumber}, nil, nil, nil, nil) + + err := db.PutBlock(chainID, block) + require.NoError(t, err) + + retrievedBlock, err := db.GetBlockByHash(chainID, block.Hash()) + assert.NoError(t, err) + assert.NotNil(t, retrievedBlock) + assert.Equal(t, block.Number(), retrievedBlock.Number()) +} + +func TestGetBlockHeaderByNumber(t *testing.T) { + db, cleanup := setupDBTest(t) + defer cleanup() + + chainID := uint64(1) + blockNumber := big.NewInt(123) + header := &types.Header{Number: blockNumber} + block := types.NewBlock(header, nil, nil, nil, nil) + + err := db.PutBlock(chainID, block) + require.NoError(t, err) + + retrievedHeader, err := db.GetBlockHeaderByNumber(chainID, blockNumber) + assert.NoError(t, err) + assert.NotNil(t, retrievedHeader) + assert.Equal(t, header.Hash(), retrievedHeader.Hash()) +} + +func TestGetBlockHeaderByHash(t *testing.T) { + db, cleanup := setupDBTest(t) + defer cleanup() + + chainID := uint64(1) + blockNumber := big.NewInt(123) + header := &types.Header{Number: blockNumber} + block := types.NewBlock(header, nil, nil, nil, nil) + + err := db.PutBlock(chainID, block) + require.NoError(t, err) + + retrievedHeader, err := db.GetBlockHeaderByHash(chainID, block.Hash()) + assert.NoError(t, err) + assert.NotNil(t, retrievedHeader) + assert.Equal(t, header.Number, retrievedHeader.Number) +} + +func TestPutAndGetTransactions(t *testing.T) { + db, cleanup := setupDBTest(t) + defer cleanup() + + chainID := uint64(1) + blockNumber := big.NewInt(123) + tx1 := types.NewTransaction(0, common.Address{}, big.NewInt(100), 21000, big.NewInt(1), nil) + tx2 := types.NewTransaction(1, common.Address{}, big.NewInt(200), 21000, big.NewInt(1), nil) + txs := types.Transactions{tx1, tx2} + + block := types.NewBlock(&types.Header{Number: blockNumber}, txs, nil, nil, nil) + + err := db.PutBlock(chainID, block) + require.NoError(t, err) + + // Test GetTransactionsByBlockHash + retrievedTxs, err := db.GetTransactionsByBlockHash(chainID, block.Hash()) + assert.NoError(t, err) + assert.Len(t, retrievedTxs, 2) + assert.Equal(t, tx1.Hash(), retrievedTxs[0].Hash()) + assert.Equal(t, tx2.Hash(), retrievedTxs[1].Hash()) + + // Test GetTransactionsByBlockNumber + retrievedTxs, err = db.GetTransactionsByBlockNumber(chainID, blockNumber) + assert.NoError(t, err) + assert.Len(t, retrievedTxs, 2) + assert.Equal(t, tx1.Hash(), retrievedTxs[0].Hash()) + assert.Equal(t, tx2.Hash(), retrievedTxs[1].Hash()) + + // Test GetTransactionByHash + retrievedTx, err := db.GetTransactionByHash(chainID, tx1.Hash()) + assert.NoError(t, err) + assert.NotNil(t, retrievedTx) + assert.Equal(t, tx1.Hash(), retrievedTx.Hash()) +} + +func TestPutAndGetTransactionReceipt(t *testing.T) { + db, cleanup := setupDBTest(t) + defer cleanup() + + chainID := uint64(1) + tx := types.NewTransaction(0, common.Address{}, big.NewInt(100), 21000, big.NewInt(1), nil) + receipt := &types.Receipt{ + TxHash: tx.Hash(), + GasUsed: 21000, + Status: types.ReceiptStatusSuccessful, + BlockNumber: big.NewInt(123), + } + + err := db.PutTransactionReceipt(chainID, receipt) + require.NoError(t, err) + + retrievedReceipt, err := db.GetTransactionReceipt(chainID, tx.Hash()) + assert.NoError(t, err) + assert.NotNil(t, retrievedReceipt) + assert.Equal(t, receipt.TxHash, retrievedReceipt.TxHash) + assert.Equal(t, receipt.GasUsed, retrievedReceipt.GasUsed) + assert.Equal(t, receipt.Status, retrievedReceipt.Status) +} + +// Add more test functions as needed... diff --git a/rpc/client.go b/rpc/client.go index 255451cbb..512aaa8b5 100644 --- a/rpc/client.go +++ b/rpc/client.go @@ -104,6 +104,8 @@ type Client struct { walletNotifier func(chainID uint64, message string) providerConfigs []params.ProviderConfig + + db *sql.DB } // Is initialized in a build-tag-dependent module @@ -136,6 +138,7 @@ func NewClient(client *gethrpc.Client, upstreamChainID uint64, upstream params.U limiterPerProvider: make(map[string]*chain.RPCRpsLimiter), log: log, providerConfigs: providerConfigs, + db: db, } var opts []gethrpc.ClientOption @@ -165,7 +168,10 @@ func NewClient(client *gethrpc.Client, upstreamChainID uint64, upstream params.U // Include the chain-id in the rpc client rpcName := fmt.Sprintf("%s-chain-id-%d", hostPortUpstream, upstreamChainID) - c.upstream = chain.NewSimpleClient(*chain.NewEthClient(ethclient.NewClient(upstreamClient), limiter, upstreamClient, rpcName), upstreamChainID) + ethClients := []*chain.EthClient{ + chain.NewEthClient(ethclient.NewClient(upstreamClient), limiter, upstreamClient, rpcName), + } + c.upstream = chain.NewCachedClient(ethClients, upstreamChainID, db) } c.router = newRouter(c.upstreamEnabled) @@ -233,7 +239,7 @@ func (c *Client) getClientUsingCache(chainID uint64) (chain.ClientInterface, err return nil, fmt.Errorf("could not find any RPC URL for chain: %d", chainID) } - client := chain.NewClient(ethClients, chainID) + client := chain.NewCachedClient(ethClients, chainID, c.db) client.WalletNotifier = c.walletNotifier c.rpcClients[chainID] = client return client, nil @@ -371,7 +377,12 @@ func (c *Client) UpdateUpstreamURL(url string) error { if err != nil { hostPortUpstream = "upstream" } - c.upstream = chain.NewSimpleClient(*chain.NewEthClient(ethclient.NewClient(rpcClient), rpsLimiter, rpcClient, hostPortUpstream), c.UpstreamChainID) + + ethClients := []*chain.EthClient{ + chain.NewEthClient(chain.NewEthClient(ethclient.NewClient(rpcClient), rpsLimiter, rpcClient, hostPortUpstream)), + } + c.upstream = chain.NewCachedClient(ethClients, c.UpstreamChainID, c.db) + c.upstreamURL = url c.Unlock() diff --git a/walletdatabase/migrations/sql/1725392981_add_blockchain_data_tables.up.sql b/walletdatabase/migrations/sql/1725392981_add_blockchain_data_tables.up.sql new file mode 100644 index 000000000..514370c64 --- /dev/null +++ b/walletdatabase/migrations/sql/1725392981_add_blockchain_data_tables.up.sql @@ -0,0 +1,28 @@ +-- store raw block headers +CREATE TABLE IF NOT EXISTS blockchain_data_blocks ( + chain_id UNSIGNED BIGINT NOT NULL, + block_number BLOB NOT NULL, + block_hash BLOB NOT NULL, + block_header_json JSON NOT NULL, + block_json JSON, + CONSTRAINT unique_block_header_per_chain_per_block_number UNIQUE (chain_id,block_number) ON CONFLICT REPLACE, + CONSTRAINT unique_block_header_per_chain_per_block_hash UNIQUE (chain_id,block_hash) ON CONFLICT REPLACE +) WITHOUT ROWID; + +CREATE INDEX IF NOT EXISTS idx_blockchain_data_block_headers_chain_id_block_number ON blockchain_data_block_headers (chain_id, block_number); +CREATE INDEX IF NOT EXISTS idx_blockchain_data_block_headers_chain_id_block_hash ON blockchain_data_block_headers (chain_id, block_hash); + +-- store raw transactions +CREATE TABLE IF NOT EXISTS blockchain_data_transactions ( + chain_id UNSIGNED BIGINT NOT NULL, + block_hash BLOB NOT NULL, + transaction_hash BLOB NOT NULL, + transaction_json JSON NOT NULL, + receipt_json JSON, + CONSTRAINT unique_transaction_per_chain_per_transaction_hash UNIQUE (chain_id, transaction_hash) ON CONFLICT REPLACE, + FOREIGN KEY(chain_id, block_hash) REFERENCES blockchain_data_block_headers(chain_id, block_hash) + ON DELETE CASCADE +) WITHOUT ROWID; + +CREATE INDEX IF NOT EXISTS idx_blockchain_data_transactions_chain_id_transaction_hash ON blockchain_data_transactions (chain_id, transaction_hash); +CREATE INDEX IF NOT EXISTS idx_blockchain_data_transactions_chain_id_block_hash ON blockchain_data_transactions (chain_id, block_hash);