Repository: tablelandnetwork/go-tableland
Pull request #565 (2023-05-09 13:23:34)

Prepare Release 1.3.2 (Ship 4b)
# Summary

Preparation for v1.3.2 release
- #564 
- #563 
- #549


Patch:
diff --git a/README.md b/README.md
index c342edf..92e5da9 100644
--- a/README.md
+++ b/README.md
@@ -2,7 +2,7 @@
 [![Go Reference](https://pkg.go.dev/badge/github.com/textileio/go-tableland.svg)](https://pkg.go.dev/github.com/textileio/go-tableland) [![Go Report Card](https://goreportcard.com/badge/github.com/textileio/go-tableland)](https://goreportcard.com/report/github.com/textileio/go-tableland)
 <h1 align="center">Tableland Validator</h1>
 
-This repository contains the Go implementation of a Tableland validator.
+A Go language implementation of the Tableland validator, enabling developers and service providers to run nodes on the Tableland network and host databases for web3 users and applications.
 
 # What is a validator?
 
@@ -42,10 +42,13 @@ To understand better the usual work mechanics of the validator, let’s go throu
 
 The validator detects the smart contract events using an EVM node API (e.g: `geth` node), which can be self-hosted or served by providers (e.g: Alchemy, Infura, etc).
 
-# How can I run a validator?
+# Running a validator
 
-Soon we'll be creating full documentation on how to configure and run a validator, stay tuned.
-If you're interested in running a Tableland validator, please [fill this form](https://hhueol4i6vp.typeform.com/to/gkcyeA22).
+While network growth is not our immediate focus, we're excited about its potential in the future. If you're curious about the process, eager to contribute, or interested in experimenting, we encourage you to try running a validator. To get started, follow the step-by-step instructions provided in our [validator documentation](https://www.notion.so/textile/Validator-documentation-9f0cc2abf424410c8659fa939ed5095e?pvs=4).
+
+We appreciate your interest and welcome any questions or feedback you may have during the process. As our project evolves, we'll be shifting our focus and priorities, including network expansion in the future. Stay tuned for updates and developments.
+
+For projects that want to use the validator API, Tableland [maintains a public gateway](https://docs.tableland.xyz/gateway-api).
 
 # Building from source
 
diff --git a/cmd/api/main.go b/cmd/api/main.go
index 57732e0..40c6c95 100644
--- a/cmd/api/main.go
+++ b/cmd/api/main.go
@@ -30,7 +30,6 @@ import (
 	"github.com/textileio/go-tableland/pkg/database"
 	"github.com/textileio/go-tableland/pkg/eventprocessor"
 	"github.com/textileio/go-tableland/pkg/eventprocessor/eventfeed"
-
 	"go.opentelemetry.io/otel/attribute"
 
 	efimpl "github.com/textileio/go-tableland/pkg/eventprocessor/eventfeed/impl"
@@ -72,11 +71,6 @@ func main() {
 		path.Join(dirPath, "database.db"),
 	)
 
-	db, err := database.Open(databaseURL, attribute.String("database", "main"))
-	if err != nil {
-		log.Fatal().Err(err).Msg("opening the read database")
-	}
-
 	// Restore provided backup (if configured).
 	if config.BootstrapBackupURL != "" {
 		if err := restoreBackup(databaseURL, config.BootstrapBackupURL); err != nil {
@@ -84,6 +78,11 @@ func main() {
 		}
 	}
 
+	db, err := database.Open(databaseURL, attribute.String("database", "main"))
+	if err != nil {
+		log.Fatal().Err(err).Msg("opening the read database")
+	}
+
 	// Parser.
 	parser, err := createParser(config.QueryConstraints)
 	if err != nil {
diff --git a/internal/gateway/gateway.go b/internal/gateway/gateway.go
index 33e226c..907acd2 100644
--- a/internal/gateway/gateway.go
+++ b/internal/gateway/gateway.go
@@ -151,9 +151,12 @@ func (g *GatewayService) GetReceiptByTransactionHash(
 		BlockNumber:   receipt.BlockNumber,
 		IndexInBlock:  receipt.IndexInBlock,
 		TxnHash:       receipt.TxnHash,
-		TableID:       receipt.TableID,
+		TableIDs:      receipt.TableIDs,
 		Error:         receipt.Error,
 		ErrorEventIdx: receipt.ErrorEventIdx,
+
+		// Deprecated
+		TableID: receipt.TableID,
 	}, true, nil
 }
 
@@ -195,14 +198,17 @@ func (g *GatewayService) emptyMetadataImage() string {
 
 // Receipt represents a Tableland receipt.
 type Receipt struct {
-	ChainID      tableland.ChainID
-	BlockNumber  int64
-	IndexInBlock int64
-	TxnHash      string
-
-	TableID       *tables.TableID
+	ChainID       tableland.ChainID
+	BlockNumber   int64
+	IndexInBlock  int64
+	TxnHash       string
+	TableIDs      []tables.TableID
 	Error         *string
 	ErrorEventIdx *int
+
+	// Deprecated: the Receipt must hold information of all tables that were modified by the transaction.
+	// This field was replaced by TableIDs.
+	TableID *tables.TableID
 }
 
 // Table represents a system-wide table stored in Tableland.
diff --git a/internal/gateway/impl/gateway_store.go b/internal/gateway/impl/gateway_store.go
index e5f30ec..89f449b 100644
--- a/internal/gateway/impl/gateway_store.go
+++ b/internal/gateway/impl/gateway_store.go
@@ -144,18 +144,33 @@ func (s *GatewayStore) GetReceipt(
 		IndexInBlock: res.IndexInBlock,
 		TxnHash:      txnHash,
 	}
+
 	if res.Error.Valid {
 		receipt.Error = &res.Error.String
 
 		errorEventIdx := int(res.ErrorEventIdx.Int64)
 		receipt.ErrorEventIdx = &errorEventIdx
 	}
+
 	if res.TableID.Valid {
 		id, err := tables.NewTableIDFromInt64(res.TableID.Int64)
 		if err != nil {
-			return gateway.Receipt{}, false, fmt.Errorf("parsing id to string: %s", err)
+			return gateway.Receipt{}, false, fmt.Errorf("parsing id integer: %s", err)
+		}
+		receipt.TableID = &id // nolint
+	}
+
+	if res.TableIds.Valid {
+		tableIdsStr := strings.Split(res.TableIds.String, ",")
+		tableIds := make([]tables.TableID, len(tableIdsStr))
+		for i, idStr := range tableIdsStr {
+			tableID, err := tables.NewTableID(idStr)
+			if err != nil {
+				return gateway.Receipt{}, false, fmt.Errorf("parsing id string: %s", err)
+			}
+			tableIds[i] = tableID
 		}
-		receipt.TableID = &id
+		receipt.TableIDs = tableIds
 	}
 
 	return receipt, true, nil
diff --git a/internal/router/controllers/apiv1/model_transaction_receipt.go b/internal/router/controllers/apiv1/model_transaction_receipt.go
index bf8d16e..6b3c7e8 100644
--- a/internal/router/controllers/apiv1/model_transaction_receipt.go
+++ b/internal/router/controllers/apiv1/model_transaction_receipt.go
@@ -13,6 +13,8 @@ type TransactionReceipt struct {
 
 	TableId string `json:"table_id,omitempty"`
 
+	TableIds []string `json:"table_ids,omitempty"`
+
 	TransactionHash string `json:"transaction_hash,omitempty"`
 
 	BlockNumber int64 `json:"block_number,omitempty"`
diff --git a/internal/router/controllers/controller.go b/internal/router/controllers/controller.go
index 7356118..7994da9 100644
--- a/internal/router/controllers/controller.go
+++ b/internal/router/controllers/controller.go
@@ -130,14 +130,21 @@ func (c *Controller) GetReceiptByTransactionHash(rw http.ResponseWriter, r *http
 		BlockNumber:     receipt.BlockNumber,
 		ChainId:         int32(receipt.ChainID),
 	}
-	if receipt.TableID != nil {
-		receiptResponse.TableId = receipt.TableID.String()
+	if receipt.TableID != nil { // nolint
+		receiptResponse.TableId = receipt.TableID.String() // nolint
 	}
 	if receipt.Error != nil {
 		receiptResponse.Error_ = *receipt.Error
 		receiptResponse.ErrorEventIdx = int32(*receipt.ErrorEventIdx)
 	}
 
+	ids := make([]string, len(receipt.TableIDs))
+	for i, tblID := range receipt.TableIDs {
+		ids[i] = tblID.String()
+	}
+
+	receiptResponse.TableIds = ids
+
 	rw.Header().Set("Content-Type", "application/json")
 	rw.WriteHeader(http.StatusOK)
 	_ = json.NewEncoder(rw).Encode(receiptResponse)
diff --git a/pkg/database/db/models.go b/pkg/database/db/models.go
index b8287ba..75d7e22 100644
--- a/pkg/database/db/models.go
+++ b/pkg/database/db/models.go
@@ -84,4 +84,5 @@ type SystemTxnReceipt struct {
 	Error         sql.NullString
 	TableID       sql.NullInt64
 	ErrorEventIdx sql.NullInt64
+	TableIds      sql.NullString
 }
diff --git a/pkg/database/db/receipt.sql.go b/pkg/database/db/receipt.sql.go
index ab27757..1709b42 100644
--- a/pkg/database/db/receipt.sql.go
+++ b/pkg/database/db/receipt.sql.go
@@ -10,7 +10,7 @@ import (
 )
 
 const getReceipt = `-- name: GetReceipt :one
-SELECT chain_id, block_number, index_in_block, txn_hash, error, table_id, error_event_idx from system_txn_receipts WHERE chain_id=?1 and txn_hash=?2
+SELECT chain_id, block_number, index_in_block, txn_hash, error, table_id, error_event_idx, table_ids from system_txn_receipts WHERE chain_id=?1 and txn_hash=?2
 `
 
 type GetReceiptParams struct {
@@ -29,6 +29,7 @@ func (q *Queries) GetReceipt(ctx context.Context, arg GetReceiptParams) (SystemT
 		&i.Error,
 		&i.TableID,
 		&i.ErrorEventIdx,
+		&i.TableIds,
 	)
 	return i, err
 }
diff --git a/pkg/database/migrations/005_receipttableids.down.sql b/pkg/database/migrations/005_receipttableids.down.sql
new file mode 100644
index 0000000..d632f05
--- /dev/null
+++ b/pkg/database/migrations/005_receipttableids.down.sql
@@ -0,0 +1,1 @@
+ALTER TABLE system_txn_receipts DROP COLUMN table_ids;
\ No newline at end of file
diff --git a/pkg/database/migrations/005_receipttableids.up.sql b/pkg/database/migrations/005_receipttableids.up.sql
new file mode 100644
index 0000000..4b018cb
--- /dev/null
+++ b/pkg/database/migrations/005_receipttableids.up.sql
@@ -0,0 +1,3 @@
+ALTER TABLE system_txn_receipts ADD table_ids TEXT;
+
+UPDATE system_txn_receipts SET table_ids=cast(table_id as text) WHERE table_id is not null;
\ No newline at end of file
diff --git a/pkg/database/migrations/migrations.go b/pkg/database/migrations/migrations.go
index bb1b6d8..ff8d68e 100644
--- a/pkg/database/migrations/migrations.go
+++ b/pkg/database/migrations/migrations.go
@@ -9,6 +9,8 @@
 // migrations/003_evm_events.up.sql
 // migrations/004_system_id.down.sql
 // migrations/004_system_id.up.sql
+// migrations/005_receipttableids.down.sql
+// migrations/005_receipttableids.up.sql
 package migrations
 
 import (
@@ -100,7 +102,7 @@ func _001_initDownSql() (*asset, error) {
 		return nil, err
 	}
 
-	info := bindataFileInfo{name: "001_init.down.sql", size: 25, mode: os.FileMode(420), modTime: time.Unix(1678739618, 0)}
+	info := bindataFileInfo{name: "001_init.down.sql", size: 25, mode: os.FileMode(420), modTime: time.Unix(1682688746, 0)}
 	a := &asset{bytes: bytes, info: info}
 	return a, nil
 }
@@ -120,7 +122,7 @@ func _001_initUpSql() (*asset, error) {
 		return nil, err
 	}
 
-	info := bindataFileInfo{name: "001_init.up.sql", size: 1907, mode: os.FileMode(420), modTime: time.Unix(1678739618, 0)}
+	info := bindataFileInfo{name: "001_init.up.sql", size: 1907, mode: os.FileMode(420), modTime: time.Unix(1682688746, 0)}
 	a := &asset{bytes: bytes, info: info}
 	return a, nil
 }
@@ -140,7 +142,7 @@ func _002_receipterroridxDownSql() (*asset, error) {
 		return nil, err
 	}
 
-	info := bindataFileInfo{name: "002_receipterroridx.down.sql", size: 60, mode: os.FileMode(420), modTime: time.Unix(1678739618, 0)}
+	info := bindataFileInfo{name: "002_receipterroridx.down.sql", size: 60, mode: os.FileMode(420), modTime: time.Unix(1682688746, 0)}
 	a := &asset{bytes: bytes, info: info}
 	return a, nil
 }
@@ -160,7 +162,7 @@ func _002_receipterroridxUpSql() (*asset, error) {
 		return nil, err
 	}
 
-	info := bindataFileInfo{name: "002_receipterroridx.up.sql", size: 129, mode: os.FileMode(420), modTime: time.Unix(1678739618, 0)}
+	info := bindataFileInfo{name: "002_receipterroridx.up.sql", size: 129, mode: os.FileMode(420), modTime: time.Unix(1682688746, 0)}
 	a := &asset{bytes: bytes, info: info}
 	return a, nil
 }
@@ -180,7 +182,7 @@ func _003_evm_eventsDownSql() (*asset, error) {
 		return nil, err
 	}
 
-	info := bindataFileInfo{name: "003_evm_events.down.sql", size: 59, mode: os.FileMode(420), modTime: time.Unix(1678739618, 0)}
+	info := bindataFileInfo{name: "003_evm_events.down.sql", size: 59, mode: os.FileMode(420), modTime: time.Unix(1682688746, 0)}
 	a := &asset{bytes: bytes, info: info}
 	return a, nil
 }
@@ -200,7 +202,7 @@ func _003_evm_eventsUpSql() (*asset, error) {
 		return nil, err
 	}
 
-	info := bindataFileInfo{name: "003_evm_events.up.sql", size: 701, mode: os.FileMode(420), modTime: time.Unix(1678739618, 0)}
+	info := bindataFileInfo{name: "003_evm_events.up.sql", size: 701, mode: os.FileMode(420), modTime: time.Unix(1682688746, 0)}
 	a := &asset{bytes: bytes, info: info}
 	return a, nil
 }
@@ -220,7 +222,7 @@ func _004_system_idDownSql() (*asset, error) {
 		return nil, err
 	}
 
-	info := bindataFileInfo{name: "004_system_id.down.sql", size: 21, mode: os.FileMode(420), modTime: time.Unix(1678739618, 0)}
+	info := bindataFileInfo{name: "004_system_id.down.sql", size: 21, mode: os.FileMode(420), modTime: time.Unix(1682688746, 0)}
 	a := &asset{bytes: bytes, info: info}
 	return a, nil
 }
@@ -240,7 +242,47 @@ func _004_system_idUpSql() (*asset, error) {
 		return nil, err
 	}
 
-	info := bindataFileInfo{name: "004_system_id.up.sql", size: 84, mode: os.FileMode(420), modTime: time.Unix(1678739618, 0)}
+	info := bindataFileInfo{name: "004_system_id.up.sql", size: 84, mode: os.FileMode(420), modTime: time.Unix(1682688746, 0)}
+	a := &asset{bytes: bytes, info: info}
+	return a, nil
+}
+
+var __005_receipttableidsDownSql = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x72\xf4\x09\x71\x0d\x52\x08\x71\x74\xf2\x71\x55\x28\xae\x2c\x2e\x49\xcd\x8d\x2f\xa9\xc8\x8b\x2f\x4a\x4d\x4e\xcd\x2c\x28\x29\x56\x70\x09\xf2\x0f\x50\x70\xf6\xf7\x09\xf5\xf5\x53\x28\x49\x4c\xca\x49\x8d\xcf\x4c\x29\xb6\x06\x04\x00\x00\xff\xff\xa6\x5c\x8c\x10\x36\x00\x00\x00")
+
+func _005_receipttableidsDownSqlBytes() ([]byte, error) {
+	return bindataRead(
+		__005_receipttableidsDownSql,
+		"005_receipttableids.down.sql",
+	)
+}
+
+func _005_receipttableidsDownSql() (*asset, error) {
+	bytes, err := _005_receipttableidsDownSqlBytes()
+	if err != nil {
+		return nil, err
+	}
+
+	info := bindataFileInfo{name: "005_receipttableids.down.sql", size: 54, mode: os.FileMode(420), modTime: time.Unix(1682951713, 0)}
+	a := &asset{bytes: bytes, info: info}
+	return a, nil
+}
+
+var __005_receipttableidsUpSql = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x72\xf4\x09\x71\x0d\x52\x08\x71\x74\xf2\x71\x55\x28\xae\x2c\x2e\x49\xcd\x8d\x2f\xa9\xc8\x8b\x2f\x4a\x4d\x4e\xcd\x2c\x28\x29\x56\x70\x74\x71\x51\x28\x49\x4c\xca\x49\x8d\xcf\x4c\x29\x56\x08\x71\x8d\x08\xb1\xe6\xe2\x0a\x0d\x70\x71\x0c\xc1\xae\x3e\xd8\x35\x04\xa1\xde\x36\x39\xb1\xb8\x44\x03\xc6\x55\x48\x2c\x56\x28\x49\xad\x28\xd1\x54\x08\xf7\x70\x0d\x72\x85\x2b\x53\xc8\x2c\x56\xc8\xcb\x2f\x51\xc8\x2b\xcd\xc9\xb1\x06\x04\x00\x00\xff\xff\xf0\xad\x75\x24\x90\x00\x00\x00")
+
+func _005_receipttableidsUpSqlBytes() ([]byte, error) {
+	return bindataRead(
+		__005_receipttableidsUpSql,
+		"005_receipttableids.up.sql",
+	)
+}
+
+func _005_receipttableidsUpSql() (*asset, error) {
+	bytes, err := _005_receipttableidsUpSqlBytes()
+	if err != nil {
+		return nil, err
+	}
+
+	info := bindataFileInfo{name: "005_receipttableids.up.sql", size: 144, mode: os.FileMode(420), modTime: time.Unix(1682951686, 0)}
 	a := &asset{bytes: bytes, info: info}
 	return a, nil
 }
@@ -305,6 +347,8 @@ var _bindata = map[string]func() (*asset, error){
 	"003_evm_events.up.sql":        _003_evm_eventsUpSql,
 	"004_system_id.down.sql":       _004_system_idDownSql,
 	"004_system_id.up.sql":         _004_system_idUpSql,
+	"005_receipttableids.down.sql": _005_receipttableidsDownSql,
+	"005_receipttableids.up.sql":   _005_receipttableidsUpSql,
 }
 
 // AssetDir returns the file names below a certain
@@ -356,6 +400,8 @@ var _bintree = &bintree{nil, map[string]*bintree{
 	"003_evm_events.up.sql":        &bintree{_003_evm_eventsUpSql, map[string]*bintree{}},
 	"004_system_id.down.sql":       &bintree{_004_system_idDownSql, map[string]*bintree{}},
 	"004_system_id.up.sql":         &bintree{_004_system_idUpSql, map[string]*bintree{}},
+	"005_receipttableids.down.sql": &bintree{_005_receipttableidsDownSql, map[string]*bintree{}},
+	"005_receipttableids.up.sql":   &bintree{_005_receipttableidsUpSql, map[string]*bintree{}},
 }}
 
 // RestoreAsset restores an asset under the given directory
diff --git a/pkg/eventprocessor/eventprocessor.go b/pkg/eventprocessor/eventprocessor.go
index cb8ae3c..b030d58 100644
--- a/pkg/eventprocessor/eventprocessor.go
+++ b/pkg/eventprocessor/eventprocessor.go
@@ -78,7 +78,10 @@ type Receipt struct {
 	IndexInBlock int64
 	TxnHash      string
 
-	TableID       *tables.TableID
+	TableIDs      tables.TableIDs
 	Error         *string
 	ErrorEventIdx *int
+
+	// Deprecated
+	TableID *tables.TableID
 }
diff --git a/pkg/eventprocessor/impl/eventprocessor.go b/pkg/eventprocessor/impl/eventprocessor.go
index 0cf9e90..05226be 100644
--- a/pkg/eventprocessor/impl/eventprocessor.go
+++ b/pkg/eventprocessor/impl/eventprocessor.go
@@ -244,14 +244,16 @@ func (ep *EventProcessor) executeBlock(ctx context.Context, block eventfeed.Bloc
 			return fmt.Errorf("executing txn events: %s", err)
 		}
 		receipt := eventprocessor.Receipt{
-			ChainID:      ep.chainID,
-			BlockNumber:  block.BlockNumber,
-			IndexInBlock: int64(idxInBlock),
-			TxnHash:      txnEvents.TxnHash.Hex(),
-
-			TableID:       txnExecResult.TableID,
+			ChainID:       ep.chainID,
+			BlockNumber:   block.BlockNumber,
+			IndexInBlock:  int64(idxInBlock),
+			TxnHash:       txnEvents.TxnHash.Hex(),
+			TableIDs:      txnExecResult.TableIDs,
 			Error:         txnExecResult.Error,
 			ErrorEventIdx: txnExecResult.ErrorEventIdx,
+
+			// Deprecated
+			TableID: txnExecResult.TableID,
 		}
 		receipts = append(receipts, receipt)
 
diff --git a/pkg/eventprocessor/impl/executor/executor.go b/pkg/eventprocessor/impl/executor/executor.go
index 313fe4b..4379dae 100644
--- a/pkg/eventprocessor/impl/executor/executor.go
+++ b/pkg/eventprocessor/impl/executor/executor.go
@@ -55,10 +55,13 @@ type BlockScope interface {
 
 // TxnExecutionResult contains the result of executing a txn with all contained events.
 type TxnExecutionResult struct {
-	TableID *tables.TableID
+	TableIDs []tables.TableID
 
 	Error         *string
 	ErrorEventIdx *int
+
+	// Deprecated
+	TableID *tables.TableID
 }
 
 // StateHash represents the state of the database at given block number for a particular chain id.
diff --git a/pkg/eventprocessor/impl/executor/impl/blockscope.go b/pkg/eventprocessor/impl/executor/impl/blockscope.go
index 1bab319..55f4085 100644
--- a/pkg/eventprocessor/impl/executor/impl/blockscope.go
+++ b/pkg/eventprocessor/impl/executor/impl/blockscope.go
@@ -137,14 +137,23 @@ func (bs *blockScope) SaveTxnReceipts(ctx context.Context, rs []eventprocessor.R
 			tableID.Valid = true
 			tableID.Int64 = r.TableID.ToBigInt().Int64()
 		}
+
 		if r.Error != nil {
 			*r.Error = strings.ToValidUTF8(*r.Error, "")
 		}
+
+		tableIDs := sql.NullString{Valid: false}
+		if len(r.TableIDs) > 0 {
+			tableIDs.Valid = true
+			tableIDs.String = r.TableIDs.String()
+		}
+
 		if _, err := bs.txn.ExecContext(
 			ctx,
-			`INSERT INTO system_txn_receipts (chain_id,txn_hash,error,error_event_idx,table_id,block_number,index_in_block) 
-				 VALUES (?1,?2,?3,?4,?5,?6,?7)`,
-			r.ChainID, r.TxnHash, r.Error, r.ErrorEventIdx, tableID, r.BlockNumber, r.IndexInBlock); err != nil {
+			`INSERT INTO system_txn_receipts 
+				(chain_id,txn_hash,error,error_event_idx,table_id,block_number,index_in_block,table_ids) 
+				VALUES (?1,?2,?3,?4,?5,?6,?7,?8)`,
+			r.ChainID, r.TxnHash, r.Error, r.ErrorEventIdx, tableID, r.BlockNumber, r.IndexInBlock, tableIDs); err != nil {
 			return fmt.Errorf("insert txn receipt: %s", err)
 		}
 	}
diff --git a/pkg/eventprocessor/impl/executor/impl/txnscope.go b/pkg/eventprocessor/impl/executor/impl/txnscope.go
index 763a79c..2a0cc66 100644
--- a/pkg/eventprocessor/impl/executor/impl/txnscope.go
+++ b/pkg/eventprocessor/impl/executor/impl/txnscope.go
@@ -56,6 +56,7 @@ func (ts *txnScope) executeTxnEvents(
 	var res eventExecutionResult
 	var err error
 
+	tableIDs := make([]tables.TableID, 0)
 	for idx, event := range evmTxn.Events {
 		switch event := event.(type) {
 		case *ethereum.ContractRunSQL:
@@ -107,9 +108,16 @@ func (ts *txnScope) executeTxnEvents(
 				ErrorEventIdx: &idx,
 			}, nil
 		}
+
+		if res.TableID != nil {
+			tableIDs = append(tableIDs, *res.TableID)
+		}
 	}
 
-	return executor.TxnExecutionResult{TableID: res.TableID}, nil
+	return executor.TxnExecutionResult{
+		TableID:  res.TableID,
+		TableIDs: tableIDs,
+	}, nil
 }
 
 // AccessControlDTO data structure from database.
diff --git a/pkg/tables/tables.go b/pkg/tables/tables.go
index 6518e19..62f2453 100644
--- a/pkg/tables/tables.go
+++ b/pkg/tables/tables.go
@@ -4,6 +4,7 @@ import (
 	"context"
 	"fmt"
 	"math/big"
+	"strings"
 
 	"github.com/ethereum/go-ethereum/common"
 )
@@ -11,6 +12,18 @@ import (
 // TableID is the ID of a Table.
 type TableID big.Int
 
+// TableIDs is a list of TableID.
+type TableIDs []TableID
+
+// String transform a list of TableIds into a string.
+func (ids TableIDs) String() string {
+	tableIdsStr := make([]string, len(ids))
+	for i, tableID := range ids {
+		tableIdsStr[i] = tableID.String()
+	}
+	return strings.Join(tableIdsStr, ",")
+}
+
 // String returns a string representation of the TableID.
 func (tid TableID) String() string {
 	bi := (big.Int)(tid)


Test patch:
diff --git a/docker/deployed/testnet/healthbot/config.json b/docker/deployed/testnet/healthbot/config.json
index 0565631..9ca77fd 100644
--- a/docker/deployed/testnet/healthbot/config.json
+++ b/docker/deployed/testnet/healthbot/config.json
@@ -7,16 +7,6 @@
         "Debug": true
     },
     "Chains": [
-        {
-            "ChainID": 421613,
-            "WalletPrivateKey": "${HEALTHBOT_ARBITRUM_GOERLI_PRIVATE_KEY}",
-            "AlchemyAPIKey": "${HEALTHBOT_ALCHEMY_ARBITRUM_GOERLI_API_KEY}",
-            "Probe": {
-                "CheckInterval": "1.5h",
-                "ReceiptTimeout": "25s",
-                "Tablename": "${HEALTHBOT_ARBITRUM_GOERLI_TABLE}"
-            }
-        },
         {
             "ChainID": 11155111,
             "WalletPrivateKey": "${HEALTHBOT_ETHEREUM_SEPOLIA_PRIVATE_KEY}",
@@ -41,16 +31,6 @@
                 "Tablename": "${HEALTHBOT_POLYGON_MUMBAI_TABLE}"
             }
         },
-        {
-            "ChainID": 420,
-            "WalletPrivateKey": "${HEALTHBOT_OPTIMISM_GOERLI_PRIVATE_KEY}",
-            "AlchemyAPIKey": "${HEALTHBOT_ALCHEMY_OPTIMISM_GOERLI_API_KEY}",
-            "Probe": {
-                "CheckInterval": "1.5h",
-                "ReceiptTimeout": "25s",
-                "Tablename": "${HEALTHBOT_OPTIMISM_GOERLI_TABLE}"
-            }
-        },
         {
             "ChainID": 3141,
             "WalletPrivateKey": "${HEALTHBOT_FILECOIN_HYPERSPACE_PRIVATE_KEY}",
diff --git a/internal/router/controllers/controller_test.go b/internal/router/controllers/controller_test.go
index d1b06cc..9c917ab 100644
--- a/internal/router/controllers/controller_test.go
+++ b/internal/router/controllers/controller_test.go
@@ -4,6 +4,7 @@ import (
 	"context"
 	"errors"
 	"fmt"
+	"math/big"
 	"net/http"
 	"net/http/httptest"
 	"strconv"
@@ -17,6 +18,7 @@ import (
 	"github.com/textileio/go-tableland/internal/router/middlewares"
 	"github.com/textileio/go-tableland/internal/tableland"
 	"github.com/textileio/go-tableland/mocks"
+	"github.com/textileio/go-tableland/pkg/tables"
 )
 
 func TestQuery(t *testing.T) {
@@ -244,6 +246,40 @@ func TestTableNotFoundMock(t *testing.T) {
 	require.JSONEq(t, expJSON, rr.Body.String())
 }
 
+func TestReceipt(t *testing.T) {
+	r := mocks.NewGateway(t)
+	r.EXPECT().GetReceiptByTransactionHash(mock.Anything, mock.Anything, mock.Anything).Return(
+		gateway.Receipt{
+			ChainID:       1337,
+			BlockNumber:   1,
+			IndexInBlock:  0,
+			TxnHash:       "0xb5c8bd9430b6cc87a0e2fe110ece6bf527fa4f170a4bc8cd032f768fc5219838",
+			TableIDs:      []tables.TableID{tables.TableID(*big.NewInt(1)), tables.TableID(*big.NewInt(2))},
+			Error:         nil,
+			ErrorEventIdx: nil,
+		},
+		true,
+		nil,
+	)
+
+	ctrl := NewController(r)
+
+	router := mux.NewRouter()
+	router.HandleFunc("/receipt/{chainId}/{transactionHash}", ctrl.GetReceiptByTransactionHash)
+
+	ctx := context.WithValue(context.Background(), middlewares.ContextKeyChainID, tableland.ChainID(1337))
+	// Table format
+	req, err := http.NewRequestWithContext(
+		ctx, "GET", "/receipt/1337/0xb5c8bd9430b6cc87a0e2fe110ece6bf527fa4f170a4bc8cd032f768fc5219838", nil,
+	)
+	require.NoError(t, err)
+	rr := httptest.NewRecorder()
+	router.ServeHTTP(rr, req)
+	require.Equal(t, http.StatusOK, rr.Code)
+	exp := `{"table_ids":["1","2"],"transaction_hash":"0xb5c8bd9430b6cc87a0e2fe110ece6bf527fa4f170a4bc8cd032f768fc5219838","block_number":1,"chain_id":1337}` // nolint
+	require.JSONEq(t, exp, rr.Body.String())
+}
+
 func parseJSONLString(val string) []string {
 	s := strings.TrimRight(val, "\n")
 	return strings.Split(s, "\n")
diff --git a/internal/tableland/impl/tableland_test.go b/internal/tableland/impl/tableland_test.go
index 104e63e..bb695ce 100644
--- a/internal/tableland/impl/tableland_test.go
+++ b/internal/tableland/impl/tableland_test.go
@@ -657,11 +657,11 @@ func requireReceipts(
 		require.NotZero(t, receipt.BlockNumber)
 		if ok {
 			require.Empty(t, receipt.Error)
-			require.NotNil(t, receipt.TableID)
-			require.NotZero(t, receipt.TableID)
+			require.NotNil(t, receipt.TableID)  // nolint
+			require.NotZero(t, receipt.TableID) // nolint
 		} else {
 			require.NotEmpty(t, receipt.Error)
-			require.Nil(t, receipt.TableID)
+			require.Nil(t, receipt.TableID) // nolint
 		}
 	}
 }
diff --git a/pkg/eventprocessor/impl/eventprocessor_replayhistory_test.go b/pkg/eventprocessor/impl/eventprocessor_replayhistory_test.go
index e09b105..bd0fe82 100644
--- a/pkg/eventprocessor/impl/eventprocessor_replayhistory_test.go
+++ b/pkg/eventprocessor/impl/eventprocessor_replayhistory_test.go
@@ -37,14 +37,14 @@ func TestReplayProductionHistory(t *testing.T) {
 	}
 
 	expectedStateHashes := map[tableland.ChainID]string{
-		1:      "bce26781eed109b8aaae2d1f688c134831fdf061",
-		5:      "21684ad97813634841393a097436e397de96692f",
-		10:     "1aa835eec9a9ac08cc2784d9d29df7fb15409d08",
-		69:     "fd1ba648c9406c0af321cb734eb203c742fff2a3",
-		137:    "fd1da780698b394a352b59e9b0c124f9cf010b67",
-		420:    "639dda72b6e4a5a8ef7ceb2b734e0b6ecc241407",
-		80001:  "50d477f69179aa0b027c8df7661666c7b5676699",
-		421613: "d58fd380066628fa92fd8a87831ea744b9ba1d8b",
+		1:      "55880bbeecd247f20c0e75e15b6ddc3a432b46f0",
+		5:      "4d9a36da2718fd0cd2f6664147358d89ab7fafa9",
+		10:     "2151d711fc50a32b7e0cbdebce7f48aa0c274ce5",
+		69:     "b1136bd05118349be32372509f05c240180a93d3",
+		137:    "12b3d0aa62b4e61b10ea81c16bf050f8c27b1dca",
+		420:    "058bd19e7874fa3c9436b0cebbcf5846f7c347f3",
+		80001:  "5c9709607bd9da6e5e80df04e9ec2b30b488793d",
+		421613: "18ae0ef43cdedc548706c61dccae533411f9d514",
 	}
 
 	historyDBURI := getHistoryDBURI(t)
diff --git a/pkg/eventprocessor/impl/eventprocessor_test.go b/pkg/eventprocessor/impl/eventprocessor_test.go
index 418818d..ab63288 100644
--- a/pkg/eventprocessor/impl/eventprocessor_test.go
+++ b/pkg/eventprocessor/impl/eventprocessor_test.go
@@ -58,10 +58,11 @@ func TestRunSQLBlockProcessing(t *testing.T) {
 		txnHashes := contractCalls.runSQL(queries)
 
 		expReceipt := eventprocessor.Receipt{
-			ChainID: chainID,
-			TxnHash: txnHashes[0].String(),
-			Error:   nil,
-			TableID: &tableID,
+			ChainID:  chainID,
+			TxnHash:  txnHashes[0].String(),
+			Error:    nil,
+			TableID:  &tableID,
+			TableIDs: []tables.TableID{tableID},
 		}
 		require.Eventually(t, checkReceipts(t, expReceipt), time.Second*5, time.Millisecond*100)
 
@@ -76,10 +77,11 @@ func TestRunSQLBlockProcessing(t *testing.T) {
 		txnHashes := contractCalls.runSQL(queries)
 
 		expReceipt := eventprocessor.Receipt{
-			ChainID: chainID,
-			TxnHash: txnHashes[0].String(),
-			Error:   &expWrongTypeErr,
-			TableID: nil,
+			ChainID:  chainID,
+			TxnHash:  txnHashes[0].String(),
+			Error:    &expWrongTypeErr,
+			TableID:  nil,
+			TableIDs: nil,
 		}
 		require.Eventually(t, checkReceipts(t, expReceipt), time.Second*5, time.Millisecond*100)
 
@@ -101,6 +103,7 @@ func TestRunSQLBlockProcessing(t *testing.T) {
 				IndexInBlock: int64(i),
 				Error:        nil,
 				TableID:      &tableID,
+				TableIDs:     []tables.TableID{tableID},
 			}
 		}
 		require.Eventually(t, checkReceipts(t, expReceipts...), time.Second*5, time.Millisecond*100)
@@ -122,6 +125,7 @@ func TestRunSQLBlockProcessing(t *testing.T) {
 			IndexInBlock: 0,
 			Error:        &expWrongTypeErr,
 			TableID:      nil,
+			TableIDs:     nil,
 		}
 		expReceipts[1] = eventprocessor.Receipt{
 			ChainID:      chainID,
@@ -129,6 +133,7 @@ func TestRunSQLBlockProcessing(t *testing.T) {
 			IndexInBlock: 1,
 			Error:        nil,
 			TableID:      &tableID,
+			TableIDs:     []tables.TableID{tableID},
 		}
 		require.Eventually(t, checkReceipts(t, expReceipts...), time.Second*5, time.Millisecond*100)
 
@@ -148,6 +153,7 @@ func TestRunSQLBlockProcessing(t *testing.T) {
 			IndexInBlock: 0,
 			Error:        nil,
 			TableID:      &tableID,
+			TableIDs:     []tables.TableID{tableID},
 		}
 		expReceipts[1] = eventprocessor.Receipt{
 			ChainID:      chainID,
@@ -155,6 +161,7 @@ func TestRunSQLBlockProcessing(t *testing.T) {
 			IndexInBlock: 1,
 			Error:        &expWrongTypeErr,
 			TableID:      nil,
+			TableIDs:     nil,
 		}
 		require.Eventually(t, checkReceipts(t, expReceipts...), time.Second*5, time.Millisecond*100)
 
@@ -176,10 +183,11 @@ func TestCreateTableBlockProcessing(t *testing.T) {
 			tableID, err := tables.NewTableID(strconv.Itoa(i + 2))
 			require.NoError(t, err)
 			expReceipt := eventprocessor.Receipt{
-				ChainID: chainID,
-				TxnHash: txnHash.String(),
-				Error:   nil,
-				TableID: &tableID,
+				ChainID:  chainID,
+				TxnHash:  txnHash.String(),
+				Error:    nil,
+				TableID:  &tableID,
+				TableIDs: []tables.TableID{tableID},
 			}
 			require.Eventually(t, checkReceipts(t, expReceipt), time.Second*5, time.Millisecond*100)
 		}
@@ -193,10 +201,11 @@ func TestCreateTableBlockProcessing(t *testing.T) {
 		txnHash := contractCalls.createTable("CREATEZ TABLE Foo_1337 (bar int)")
 
 		expReceipt := eventprocessor.Receipt{
-			ChainID: chainID,
-			TxnHash: txnHash.String(),
-			Error:   &expWrongTypeErr,
-			TableID: nil,
+			ChainID:  chainID,
+			TxnHash:  txnHash.String(),
+			Error:    &expWrongTypeErr,
+			TableID:  nil,
+			TableIDs: nil,
 		}
 		require.Eventually(t, checkReceipts(t, expReceipt), time.Second*5, time.Millisecond*100)
 	})
@@ -214,10 +223,11 @@ func TestQueryWithWrongTableTarget(t *testing.T) {
 
 	expErr := "query targets table id 9999 and not 1"
 	expReceipt := eventprocessor.Receipt{
-		ChainID: chainID,
-		TxnHash: txnHashes[0].String(),
-		Error:   &expErr,
-		TableID: nil,
+		ChainID:  chainID,
+		TxnHash:  txnHashes[0].String(),
+		Error:    &expErr,
+		TableID:  nil,
+		TableIDs: nil,
 	}
 	require.Eventually(t, checkReceipts(t, expReceipt), time.Second*5, time.Millisecond*100)
 }
@@ -233,10 +243,11 @@ func TestSetController(t *testing.T) {
 
 		tid := tables.TableID(*big.NewInt(1))
 		expReceipt := eventprocessor.Receipt{
-			ChainID: chainID,
-			TxnHash: txnHash.Hex(),
-			Error:   nil,
-			TableID: &tid,
+			ChainID:  chainID,
+			TxnHash:  txnHash.Hex(),
+			Error:    nil,
+			TableID:  &tid,
+			TableIDs: []tables.TableID{tid},
 		}
 		require.Eventually(t, checkReceipts(t, expReceipt), time.Second*5, time.Millisecond*100)
 	})
@@ -247,10 +258,11 @@ func TestSetController(t *testing.T) {
 
 		tid := tables.TableID(*big.NewInt(1))
 		expReceipt := eventprocessor.Receipt{
-			ChainID: chainID,
-			TxnHash: txnHash.Hex(),
-			Error:   nil,
-			TableID: &tid,
+			ChainID:  chainID,
+			TxnHash:  txnHash.Hex(),
+			Error:    nil,
+			TableID:  &tid,
+			TableIDs: []tables.TableID{tid},
 		}
 		require.Eventually(t, checkReceipts(t, expReceipt), time.Second*5, time.Millisecond*100)
 	})
@@ -265,10 +277,11 @@ func TestTransfer(t *testing.T) {
 	tableID, err := tables.NewTableID("2")
 	require.NoError(t, err)
 	expReceipt := eventprocessor.Receipt{
-		ChainID: chainID,
-		TxnHash: txnHash.String(),
-		Error:   nil,
-		TableID: &tableID,
+		ChainID:  chainID,
+		TxnHash:  txnHash.String(),
+		Error:    nil,
+		TableID:  &tableID,
+		TableIDs: []tables.TableID{tableID},
 	}
 	require.Eventually(t, checkReceipts(t, expReceipt), time.Second*5, time.Millisecond*100)
 
@@ -278,10 +291,11 @@ func TestTransfer(t *testing.T) {
 
 		tid := tables.TableID(*big.NewInt(1))
 		expReceipt := eventprocessor.Receipt{
-			ChainID: chainID,
-			TxnHash: txnHash.Hex(),
-			Error:   nil,
-			TableID: &tid,
+			ChainID:  chainID,
+			TxnHash:  txnHash.Hex(),
+			Error:    nil,
+			TableID:  &tid,
+			TableIDs: []tables.TableID{tid},
 		}
 		require.Eventually(t, checkReceipts(t, expReceipt), time.Second*5, time.Millisecond*100)
 	})
@@ -411,7 +425,8 @@ func setup(t *testing.T) (
 				require.Equal(t, expReceipt.IndexInBlock, gotReceipt.IndexInBlock)
 				require.Equal(t, expReceipt.TxnHash, gotReceipt.TxnHash)
 				require.Equal(t, expReceipt.Error, gotReceipt.Error)
-				require.Equal(t, expReceipt.TableID, gotReceipt.TableID)
+				require.Equal(t, expReceipt.TableID, gotReceipt.TableID) // nolint
+				require.Equal(t, expReceipt.TableIDs, tables.TableIDs(gotReceipt.TableIDs))
 			}
 			return true
 		}