Repository: tablelandnetwork/go-tableland
Pull request #169 (2022-07-09 03:48:40)

Add rpc endpoint validateWriteQuery
This is a first try at writing production go, so any feedback would be really appreciated.  Hopefully it satisfies the Validator's part of https://www.notion.so/textile/Add-enableTestnetRPCRelay-flag-in-JS-SDK-d6ccf9e045bb4c54a43bbb0b0e78483d

jsign: @joewagner, I know this is draft, but just signaling that it's looking good.
joewagner: > @joewagner, I know this is draft, but just signaling that it's looking good.

Thanks! glad to get feedback early and often.  I have a couple questions that I'll leave as inline comments, no hurry on reviewing
jsign: @joewagner, BTW, congrats on the PR. Happy to see more help from you here!
joewagner: > @joewagner, BTW, congrats on the PR. Happy to see more help from you here!

Thanks!  Thanks especially for all the guidance and help.  It's fun to be able to work on this, hopefully more to come

Patch:
diff --git a/internal/tableland/impl/mesa.go b/internal/tableland/impl/mesa.go
index 82c9db7..7c5b660 100644
--- a/internal/tableland/impl/mesa.go
+++ b/internal/tableland/impl/mesa.go
@@ -49,6 +49,52 @@ func (t *TablelandMesa) ValidateCreateTable(
 	return tableland.ValidateCreateTableResponse{StructureHash: createStmt.GetStructureHash()}, nil
 }
 
+// ValidateWriteQuery allows the user to validate a write query.
+func (t *TablelandMesa) ValidateWriteQuery(
+	ctx context.Context,
+	req tableland.ValidateWriteQueryRequest) (tableland.ValidateWriteQueryResponse, error) {
+	ctxController := ctx.Value(middlewares.ContextKeyAddress)
+	controller, ok := ctxController.(string)
+	if !ok || controller == "" {
+		return tableland.ValidateWriteQueryResponse{}, errors.New("no controller address found in context")
+	}
+
+	ctxChainID := ctx.Value(middlewares.ContextKeyChainID)
+	chainID, ok := ctxChainID.(tableland.ChainID)
+	if !ok {
+		return tableland.ValidateWriteQueryResponse{}, errors.New("no chain id found in context")
+	}
+
+	stack, chainOk := t.chainStacks[chainID]
+	if !chainOk {
+		return tableland.ValidateWriteQueryResponse{}, fmt.Errorf("chain id %d isn't supported in the validator", chainID)
+	}
+
+	mutatingStmts, err := t.parser.ValidateMutatingQuery(req.Statement, chainID)
+	if err != nil {
+		return tableland.ValidateWriteQueryResponse{}, fmt.Errorf("validating query: %s", err)
+	}
+
+	tableID := mutatingStmts[0].GetTableID()
+
+	table, err := stack.Store.GetTable(ctx, tableID)
+
+	// if the tableID is not valid err will exist
+	if err != nil {
+		return tableland.ValidateWriteQueryResponse{}, fmt.Errorf("getting table: %s", err)
+	}
+	// if the prefix is wrong the statement is not valid
+	prefix := mutatingStmts[0].GetPrefix()
+	if table.Prefix != prefix {
+		return tableland.ValidateWriteQueryResponse{}, fmt.Errorf(
+			"table prefix doesn't match (exp %s, got %s)", table.Prefix, prefix)
+	}
+
+	response := tableland.ValidateWriteQueryResponse{}
+	response.TableID = tableID.String()
+	return response, nil
+}
+
 // RelayWriteQuery allows the user to rely on the validator wrapping the query in a chain transaction.
 func (t *TablelandMesa) RelayWriteQuery(
 	ctx context.Context,
@@ -134,6 +180,7 @@ func (t *TablelandMesa) GetReceipt(
 			Error:       receipt.Error,
 		},
 	}
+
 	if receipt.TableID != nil {
 		tID := receipt.TableID.String()
 		ret.Receipt.TableID = &tID
diff --git a/internal/tableland/impl/mesa_instrumented.go b/internal/tableland/impl/mesa_instrumented.go
index a8b32c0..91813d4 100644
--- a/internal/tableland/impl/mesa_instrumented.go
+++ b/internal/tableland/impl/mesa_instrumented.go
@@ -54,6 +54,17 @@ func (t *InstrumentedTablelandMesa) ValidateCreateTable(ctx context.Context,
 	return resp, err
 }
 
+// ValidateWriteQuery validates a statement that would mutate a table and returns the table ID.
+func (t *InstrumentedTablelandMesa) ValidateWriteQuery(ctx context.Context,
+	req tableland.ValidateWriteQueryRequest) (tableland.ValidateWriteQueryResponse, error) {
+	start := time.Now()
+	resp, err := t.tableland.ValidateWriteQuery(ctx, req)
+	latency := time.Since(start).Milliseconds()
+	chainID, _ := ctx.Value(middlewares.ContextKeyChainID).(tableland.ChainID)
+	t.record(ctx, recordData{"ValidateWriteQuery", "", "", err == nil, latency, chainID})
+	return resp, err
+}
+
 // RunReadQuery allows the user to run SQL.
 func (t *InstrumentedTablelandMesa) RunReadQuery(ctx context.Context,
 	req tableland.RunReadQueryRequest) (tableland.RunReadQueryResponse, error) {
diff --git a/internal/tableland/tableland.go b/internal/tableland/tableland.go
index 11296a5..bfb7ff9 100644
--- a/internal/tableland/tableland.go
+++ b/internal/tableland/tableland.go
@@ -58,6 +58,16 @@ type ValidateCreateTableResponse struct {
 	StructureHash string `json:"structure_hash"`
 }
 
+// ValidateWriteQueryRequest is a ValidateWriteQuery request.
+type ValidateWriteQueryRequest struct {
+	Statement string `json:"statement"`
+}
+
+// ValidateWriteQueryResponse is a ValidateWriteQuery response.
+type ValidateWriteQueryResponse struct {
+	TableID string `json:"table_id"`
+}
+
 // SetControllerRequest is a user SetController request.
 type SetControllerRequest struct {
 	Controller string `json:"controller"`
@@ -80,6 +90,7 @@ type SQLRunner interface {
 type Tableland interface {
 	SQLRunner
 	ValidateCreateTable(context.Context, ValidateCreateTableRequest) (ValidateCreateTableResponse, error)
+	ValidateWriteQuery(context.Context, ValidateWriteQueryRequest) (ValidateWriteQueryResponse, error)
 	RelayWriteQuery(context.Context, RelayWriteQueryRequest) (RelayWriteQueryResponse, error)
 	GetReceipt(context.Context, GetReceiptRequest) (GetReceiptResponse, error)
 	SetController(context.Context, SetControllerRequest) (SetControllerResponse, error)
diff --git a/pkg/sqlstore/impl/system_store_instrumented.go b/pkg/sqlstore/impl/system_store_instrumented.go
index a26aed7..e05423a 100644
--- a/pkg/sqlstore/impl/system_store_instrumented.go
+++ b/pkg/sqlstore/impl/system_store_instrumented.go
@@ -7,6 +7,7 @@ import (
 	"time"
 
 	"github.com/ethereum/go-ethereum/common"
+	"github.com/rs/zerolog/log"
 	"github.com/textileio/go-tableland/internal/tableland"
 	"github.com/textileio/go-tableland/pkg/eventprocessor"
 	"github.com/textileio/go-tableland/pkg/nonce"
@@ -208,6 +209,7 @@ func (s *InstrumentedSystemStore) Begin(ctx context.Context) (*sql.Tx, error) {
 func (s *InstrumentedSystemStore) GetReceipt(
 	ctx context.Context,
 	txnHash string) (eventprocessor.Receipt, bool, error) {
+	log.Debug().Str("hash", txnHash).Msg("call GetReceipt")
 	start := time.Now()
 	receipt, ok, err := s.store.GetReceipt(ctx, txnHash)
 	latency := time.Since(start).Milliseconds()
diff --git a/tableland-openapi-spec.yaml b/tableland-openapi-spec.yaml
index 819c255..2f5f630 100644
--- a/tableland-openapi-spec.yaml
+++ b/tableland-openapi-spec.yaml
@@ -46,6 +46,14 @@ paths:
                   params:
                     - id: "0"
                       create_statement: "CREATE TABLE myname_31337 (column_a int, column_b text);"
+              validateWriteQuery:
+                value:
+                  jsonrpc: "2.0"
+                  method: tableland_validateWriteQuery
+                  id: 1
+                  params:
+                    - id: "0"
+                      statement: "INSERT INTO healthbot_31337_1 VALUES (1);"
               runReadQuery:
                 value:
                   jsonrpc: "2.0"
@@ -111,6 +119,12 @@ paths:
                     id: 1
                     result:
                       structure_hash: "ef7be01282ea97380e4d3bbcba6774cbc7242c46ee51b7e611f1efdfa3623e53"
+                validateWriteQuery:
+                  value:
+                    jsonrpc: "2.0"
+                    id: 1
+                    result:
+                      table_id: "1"
                 relayWriteQuery:
                   value:
                     jsonrpc: "2.0"
@@ -416,6 +430,35 @@ components:
             properties:
               create_statement:
                 type: "string"
+    validateWriteQuery:
+      type: "object"
+      required:
+        - "method"
+        - "id"
+        - "jsonrpc"
+        - "params"
+      properties:
+        method:
+          type: "string"
+          description: "Method name"
+        id:
+          type: "integer"
+          default: 1
+          format: int32
+          description: "Request ID"
+        jsonrpc:
+          type: "string"
+          default: "2.0"
+          description: "JSON-RPC Version (2.0)"
+        params:
+          type: "array"
+          items:
+            type: "object"
+            required:
+              - "statement"
+            properties:
+              statement:
+                type: "string"
     getReceipt:
       type: "object"
       required:


Test patch:
diff --git a/internal/router/middlewares/authentication_test.go b/internal/router/middlewares/authentication_test.go
index f2a09eb..cb45aed 100644
--- a/internal/router/middlewares/authentication_test.go
+++ b/internal/router/middlewares/authentication_test.go
@@ -51,6 +51,7 @@ func TestOptionality(t *testing.T) {
 		{rpcMethodName: "tableland_runReadQuery", expStatusCode: http.StatusOK},
 		{rpcMethodName: "tableland_relayWriteQuery", expStatusCode: http.StatusUnauthorized},
 		{rpcMethodName: "tableland_validateCreateTable", expStatusCode: http.StatusUnauthorized},
+		{rpcMethodName: "tableland_validateWriteQuery", expStatusCode: http.StatusUnauthorized},
 		{rpcMethodName: "tableland_getReceipt", expStatusCode: http.StatusUnauthorized},
 		{rpcMethodName: "tableland_setController", expStatusCode: http.StatusUnauthorized},
 	}