Repository: tablelandnetwork/go-tableland
Pull request #423 (2022-12-19 14:54:17)

healthbot: stop using on validator transaction relaying
# Summary

Closes https://github.com/tablelandnetwork/go-tableland/issues/400

# Context

In the upcoming API v1, validators won't be offering transaction relaying. Our current Healthbot implementation 

**Note: this PR is ready for review, but keeping it as draft until Bruno comes back**

# Implementation overview

The implementation goes along the lines of a previously stalled attempt #279 with some slight changes.

The infrastructure work we have to in each validator is to update the `deployed/<env>/healthbot/.env_healthbot` files removing the previous `HEALTHBOT_<chainName>_SIWE` and adding `HEALTHBOT_<chainName>_PRIVATE_KEY`.

# Implementation details and review orientation

NA

# Checklist
- [X]  Are changes backward compatible with existing SDKs, or is there a plan to manage it correctly?
- [X]  Are changes covered by existing tests, or were new tests included?
- [X]  Are code changes optimized for future code readers, commenting on problematic areas to understand (if any)?
- [X]  Future-self question: Did you avoid unjustified/unnecessary complexity to achieve the goal?



Patch:
diff --git a/cmd/healthbot/config.go b/cmd/healthbot/config.go
index 91cb710..2ef1925 100644
--- a/cmd/healthbot/config.go
+++ b/cmd/healthbot/config.go
@@ -22,17 +22,16 @@ type config struct {
 		Human bool `default:"false"`
 		Debug bool `default:"false"`
 	}
-	Target string `default:""`
 	Chains []ChainConfig
 }
 
 // ChainConfig contains probe configuration for a particular chain.
 type ChainConfig struct {
-	Name  string
-	Probe struct {
+	ChainID          int
+	WalletPrivateKey string
+	Probe            struct {
 		CheckInterval  string `default:"15s"`
 		ReceiptTimeout string `default:"20s"`
-		SIWE           string `default:""`
 		Tablename      string `default:""`
 	}
 }
diff --git a/cmd/healthbot/counterprobe/counterprobe.go b/cmd/healthbot/counterprobe/counterprobe.go
index 037d475..6aa8712 100644
--- a/cmd/healthbot/counterprobe/counterprobe.go
+++ b/cmd/healthbot/counterprobe/counterprobe.go
@@ -4,17 +4,14 @@ import (
 	"context"
 	"errors"
 	"fmt"
-	"net/url"
 	"sync"
 	"time"
 
 	"github.com/rs/zerolog"
 	logger "github.com/rs/zerolog/log"
-	"github.com/textileio/go-tableland/internal/router/controllers/legacy"
+	clientV1 "github.com/textileio/go-tableland/pkg/client/v1"
 	"go.opentelemetry.io/otel/attribute"
 	"go.opentelemetry.io/otel/metric/instrument/syncint64"
-
-	"github.com/ethereum/go-ethereum/rpc"
 )
 
 const (
@@ -24,14 +21,13 @@ const (
 // CounterProbe allows running an e2e probe for a pre-minted table
 // that has a counter column.
 type CounterProbe struct {
-	log zerolog.Logger
+	log    zerolog.Logger
+	client *clientV1.Client
 
 	checkInterval  time.Duration
 	receiptTimeout time.Duration
 	tableName      string
 
-	rpcClient *rpc.Client
-
 	lock                 sync.RWMutex
 	mLastCounterValue    int64
 	mLastCheck           time.Time
@@ -43,8 +39,7 @@ type CounterProbe struct {
 // New returns a *CounterProbe.
 func New(
 	chainName string,
-	endpoint string,
-	siwe string,
+	client *clientV1.Client,
 	tableName string,
 	checkInterval time.Duration,
 	receiptTimeout time.Duration,
@@ -55,24 +50,16 @@ func New(
 		Logger()
 
 	if receiptTimeout == 0 {
-		return nil, fmt.Errorf("receipt timeout can't be zero")
+		return nil, errors.New("receipt timeout can't be zero")
 	}
 	if len(tableName) == 0 {
 		return nil, errors.New("tablename is empty")
 	}
-	if _, err := url.ParseQuery(endpoint); err != nil {
-		return nil, fmt.Errorf("invalid endpoint target: %s", err)
-	}
-	rpcClient, err := rpc.Dial(endpoint)
-	if err != nil {
-		return nil, fmt.Errorf("creating jsonrpc client: %s", err)
-	}
-	rpcClient.SetHeader("Authorization", "Bearer "+siwe)
 
 	cp := &CounterProbe{
 		log:            log,
 		checkInterval:  checkInterval,
-		rpcClient:      rpcClient,
+		client:         client,
 		tableName:      tableName,
 		receiptTimeout: receiptTimeout,
 	}
@@ -149,57 +136,35 @@ func (cp *CounterProbe) healthCheck(ctx context.Context) (int64, error) {
 }
 
 func (cp *CounterProbe) increaseCounterValue(ctx context.Context) error {
-	updateCounterReq := legacy.RelayWriteQueryRequest{
-		Statement: fmt.Sprintf("update %s set counter=counter+1", cp.tableName),
-	}
-	var updateCounterRes legacy.RelayWriteQueryResponse
-	if err := cp.rpcClient.CallContext(ctx, &updateCounterRes, "tableland_relayWriteQuery", updateCounterReq); err != nil {
-		return fmt.Errorf("calling tableland_runReadQuery: %s", err)
-	}
-
-	getReceiptRequest := legacy.GetReceiptRequest{
-		TxnHash: updateCounterRes.Transaction.Hash,
+	txnHash, err := cp.client.Write(ctx, fmt.Sprintf("update %s set counter=counter+1", cp.tableName))
+	if err != nil {
+		return fmt.Errorf("calling client Write: %s", err)
 	}
 
 	start := time.Now()
-	deadline := time.Now().Add(cp.receiptTimeout)
-	for time.Now().Before(deadline) {
-		var getReceiptResponse legacy.GetReceiptResponse
-		if err := cp.rpcClient.CallContext(ctx, &getReceiptResponse, "tableland_getReceipt", getReceiptRequest); err != nil {
-			return fmt.Errorf("calling tableland_getReceipt: %s", err)
-		}
-		if getReceiptResponse.Ok {
-			if getReceiptResponse.Receipt.Error != "" {
-				return fmt.Errorf("receipt found but has an error %s", getReceiptResponse.Receipt.Error)
-			}
-			cp.log.Info().Int64("duration", time.Since(start).Milliseconds()).Msg("receipt confirmed")
-			return nil
-		}
-		time.Sleep(time.Second * 5)
-	}
-
-	return fmt.Errorf("timed out waiting for receipt %s", getReceiptRequest.TxnHash)
-}
-
-func (cp *CounterProbe) getCurrentCounterValue(ctx context.Context) (int64, error) {
-	output := "table"
-	getCounterReq := legacy.RunReadQueryRequest{
-		Statement: fmt.Sprintf("select * from %s", cp.tableName),
-		Output:    &output,
-	}
-
-	type data struct {
-		Rows [][]int64 `json:"rows"`
-	}
-	var getCounterRes struct {
-		Result data `json:"data"`
+	receipt, ok, err := cp.client.Receipt(ctx, txnHash, clientV1.WaitFor(cp.receiptTimeout))
+	if err != nil {
+		return fmt.Errorf("calling tableland_getReceipt: %s", err)
 	}
-	if err := cp.rpcClient.CallContext(ctx, &getCounterRes, "tableland_runReadQuery", getCounterReq); err != nil {
-		return 0, fmt.Errorf("calling tableland_runSQL: %s", err)
+	if !ok {
+		return fmt.Errorf("waiting for receipt %s timed out", txnHash)
 	}
-	if len(getCounterRes.Result.Rows) != 1 || len(getCounterRes.Result.Rows[0]) != 1 {
-		return 0, fmt.Errorf("unexpected response format")
+	if receipt.Error_ != "" {
+		return fmt.Errorf("receipt found but has an error %s", receipt.Error_)
 	}
+	cp.log.Info().Int64("duration", time.Since(start).Milliseconds()).Msg("receipt confirmed")
+	return nil
+}
 
-	return getCounterRes.Result.Rows[0][0], nil
+func (cp *CounterProbe) getCurrentCounterValue(ctx context.Context) (int64, error) {
+	var counter int64
+	if err := cp.client.Read(
+		ctx,
+		fmt.Sprintf("select counter from %s", cp.tableName),
+		&counter,
+		clientV1.ReadExtract(),
+		clientV1.ReadUnwrap()); err != nil {
+		return 0, fmt.Errorf("calling read query: %s", err)
+	}
+	return counter, nil
 }
diff --git a/cmd/healthbot/main.go b/cmd/healthbot/main.go
index 54679f2..0aa6a99 100644
--- a/cmd/healthbot/main.go
+++ b/cmd/healthbot/main.go
@@ -11,8 +11,11 @@ import (
 	"github.com/rs/zerolog/log"
 	"github.com/textileio/go-tableland/buildinfo"
 	"github.com/textileio/go-tableland/cmd/healthbot/counterprobe"
+	"github.com/textileio/go-tableland/pkg/client"
+	clientV1 "github.com/textileio/go-tableland/pkg/client/v1"
 	"github.com/textileio/go-tableland/pkg/logging"
 	"github.com/textileio/go-tableland/pkg/metrics"
+	"github.com/textileio/go-tableland/pkg/wallet"
 )
 
 func main() {
@@ -36,10 +39,24 @@ func main() {
 			log.Fatal().Err(err).Msgf("receipt timeout has invalid format: %s", chainCfg.Probe.ReceiptTimeout)
 		}
 
+		wallet, err := wallet.NewWallet(chainCfg.WalletPrivateKey)
+		if err != nil {
+			log.Fatal().Err(err).Msg("unable to create wallet from private key string")
+		}
+
+		chain, ok := client.Chains[client.ChainID(chainCfg.ChainID)]
+		if !ok {
+			log.Fatal().Int("chain_id", chainCfg.ChainID).Msg("the chain id isn't supported in the Tableland client")
+		}
+
+		client, err := clientV1.NewClient(ctx, wallet, clientV1.NewClientChain(chain))
+		if err != nil {
+			log.Fatal().Err(err).Msg("error creating tbl client")
+		}
+
 		cp, err := counterprobe.New(
-			chainCfg.Name,
-			cfg.Target,
-			chainCfg.Probe.SIWE,
+			chain.Name,
+			client,
 			chainCfg.Probe.Tablename,
 			checkInterval,
 			receiptTimeout)
diff --git a/docker/deployed/staging/healthbot/.env_healthbot.example b/docker/deployed/staging/healthbot/.env_healthbot.example
index 1f179f8..fae7a78 100644
--- a/docker/deployed/staging/healthbot/.env_healthbot.example
+++ b/docker/deployed/staging/healthbot/.env_healthbot.example
@@ -1,2 +1,2 @@
-HEALTHBOT_OPTIMISM_GOERLI_SIWE=
+HEALTHBOT_OPTIMISM_GOERLI_PRIVATE_KEY=
 HEALTHBOT_OPTIMISM_GOERLI_TABLE=
\ No newline at end of file
diff --git a/docker/deployed/staging/healthbot/config.json b/docker/deployed/staging/healthbot/config.json
index c93e663..8598c10 100644
--- a/docker/deployed/staging/healthbot/config.json
+++ b/docker/deployed/staging/healthbot/config.json
@@ -6,14 +6,13 @@
         "Human": false,
         "Debug": true
     },
-    "Target": "https://staging.tableland.network/rpc",
     "Chains": [
         {
-            "Name": "optimism-goerli",
+            "ChainID": 5,
+            "WalletPrivateKey": "${HEALTHBOT_OPTIMISM_GOERLI_PRIVATE_KEY}",
             "Probe": {
                 "CheckInterval": "360s",
                 "ReceiptTimeout": "20s",
-                "SIWE": "${HEALTHBOT_OPTIMISM_GOERLI_SIWE}",
                 "Tablename": "${HEALTHBOT_OPTIMISM_GOERLI_TABLE}"
             }
         }
diff --git a/pkg/client/chains.go b/pkg/client/chains.go
index 224b99d..52a600d 100644
--- a/pkg/client/chains.go
+++ b/pkg/client/chains.go
@@ -40,64 +40,64 @@ var ChainIDs = struct {
 type Chain struct {
 	Endpoint     string
 	ID           ChainID
+	Name         string
 	ContractAddr common.Address
 }
 
 // Chains is the connection info for all chains supported by Tableland.
-var Chains = struct {
-	Ethereum       Chain
-	Optimism       Chain
-	Polygon        Chain
-	Arbitrum       Chain
-	EthereumGoerli Chain
-	OptimismGoerli Chain
-	ArbitrumGoerli Chain
-	PolygonMumbai  Chain
-	Local          Chain
-}{
-	Ethereum: Chain{
+var Chains = map[ChainID]Chain{
+	ChainIDs.Ethereum: {
 		Endpoint:     mainnetURL,
 		ID:           ChainIDs.Ethereum,
+		Name:         "Ethereum",
 		ContractAddr: common.HexToAddress("0x012969f7e3439a9B04025b5a049EB9BAD82A8C12"),
 	},
-	Optimism: Chain{
+	ChainIDs.Optimism: {
 		Endpoint:     mainnetURL,
 		ID:           ChainIDs.Optimism,
+		Name:         "Optimism",
 		ContractAddr: common.HexToAddress("0xfad44BF5B843dE943a09D4f3E84949A11d3aa3e6"),
 	},
-	Polygon: Chain{
+	ChainIDs.Polygon: {
 		Endpoint:     mainnetURL,
 		ID:           ChainIDs.Polygon,
+		Name:         "Polygon",
 		ContractAddr: common.HexToAddress("0x5c4e6A9e5C1e1BF445A062006faF19EA6c49aFeA"),
 	},
-	Arbitrum: Chain{
+	ChainIDs.Arbitrum: {
 		Endpoint:     mainnetURL,
 		ID:           ChainIDs.Arbitrum,
+		Name:         "Arbitrum",
 		ContractAddr: common.HexToAddress("0x9aBd75E8640871A5a20d3B4eE6330a04c962aFfd"),
 	},
-	EthereumGoerli: Chain{
+	ChainIDs.EthereumGoerli: {
 		Endpoint:     testnetURL,
 		ID:           ChainIDs.EthereumGoerli,
+		Name:         "Ethereum Goerli",
 		ContractAddr: common.HexToAddress("0xDA8EA22d092307874f30A1F277D1388dca0BA97a"),
 	},
-	OptimismGoerli: Chain{
+	ChainIDs.OptimismGoerli: {
 		Endpoint:     testnetURL,
 		ID:           ChainIDs.OptimismGoerli,
+		Name:         "Optimism Goerli",
 		ContractAddr: common.HexToAddress("0xC72E8a7Be04f2469f8C2dB3F1BdF69A7D516aBbA"),
 	},
-	ArbitrumGoerli: Chain{
+	ChainIDs.ArbitrumGoerli: {
 		Endpoint:     testnetURL,
 		ID:           ChainIDs.ArbitrumGoerli,
+		Name:         "Arbitrum Goerli",
 		ContractAddr: common.HexToAddress("0x033f69e8d119205089Ab15D340F5b797732f646b"),
 	},
-	PolygonMumbai: Chain{
+	ChainIDs.PolygonMumbai: {
 		Endpoint:     testnetURL,
 		ID:           ChainIDs.PolygonMumbai,
+		Name:         "Polygon Mumbai",
 		ContractAddr: common.HexToAddress("0x4b48841d4b32C4650E4ABc117A03FE8B51f38F68"),
 	},
-	Local: Chain{
+	ChainIDs.Local: {
 		Endpoint:     localURL,
 		ID:           ChainIDs.Local,
+		Name:         "Local",
 		ContractAddr: common.HexToAddress("0xe7f1725e7734ce288f8367e1bb143e90bb3f0512"),
 	},
 }
diff --git a/pkg/client/legacy/client.go b/pkg/client/legacy/client.go
index dbfc592..1380426 100644
--- a/pkg/client/legacy/client.go
+++ b/pkg/client/legacy/client.go
@@ -23,7 +23,7 @@ import (
 	"github.com/textileio/go-tableland/pkg/wallet"
 )
 
-var defaultChain = client.Chains.PolygonMumbai
+var defaultChain = client.Chains[client.ChainIDs.PolygonMumbai]
 
 // TxnReceipt is a Tableland event processing receipt.
 // TODO(json-rpc): remove client_legacy package when support is dropped.
diff --git a/pkg/client/v1/client.go b/pkg/client/v1/client.go
index a5c5ce9..674779b 100644
--- a/pkg/client/v1/client.go
+++ b/pkg/client/v1/client.go
@@ -21,7 +21,7 @@ import (
 	"github.com/textileio/go-tableland/pkg/wallet"
 )
 
-var defaultChain = client.Chains.PolygonMumbai
+var defaultChain = client.Chains[client.ChainIDs.PolygonMumbai]
 
 // Client is the Tableland client.
 type Client struct {


Test patch:
diff --git a/cmd/healthbot/counterprobe/counterprobe_test.go b/cmd/healthbot/counterprobe/counterprobe_test.go
index 2cb4344..b4eec19 100644
--- a/cmd/healthbot/counterprobe/counterprobe_test.go
+++ b/cmd/healthbot/counterprobe/counterprobe_test.go
@@ -6,15 +6,23 @@ import (
 	"time"
 
 	"github.com/stretchr/testify/require"
+	"github.com/textileio/go-tableland/pkg/client"
+	clientV1 "github.com/textileio/go-tableland/pkg/client/v1"
+	"github.com/textileio/go-tableland/pkg/wallet"
 )
 
 func TestProduction(t *testing.T) {
 	t.SkipNow()
-	siwe := "fillme"
-	endpoint := "https://testnet.tableland.network/rpc"
-	tblname := "Runbook_24"
 
-	cp, err := New("optimism-mainnet", endpoint, siwe, tblname, time.Second, time.Second*10)
+	ctx := context.Background()
+	wallet, err := wallet.NewWallet("FILL ME")
+	require.NoError(t, err)
+
+	chain := client.Chains[client.ChainIDs.Optimism]
+	client, err := clientV1.NewClient(ctx, wallet, clientV1.NewClientChain(chain))
+	require.NoError(t, err)
+
+	cp, err := New("optimism-mainnet", client, "Runbook_24", time.Second, time.Second*10)
 	require.NoError(t, err)
 
 	value, err := cp.healthCheck(context.Background())
diff --git a/docker/deployed/testnet/healthbot/.env_healthbot.example b/docker/deployed/testnet/healthbot/.env_healthbot.example
index 18859e8..a4116f9 100644
--- a/docker/deployed/testnet/healthbot/.env_healthbot.example
+++ b/docker/deployed/testnet/healthbot/.env_healthbot.example
@@ -1,9 +1,9 @@
-HEALTHBOT_ETHEREUM_GOERLI_SIWE=
+HEALTHBOT_ETHEREUM_GOERLI_PRIVATE_KEY=
 HEALTHBOT_ETHEREUM_GOERLI_TABLE=
-HEALTHBOT_POLYGON_MUMBAI_SIWE=
+HEALTHBOT_POLYGON_MUMBAI_PRIVATE_KEY=
 HEALTHBOT_POLYGON_MUMBAI_TABLE=
-HEALTHBOT_ARBITRUM_GOERLI_SIWE=
+HEALTHBOT_ARBITRUM_GOERLI_PRIVATE_KEY=
 HEALTHBOT_ARBITRUM_GOERLI_TABLE=
-HEALTHBOT_OPTIMISM_GOERLI_SIWE=
+HEALTHBOT_OPTIMISM_GOERLI_PRIVATE_KEY=
 HEALTHBOT_OPTIMISM_GOERLI_TABLE=
 
diff --git a/docker/deployed/testnet/healthbot/config.json b/docker/deployed/testnet/healthbot/config.json
index a36ac25..d7ffbdf 100644
--- a/docker/deployed/testnet/healthbot/config.json
+++ b/docker/deployed/testnet/healthbot/config.json
@@ -6,41 +6,40 @@
         "Human": false,
         "Debug": true
     },
-    "Target": "https://testnet.tableland.network/rpc",
     "Chains": [
         {
-            "Name": "arbitrum-goerli",
+            "Name": 421613,
+            "WalletPrivateKey": "${HEALTHBOT_ARBITRUM_GOERLI_PRIVATE_KEY}",
             "Probe": {
                 "CheckInterval": "360s",
                 "ReceiptTimeout": "25s",
-                "SIWE": "${HEALTHBOT_ARBITRUM_GOERLI_SIWE}",
                 "Tablename": "${HEALTHBOT_ARBITRUM_GOERLI_TABLE}"
             }
         },
         {
-            "Name": "ethereum-goerli",
+            "ChainID": 5,
+            "WalletPrivateKey": "${HEALTHBOT_ETHEREUM_GOERLI_PRIVATE_KEY}",
             "Probe": {
                 "CheckInterval": "1h",
                 "ReceiptTimeout": "90s",
-                "SIWE": "${HEALTHBOT_ETHEREUM_GOERLI_SIWE}",
                 "Tablename": "${HEALTHBOT_ETHEREUM_GOERLI_TABLE}"
             }
         },
         {
-            "Name": "polygon-mumbai",
+            "ChainID": 80001,
+            "WalletPrivateKey": "${HEALTHBOT_POLYGON_MUMBAI_PRIVATE_KEY}",
             "Probe": {
                 "CheckInterval": "240s",
                 "ReceiptTimeout": "40s",
-                "SIWE": "${HEALTHBOT_POLYGON_MUMBAI_SIWE}",
                 "Tablename": "${HEALTHBOT_POLYGON_MUMBAI_TABLE}"
             }
         },
         {
-            "Name": "optimism-goerli",
+            "ChainID": 420,
+            "WalletPrivateKey": "${HEALTHBOT_OPTIMISM_GOERLI_PRIVATE_KEY}",
             "Probe": {
                 "CheckInterval": "360s",
                 "ReceiptTimeout": "25s",
-                "SIWE": "${HEALTHBOT_OPTIMISM_GOERLI_SIWE}",
                 "Tablename": "${HEALTHBOT_OPTIMISM_GOERLI_TABLE}"
             }
         }