diff --git a/Makefile b/Makefile index 3b9ec4f3..4b39124e 100644 --- a/Makefile +++ b/Makefile @@ -36,7 +36,7 @@ system-sql-assets: .PHONY: system-sql-assets mocks: clean-mocks - go run github.com/vektra/mockery/v2@v2.14.0 --name='\b(?:SQLRunner|Tableland)\b' --recursive --with-expecter + go run github.com/vektra/mockery/v2@v2.14.0 --name='\b(?:Gateway)\b' --recursive --with-expecter .PHONY: mocks clean-mocks: diff --git a/README.md b/README.md index f721d94c..5b787b08 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,6 @@ They have the following responsibilities: - Listen to on-chain events to materialize Tableland-compliant SQL queries in a database engine (currently, SQLite by default). - Serve read-queries (e.g: `SELECT * FROM foo_69_1`) to the external world. -- Relay write-queries to the `Registry` SC on behalf of users. > 💡 The responsibilities of the validator will continue to change as the Tableland protocol evolves. In the future, validators will have more responsibilities in the network. @@ -36,7 +35,7 @@ To understand better the usual work mechanics of the validator, let’s go throu 6- The validators will detect the new event and execute the mutating query in the corresponding table. -7- The user can query the RPC endpoint of the validator to execute read-queries (e.g: `SELECT * FROM ...`), to see the materialized result of its interaction with the SC. +7- The user can query the `/query?statement=...` REST endpoint of the validator to execute read-queries (e.g: `SELECT * FROM ...`), to see the materialized result of its interaction with the SC. > 💡 The description above is optimized to understand the general mechanics of the validator. Minting tables, and executing mutating statements also imply more work both at the SC and validator levels (e.g: ACL enforcing); we’re skipping them here. @@ -63,7 +62,6 @@ The `cmd/toolkit` is a CLI which contain useful commands: - `gaspricebump`: Bumps gas price for a stuck transaction - `sc`: Offers smart sontract calls -- `siwe`: SIWE utilities - `wallet`: Offers wallet utilites # Contributing diff --git a/cmd/api/config.go b/cmd/api/config.go index 1ace3365..135334cf 100644 --- a/cmd/api/config.go +++ b/cmd/api/config.go @@ -96,10 +96,9 @@ type QueryConstraints struct { // ChainConfig contains all the chain execution stack configuration for a particular EVM chain. type ChainConfig struct { - Name string `default:""` - ChainID tableland.ChainID `default:"0"` - AllowTransactionRelay bool `default:"false"` - Registry struct { + Name string `default:""` + ChainID tableland.ChainID `default:"0"` + Registry struct { EthEndpoint string `default:"eth_endpoint"` ContractAddress string `default:"contract_address"` } diff --git a/cmd/api/main.go b/cmd/api/main.go index a5e1ddb1..1d4c8ff6 100644 --- a/cmd/api/main.go +++ b/cmd/api/main.go @@ -19,8 +19,8 @@ import ( "github.com/textileio/cli" "github.com/textileio/go-tableland/buildinfo" "github.com/textileio/go-tableland/internal/chains" + "github.com/textileio/go-tableland/internal/gateway" "github.com/textileio/go-tableland/internal/router" - systemimpl "github.com/textileio/go-tableland/internal/system/impl" "github.com/textileio/go-tableland/internal/tableland" "github.com/textileio/go-tableland/internal/tableland/impl" "github.com/textileio/go-tableland/pkg/backup" @@ -37,10 +37,8 @@ import ( parserimpl "github.com/textileio/go-tableland/pkg/parsing/impl" "github.com/textileio/go-tableland/pkg/readstatementresolver" "github.com/textileio/go-tableland/pkg/sqlstore" - sqlstoreimpl "github.com/textileio/go-tableland/pkg/sqlstore/impl" "github.com/textileio/go-tableland/pkg/sqlstore/impl/system" - "github.com/textileio/go-tableland/pkg/sqlstore/impl/user" - "github.com/textileio/go-tableland/pkg/tables/impl/ethereum" + "github.com/textileio/go-tableland/pkg/telemetry" "github.com/textileio/go-tableland/pkg/telemetry/chainscollector" "github.com/textileio/go-tableland/pkg/telemetry/publisher" @@ -94,18 +92,17 @@ func main() { log.Fatal().Err(err).Msg("creating chains stack") } - // User store. eps := make(map[tableland.ChainID]eventprocessor.EventProcessor, len(chainStacks)) for chainID, stack := range chainStacks { eps[chainID] = stack.EventProcessor } - userStore, err := user.New(databaseURL, readstatementresolver.New(eps)) - if err != nil { - log.Fatal().Err(err).Msg("creating user store") + + for _, stack := range chainStacks { + stack.Store.SetReadResolver(readstatementresolver.New(eps)) } // HTTP API server. - closeHTTPServer, err := createAPIServer(config.HTTP, config.Gateway, parser, userStore, chainStacks) + closeHTTPServer, err := createAPIServer(config.HTTP, config.Gateway, parser, chainStacks) if err != nil { log.Fatal().Err(err).Msg("creating HTTP server") } @@ -147,11 +144,6 @@ func main() { log.Error().Err(err).Msg("closing backuper") } - // Close user store. - if err := userStore.Close(); err != nil { - log.Error().Err(err).Msg("closing user store") - } - // Close telemetry. if err := closeTelemetryModule(ctx); err != nil { log.Error().Err(err).Msg("closing telemetry module") @@ -172,7 +164,7 @@ func createChainIDStack( return chains.ChainStack{}, fmt.Errorf("failed initialize sqlstore: %s", err) } - systemStore, err := sqlstoreimpl.NewInstrumentedSystemStore(config.ChainID, store) + systemStore, err := system.NewInstrumentedSystemStore(config.ChainID, store) if err != nil { return chains.ChainStack{}, fmt.Errorf("instrumenting system store: %s", err) } @@ -216,18 +208,8 @@ func createChainIDStack( } scAddress := common.HexToAddress(config.Registry.ContractAddress) - registry, err := ethereum.NewClient( - conn, - config.ChainID, - scAddress, - wallet, - tracker, - ) - if err != nil { - return chains.ChainStack{}, fmt.Errorf("failed to create ethereum client: %s", err) - } - acl := impl.NewACL(systemStore, registry) + acl := impl.NewACL(systemStore) ex, err := executor.NewExecutor(config.ChainID, executorsDB, parser, tableConstraints.MaxRowCount, acl) if err != nil { @@ -270,10 +252,7 @@ func createChainIDStack( } return chains.ChainStack{ Store: systemStore, - Registry: registry, EventProcessor: ep, - // TODO: we can remove the AllowTransactionRelay config property entirely in a future PR - AllowTransactionRelay: false, Close: func(ctx context.Context) error { log.Info().Int64("chain_id", int64(config.ChainID)).Msg("closing stack...") defer log.Info().Int64("chain_id", int64(config.ChainID)).Msg("stack closed") @@ -396,8 +375,8 @@ func createParser(queryConstraints QueryConstraints) (parsing.SQLValidator, erro parser, err := parserimpl.New([]string{ "sqlite_", - systemimpl.SystemTablesPrefix, - systemimpl.RegistryTableName, + parsing.SystemTablesPrefix, + parsing.RegistryTableName, }, parserOpts...) if err != nil { return nil, fmt.Errorf("new parser: %s", err) @@ -479,37 +458,26 @@ func createAPIServer( httpConfig HTTPConfig, gatewayConfig GatewayConfig, parser parsing.SQLValidator, - userStore *user.UserStore, chainStacks map[tableland.ChainID]chains.ChainStack, ) (moduleCloser, error) { - instrUserStore, err := sqlstoreimpl.NewInstrumentedUserStore(userStore) - if err != nil { - return nil, fmt.Errorf("creating instrumented user store: %s", err) - } - - mesaService := impl.NewTablelandMesa(parser, instrUserStore, chainStacks) - mesaService, err = impl.NewInstrumentedTablelandMesa(mesaService) - if err != nil { - return nil, fmt.Errorf("instrumenting mesa: %s", err) - } - supportedChainIDs := make([]tableland.ChainID, 0, len(chainStacks)) stores := make(map[tableland.ChainID]sqlstore.SystemStore, len(chainStacks)) for chainID, stack := range chainStacks { stores[chainID] = stack.Store supportedChainIDs = append(supportedChainIDs, chainID) } - sysStore, err := systemimpl.NewSystemSQLStoreService( + g, err := gateway.NewGateway( + parser, stores, gatewayConfig.ExternalURIPrefix, gatewayConfig.MetadataRendererURI, gatewayConfig.AnimationRendererURI) if err != nil { - return nil, fmt.Errorf("creating system store: %s", err) + return nil, fmt.Errorf("creating gateway: %s", err) } - systemService, err := systemimpl.NewInstrumentedSystemSQLStoreService(sysStore) + g, err = gateway.NewInstrumentedGateway(g) if err != nil { - return nil, fmt.Errorf("instrumenting system sql store: %s", err) + return nil, fmt.Errorf("instrumenting gateway: %s", err) } rateLimInterval, err := time.ParseDuration(httpConfig.RateLimInterval) if err != nil { @@ -517,8 +485,7 @@ func createAPIServer( } router, err := router.ConfiguredRouter( - mesaService, - systemService, + g, httpConfig.MaxRequestPerInterval, rateLimInterval, supportedChainIDs, diff --git a/cmd/toolkit/main.go b/cmd/toolkit/main.go index 4ee3be58..9c7b67c9 100644 --- a/cmd/toolkit/main.go +++ b/cmd/toolkit/main.go @@ -1,8 +1,6 @@ package main import ( - "time" - "github.com/spf13/cobra" ) @@ -20,16 +18,11 @@ func main() { } func init() { - rootCmd.AddCommand(siweCmd) rootCmd.AddCommand(scCmd) rootCmd.AddCommand(walletCmd) rootCmd.AddCommand(gasPriceBumperCmd) rootCmd.AddCommand(replaceNonceRangeCmd) - siweCreateCmd.Flags().Duration("duration", time.Hour*24*365*100, "validity duration") - siweCreateCmd.Flags().Int("chain-id", 69, "chain id") - siweCmd.AddCommand(siweCreateCmd) - scCmd.PersistentFlags().String("contract-address", "", "the smart contract address") scCmd.PersistentFlags().Int("chain-id", 69, "chain id") scCmd.PersistentFlags().String("privatekey", "", "the private key used to make the contract calls") diff --git a/cmd/toolkit/siwe.go b/cmd/toolkit/siwe.go deleted file mode 100644 index a8a3d071..00000000 --- a/cmd/toolkit/siwe.go +++ /dev/null @@ -1,51 +0,0 @@ -package main - -import ( - "errors" - "fmt" - - "github.com/spf13/cobra" - "github.com/textileio/go-tableland/internal/tableland" - "github.com/textileio/go-tableland/pkg/siwe" - "github.com/textileio/go-tableland/pkg/wallet" -) - -var siweCmd = &cobra.Command{ - Use: "siwe", - Short: "SIWE utilities", - Long: `Sign-In With Ethereum utilities`, - Args: cobra.ExactArgs(1), -} - -var siweCreateCmd = &cobra.Command{ - Use: "create", - Short: "Creates a SIWE token", - Long: `Creates a SIWE token to be used in Tableland RPC calls`, - Args: cobra.ExactArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - duration, err := cmd.Flags().GetDuration("duration") - if err != nil { - return errors.New("failed to parse duration") - } - chainID, err := cmd.Flags().GetInt("chain-id") - if err != nil { - return errors.New("failed to parse chain-id") - } - privateKey := args[0] - - w, err := wallet.NewWallet(privateKey) - if err != nil { - return fmt.Errorf("decoding private key: %s", err) - } - - siwe, err := siwe.EncodedSIWEMsg(tableland.ChainID(chainID), w, duration) - if err != nil { - return fmt.Errorf("creating bearer value: %v", err) - } - - fmt.Printf("%s\n\n", siwe) - fmt.Printf("Signed by %s\n", w.Address().Hex()) - - return nil - }, -} diff --git a/cmd/toolkit/smart_contract.go b/cmd/toolkit/smart_contract.go index bb874d9a..6d35d029 100644 --- a/cmd/toolkit/smart_contract.go +++ b/cmd/toolkit/smart_contract.go @@ -9,9 +9,9 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/ethclient" "github.com/spf13/cobra" - systemimpl "github.com/textileio/go-tableland/internal/system/impl" "github.com/textileio/go-tableland/internal/tableland" "github.com/textileio/go-tableland/pkg/nonce/impl" + "github.com/textileio/go-tableland/pkg/parsing" parserimpl "github.com/textileio/go-tableland/pkg/parsing/impl" "github.com/textileio/go-tableland/pkg/tables" "github.com/textileio/go-tableland/pkg/tables/impl/ethereum" @@ -63,8 +63,8 @@ var runSQLCmd = &cobra.Command{ parser, err := parserimpl.New([]string{ "sqlite_", - systemimpl.SystemTablesPrefix, - systemimpl.RegistryTableName, + parsing.SystemTablesPrefix, + parsing.RegistryTableName, }) if err != nil { return fmt.Errorf("new parser: %s", err) @@ -138,8 +138,8 @@ var createTableCmd = &cobra.Command{ parser, err := parserimpl.New([]string{ "sqlite_", - systemimpl.SystemTablesPrefix, - systemimpl.RegistryTableName, + parsing.SystemTablesPrefix, + parsing.RegistryTableName, }) if err != nil { return fmt.Errorf("new parser: %s", err) diff --git a/docker/deployed/mainnet/api/config.json b/docker/deployed/mainnet/api/config.json index df8ef28c..c65f97b3 100644 --- a/docker/deployed/mainnet/api/config.json +++ b/docker/deployed/mainnet/api/config.json @@ -54,7 +54,6 @@ { "Name": "Ethereum Mainnet", "ChainID": 1, - "AllowTransactionRelay": false, "Registry": { "EthEndpoint": "wss://eth-mainnet.g.alchemy.com/v2/${VALIDATOR_ALCHEMY_ETHEREUM_MAINNET_API_KEY}", "ContractAddress": "0x012969f7e3439a9B04025b5a049EB9BAD82A8C12" @@ -82,7 +81,6 @@ { "Name": "Arbitrum Mainnet", "ChainID": 42161, - "AllowTransactionRelay": false, "Registry": { "EthEndpoint": "https://arb-mainnet.g.alchemy.com/v2/${VALIDATOR_ALCHEMY_ARBITRUM_MAINNET_API_KEY}", "ContractAddress": "0x9aBd75E8640871A5a20d3B4eE6330a04c962aFfd" @@ -137,7 +135,6 @@ { "Name": "Polygon Mainnet", "ChainID": 137, - "AllowTransactionRelay": false, "Registry": { "EthEndpoint": "wss://polygon-mainnet.g.alchemy.com/v2/${VALIDATOR_ALCHEMY_POLYGON_MAINNET_API_KEY}", "ContractAddress": "0x5c4e6A9e5C1e1BF445A062006faF19EA6c49aFeA" @@ -165,7 +162,6 @@ { "Name": "Optimism Mainnet", "ChainID": 10, - "AllowTransactionRelay": false, "Registry": { "EthEndpoint": "wss://opt-mainnet.g.alchemy.com/v2/${VALIDATOR_ALCHEMY_OPTIMISM_MAINNET_API_KEY}", "ContractAddress": "0xfad44BF5B843dE943a09D4f3E84949A11d3aa3e6" diff --git a/docker/deployed/mainnet/healthbot/config.json b/docker/deployed/mainnet/healthbot/config.json index 42c30fc2..96310f5d 100644 --- a/docker/deployed/mainnet/healthbot/config.json +++ b/docker/deployed/mainnet/healthbot/config.json @@ -6,6 +6,6 @@ "Human": false, "Debug": true }, - "Target": "https://tableland.network/rpc", + "Target": "https://tableland.network/", "Chains": [] } \ No newline at end of file diff --git a/docker/deployed/staging/api/config.json b/docker/deployed/staging/api/config.json index 404ecff3..6c5e58e1 100644 --- a/docker/deployed/staging/api/config.json +++ b/docker/deployed/staging/api/config.json @@ -52,7 +52,6 @@ "Chains": [ { "Name": "Optimism Goerli", - "AllowTransactionRelay": false, "ChainID": 420, "Registry": { "EthEndpoint": "wss://opt-goerli.g.alchemy.com/v2/${VALIDATOR_ALCHEMY_OPTIMISM_GOERLI_API_KEY}", diff --git a/docker/deployed/testnet/api/config.json b/docker/deployed/testnet/api/config.json index f43073d3..cea42fcb 100644 --- a/docker/deployed/testnet/api/config.json +++ b/docker/deployed/testnet/api/config.json @@ -54,7 +54,6 @@ { "Name": "Ethereum Goerli", "ChainID": 5, - "AllowTransactionRelay": false, "Registry": { "EthEndpoint": "wss://eth-goerli.alchemyapi.io/v2/${VALIDATOR_ALCHEMY_ETHEREUM_GOERLI_API_KEY}", "ContractAddress": "0xDA8EA22d092307874f30A1F277D1388dca0BA97a" @@ -82,7 +81,6 @@ { "Name": "Polygon Mumbai", "ChainID": 80001, - "AllowTransactionRelay": false, "Registry": { "EthEndpoint": "wss://polygon-mumbai.g.alchemy.com/v2/${VALIDATOR_ALCHEMY_POLYGON_MUMBAI_API_KEY}", "ContractAddress": "0x4b48841d4b32C4650E4ABc117A03FE8B51f38F68" @@ -110,7 +108,6 @@ { "Name": "Arbitrum Goerli", "ChainID": 421613, - "AllowTransactionRelay": false, "Registry": { "EthEndpoint": "wss://arb-goerli.g.alchemy.com/v2/${VALIDATOR_ALCHEMY_ARBITRUM_GOERLI_API_KEY}", "ContractAddress": "0x033f69e8d119205089Ab15D340F5b797732f646b" @@ -137,7 +134,6 @@ }, { "Name": "Optimism Goerli", - "AllowTransactionRelay": false, "ChainID": 420, "Registry": { "EthEndpoint": "wss://opt-goerli.g.alchemy.com/v2/${VALIDATOR_ALCHEMY_OPTIMISM_GOERLI_API_KEY}", diff --git a/docker/local/api/config.json b/docker/local/api/config.json index d56273b2..184710c6 100644 --- a/docker/local/api/config.json +++ b/docker/local/api/config.json @@ -12,7 +12,6 @@ { "Name": "Local Hardhat", "ChainID": 31337, - "AllowTransactionRelay": false, "Registry": { "EthEndpoint": "ws://host.docker.internal:8545", "ContractAddress": "[FILL ME]" diff --git a/docker/observability/grafana/provisioning/dashboards/validator-dashboard.json b/docker/observability/grafana/provisioning/dashboards/validator-dashboard.json index eeccea3e..94895f11 100644 --- a/docker/observability/grafana/provisioning/dashboards/validator-dashboard.json +++ b/docker/observability/grafana/provisioning/dashboards/validator-dashboard.json @@ -64,42 +64,30 @@ "color": { "mode": "thresholds" }, - "decimals": 0, - "displayName": "${__series.name}", "mappings": [], - "max": 20, - "min": 0, "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null - }, - { - "color": "yellow", - "value": 5 - }, - { - "color": "red", - "value": 10 } ] }, - "unit": "attempts" + "unit": "ms" }, "overrides": [] }, "gridPos": { "h": 7, - "w": 6, + "w": 2, "x": 0, "y": 1 }, - "id": 71, + "id": 85, "options": { - "colorMode": "background", - "graphMode": "none", + "colorMode": "value", + "graphMode": "area", "justifyMode": "auto", "orientation": "auto", "reduceOptions": { @@ -109,26 +97,23 @@ "fields": "", "values": false }, - "text": {}, "textMode": "auto" }, - "pluginVersion": "9.2.2", + "pluginVersion": "9.3.2", "targets": [ { "datasource": { "type": "prometheus", "uid": "P1809F7CD0C75ACF3" }, - "exemplar": true, - "expr": "sum by (chain_id) (tableland_wallettracker_txn_confirmation_attempts{service_name=\"tableland:api\"})", - "format": "time_series", - "instant": false, - "interval": "", - "legendFormat": "Chain ID {{chain_id}}", + "editorMode": "code", + "expr": "runtime_uptime_milliseconds", + "legendFormat": "{{job}}", + "range": true, "refId": "A" } ], - "title": "Txn confirmation retries", + "title": "Uptime", "type": "stat" }, { @@ -166,8 +151,8 @@ }, "gridPos": { "h": 7, - "w": 17, - "x": 6, + "w": 22, + "x": 2, "y": 1 }, "id": 39, @@ -186,7 +171,7 @@ "text": {}, "textMode": "auto" }, - "pluginVersion": "9.2.2", + "pluginVersion": "9.3.2", "targets": [ { "datasource": { @@ -225,43 +210,37 @@ "color": { "mode": "thresholds" }, - "decimals": 0, - "displayName": "${__series.name}", "mappings": [], - "max": 0, - "min": 0, + "max": 1, + "noValue": "No", "thresholds": { "mode": "absolute", "steps": [ { - "color": "green", + "color": "red", "value": null }, { - "color": "yellow", - "value": 3 - }, - { - "color": "red", - "value": 5 + "color": "green", + "value": 1 } ] }, - "unit": "bumps" + "unit": "bool_yes_no" }, "overrides": [] }, "gridPos": { "h": 7, - "w": 6, + "w": 5, "x": 0, "y": 8 }, - "id": 73, + "id": 64, "options": { "colorMode": "background", "graphMode": "none", - "justifyMode": "auto", + "justifyMode": "center", "orientation": "auto", "reduceOptions": { "calcs": [ @@ -273,7 +252,7 @@ "text": {}, "textMode": "auto" }, - "pluginVersion": "9.2.2", + "pluginVersion": "9.3.2", "targets": [ { "datasource": { @@ -281,15 +260,13 @@ "uid": "P1809F7CD0C75ACF3" }, "exemplar": true, - "expr": "sum by (chain_id) (increase(tableland_wallettracker_gas_bumps{namespace=\"$namespace\"}[1h]))", - "format": "time_series", - "instant": false, + "expr": "sum by (chain_id)(tableland_wallettracker_eth_client_unhealthy{service_name=\"tableland:api\"}) < BOOL 1", "interval": "", - "legendFormat": "Chain ID {{chain_id}}", + "legendFormat": "ChainID: {{chain_id}}", "refId": "A" } ], - "title": "Txn Gas Price Bumps", + "title": "Ethereum node API is healthy", "type": "stat" }, { @@ -328,8 +305,8 @@ }, "gridPos": { "h": 7, - "w": 6, - "x": 6, + "w": 8, + "x": 5, "y": 8 }, "id": 62, @@ -348,7 +325,7 @@ "text": {}, "textMode": "auto" }, - "pluginVersion": "9.2.2", + "pluginVersion": "9.3.2", "targets": [ { "datasource": { @@ -367,175 +344,6 @@ "title": "Wallet balances", "type": "stat" }, - { - "datasource": { - "type": "prometheus", - "uid": "P1809F7CD0C75ACF3" - }, - "fieldConfig": { - "defaults": { - "color": { - "mode": "thresholds" - }, - "mappings": [], - "max": 1, - "noValue": "No", - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "red", - "value": null - }, - { - "color": "green", - "value": 1 - } - ] - }, - "unit": "bool_yes_no" - }, - "overrides": [] - }, - "gridPos": { - "h": 7, - "w": 5, - "x": 12, - "y": 8 - }, - "id": 64, - "options": { - "colorMode": "background", - "graphMode": "none", - "justifyMode": "center", - "orientation": "auto", - "reduceOptions": { - "calcs": [ - "lastNotNull" - ], - "fields": "", - "values": false - }, - "text": {}, - "textMode": "auto" - }, - "pluginVersion": "9.2.2", - "targets": [ - { - "datasource": { - "type": "prometheus", - "uid": "P1809F7CD0C75ACF3" - }, - "exemplar": true, - "expr": "sum by (chain_id)(tableland_wallettracker_eth_client_unhealthy{service_name=\"tableland:api\"}) < BOOL 1", - "interval": "", - "legendFormat": "ChainID: {{chain_id}}", - "refId": "A" - } - ], - "title": "Ethereum node API is healthy", - "type": "stat" - }, - { - "datasource": { - "type": "prometheus", - "uid": "P1809F7CD0C75ACF3" - }, - "description": "", - "fieldConfig": { - "defaults": { - "color": { - "mode": "palette-classic" - }, - "custom": { - "axisCenteredZero": false, - "axisColorMode": "text", - "axisLabel": "", - "axisPlacement": "auto", - "barAlignment": 0, - "drawStyle": "line", - "fillOpacity": 0, - "gradientMode": "none", - "hideFrom": { - "legend": false, - "tooltip": false, - "viz": false - }, - "lineInterpolation": "linear", - "lineWidth": 1, - "pointSize": 5, - "scaleDistribution": { - "type": "linear" - }, - "showPoints": "auto", - "spanNulls": false, - "stacking": { - "group": "A", - "mode": "none" - }, - "thresholdsStyle": { - "mode": "off" - } - }, - "displayName": "${__series.name}", - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": null - }, - { - "color": "yellow", - "value": 5 - }, - { - "color": "red", - "value": 10 - } - ] - }, - "unit": "txns" - }, - "overrides": [] - }, - "gridPos": { - "h": 7, - "w": 6, - "x": 17, - "y": 8 - }, - "id": 68, - "options": { - "legend": { - "calcs": [], - "displayMode": "list", - "placement": "bottom", - "showLegend": true - }, - "tooltip": { - "mode": "single", - "sort": "none" - } - }, - "pluginVersion": "8.2.0", - "targets": [ - { - "datasource": { - "type": "prometheus", - "uid": "P1809F7CD0C75ACF3" - }, - "exemplar": true, - "expr": "sum by (chain_id) (tableland_wallettracker_pending_txns{service_name=\"tableland:api\"})", - "interval": "", - "legendFormat": "ChainID {{chain_id}}", - "refId": "A" - } - ], - "title": "Pending txns", - "type": "timeseries" - }, { "datasource": { "type": "prometheus", @@ -565,10 +373,10 @@ "overrides": [] }, "gridPos": { - "h": 5, - "w": 4, - "x": 0, - "y": 15 + "h": 7, + "w": 3, + "x": 13, + "y": 8 }, "id": 82, "options": { @@ -586,85 +394,18 @@ "text": {}, "textMode": "auto" }, - "pluginVersion": "9.2.2", - "targets": [ - { - "datasource": { - "type": "prometheus", - "uid": "P1809F7CD0C75ACF3" - }, - "expr": "time() - tableland_backup_last_execution", - "refId": "A" - } - ], - "title": "Last backup", - "type": "stat" - }, - { - "datasource": { - "type": "prometheus", - "uid": "P1809F7CD0C75ACF3" - }, - "fieldConfig": { - "defaults": { - "color": { - "mode": "thresholds" - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": null - }, - { - "color": "dark-red", - "value": 18000 - } - ] - }, - "unit": "dtdurations" - }, - "overrides": [] - }, - "gridPos": { - "h": 5, - "w": 6, - "x": 4, - "y": 15 - }, - "id": 83, - "options": { - "colorMode": "value", - "graphMode": "area", - "justifyMode": "auto", - "orientation": "auto", - "reduceOptions": { - "calcs": [ - "lastNotNull" - ], - "fields": "", - "values": false - }, - "text": {}, - "textMode": "auto" - }, - "pluginVersion": "9.2.2", + "pluginVersion": "9.3.2", "targets": [ { "datasource": { - "type": "prometheus", - "uid": "P1809F7CD0C75ACF3" - }, - "editorMode": "code", - "expr": "tableland_eventprocessor_hash_calculation_elapsed_time / 1000", - "legendFormat": "Chain ID: {{chain_id}}", - "range": true, + "type": "prometheus", + "uid": "P1809F7CD0C75ACF3" + }, + "expr": "time() - tableland_backup_last_execution", "refId": "A" } ], - "title": "Last Hash Calculation Elapsed Time", + "title": "Last backup", "type": "stat" }, { @@ -684,20 +425,24 @@ { "color": "green", "value": null + }, + { + "color": "dark-red", + "value": 18000 } ] }, - "unit": "ms" + "unit": "dtdurations" }, "overrides": [] }, "gridPos": { - "h": 5, - "w": 4, - "x": 10, - "y": 15 + "h": 7, + "w": 8, + "x": 16, + "y": 8 }, - "id": 85, + "id": 83, "options": { "colorMode": "value", "graphMode": "area", @@ -710,9 +455,10 @@ "fields": "", "values": false }, + "text": {}, "textMode": "auto" }, - "pluginVersion": "9.2.2", + "pluginVersion": "9.3.2", "targets": [ { "datasource": { @@ -720,13 +466,13 @@ "uid": "P1809F7CD0C75ACF3" }, "editorMode": "code", - "expr": "runtime_uptime_milliseconds", - "legendFormat": "{{job}}", + "expr": "tableland_eventprocessor_hash_calculation_elapsed_time / 1000", + "legendFormat": "Chain ID: {{chain_id}}", "range": true, "refId": "A" } ], - "title": "Uptime", + "title": "Last Hash Calculation Elapsed Time", "type": "stat" }, { @@ -739,7 +485,7 @@ "h": 1, "w": 24, "x": 0, - "y": 20 + "y": 15 }, "id": 48, "panels": [], @@ -813,7 +559,7 @@ "h": 9, "w": 6, "x": 0, - "y": 21 + "y": 16 }, "id": 46, "options": { @@ -904,7 +650,7 @@ "h": 9, "w": 6, "x": 6, - "y": 21 + "y": 16 }, "id": 51, "options": { @@ -1000,7 +746,7 @@ "h": 9, "w": 6, "x": 12, - "y": 21 + "y": 16 }, "id": 49, "options": { @@ -1097,7 +843,7 @@ "h": 9, "w": 6, "x": 18, - "y": 21 + "y": 16 }, "id": 52, "options": { @@ -1163,7 +909,7 @@ "h": 7, "w": 6, "x": 0, - "y": 30 + "y": 25 }, "heatmap": {}, "hideZeroBuckets": true, @@ -1210,7 +956,7 @@ "unit": "ms" } }, - "pluginVersion": "9.2.2", + "pluginVersion": "9.3.2", "reverseYBuckets": false, "targets": [ { @@ -1276,7 +1022,7 @@ "h": 7, "w": 6, "x": 6, - "y": 30 + "y": 25 }, "heatmap": {}, "hideZeroBuckets": true, @@ -1323,7 +1069,7 @@ "unit": "ms" } }, - "pluginVersion": "9.2.2", + "pluginVersion": "9.3.2", "reverseYBuckets": false, "targets": [ { @@ -1392,7 +1138,7 @@ "h": 7, "w": 4, "x": 12, - "y": 30 + "y": 25 }, "id": 50, "options": { @@ -1410,7 +1156,7 @@ "text": {}, "textMode": "auto" }, - "pluginVersion": "9.2.2", + "pluginVersion": "9.3.2", "targets": [ { "datasource": { @@ -1437,7 +1183,7 @@ "h": 1, "w": 24, "x": 0, - "y": 37 + "y": 32 }, "id": 8, "panels": [], @@ -1531,7 +1277,7 @@ "h": 8, "w": 6, "x": 0, - "y": 38 + "y": 33 }, "id": 37, "options": { @@ -1642,7 +1388,7 @@ "h": 8, "w": 6, "x": 6, - "y": 38 + "y": 33 }, "id": 79, "options": { @@ -1665,7 +1411,7 @@ }, "editorMode": "code", "exemplar": true, - "expr": "sum by (http_server_name) (\n rate(http_server_request_count_total{service_name=\"tableland:api\", http_server_name!=\"rpc\"}[5m])\n)", + "expr": "sum by (http_server_name) (\n rate(http_server_request_count_total{service_name=\"tableland:api\"}[5m])\n)", "interval": "", "legendFormat": "__auto", "range": true, @@ -1737,7 +1483,7 @@ "h": 8, "w": 6, "x": 12, - "y": 38 + "y": 33 }, "id": 15, "options": { @@ -1760,7 +1506,7 @@ }, "editorMode": "code", "exemplar": true, - "expr": "histogram_quantile(0.95, sum(rate(http_server_duration_bucket{http_server_name!=\"rpc\"}[5m])) by (http_server_name, le))", + "expr": "histogram_quantile(0.95, sum(rate(http_server_duration_bucket{}[5m])) by (http_server_name, le))", "interval": "", "legendFormat": "__auto", "range": true, @@ -1847,8 +1593,8 @@ "gridPos": { "h": 8, "w": 6, - "x": 0, - "y": 46 + "x": 18, + "y": 33 }, "id": 80, "options": { @@ -1881,199 +1627,6 @@ "title": "API Response bytes/s ", "type": "timeseries" }, - { - "datasource": { - "type": "prometheus", - "uid": "P1809F7CD0C75ACF3" - }, - "fieldConfig": { - "defaults": { - "color": { - "mode": "palette-classic" - }, - "custom": { - "axisCenteredZero": false, - "axisColorMode": "text", - "axisLabel": "", - "axisPlacement": "auto", - "barAlignment": 0, - "drawStyle": "line", - "fillOpacity": 0, - "gradientMode": "none", - "hideFrom": { - "legend": false, - "tooltip": false, - "viz": false - }, - "lineInterpolation": "linear", - "lineWidth": 1, - "pointSize": 5, - "scaleDistribution": { - "type": "linear" - }, - "showPoints": "auto", - "spanNulls": false, - "stacking": { - "group": "A", - "mode": "none" - }, - "thresholdsStyle": { - "mode": "off" - } - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": null - }, - { - "color": "red", - "value": 80 - } - ] - }, - "unit": "reqps" - }, - "overrides": [] - }, - "gridPos": { - "h": 8, - "w": 6, - "x": 6, - "y": 46 - }, - "id": 6, - "options": { - "legend": { - "calcs": [], - "displayMode": "list", - "placement": "bottom", - "showLegend": true - }, - "tooltip": { - "mode": "single", - "sort": "none" - } - }, - "targets": [ - { - "datasource": { - "type": "prometheus", - "uid": "P1809F7CD0C75ACF3" - }, - "editorMode": "code", - "exemplar": true, - "expr": "sum by (method) (rate(tableland_mesa_call_latency_count{service_name=\"tableland:api\"}[5m]))\n", - "interval": "", - "legendFormat": "{{method}}", - "range": true, - "refId": "A" - } - ], - "title": "JSON-RPC req/s", - "type": "timeseries" - }, - { - "datasource": { - "type": "prometheus", - "uid": "P1809F7CD0C75ACF3" - }, - "description": "The latency of 95% tile. Note that CreateTable is a long running request since requires sending a txn to Ethereum. The rest should have low-ish latencies.", - "fieldConfig": { - "defaults": { - "color": { - "mode": "palette-classic" - }, - "custom": { - "axisCenteredZero": false, - "axisColorMode": "text", - "axisLabel": "", - "axisPlacement": "auto", - "barAlignment": 0, - "drawStyle": "line", - "fillOpacity": 0, - "gradientMode": "none", - "hideFrom": { - "legend": false, - "tooltip": false, - "viz": false - }, - "lineInterpolation": "linear", - "lineWidth": 1, - "pointSize": 5, - "scaleDistribution": { - "log": 2, - "type": "log" - }, - "showPoints": "auto", - "spanNulls": false, - "stacking": { - "group": "A", - "mode": "none" - }, - "thresholdsStyle": { - "mode": "off" - } - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": null - }, - { - "color": "red", - "value": 80 - } - ] - }, - "unit": "ms" - }, - "overrides": [] - }, - "gridPos": { - "h": 8, - "w": 6, - "x": 12, - "y": 46 - }, - "id": 56, - "maxDataPoints": 25, - "options": { - "legend": { - "calcs": [], - "displayMode": "list", - "placement": "bottom", - "showLegend": true - }, - "tooltip": { - "mode": "single", - "sort": "none" - } - }, - "targets": [ - { - "datasource": { - "type": "prometheus", - "uid": "P1809F7CD0C75ACF3" - }, - "exemplar": false, - "expr": "histogram_quantile(0.95, sum(rate(tableland_mesa_call_latency_bucket{service_name=\"tableland:api\"}[10m])) by (le, method))", - "format": "time_series", - "instant": false, - "interval": "", - "legendFormat": "{{method}}", - "refId": "A" - } - ], - "title": "JSON-RPC 95-th latency", - "type": "timeseries" - }, { "collapsed": false, "datasource": { @@ -2084,7 +1637,7 @@ "h": 1, "w": 24, "x": 0, - "y": 54 + "y": 41 }, "id": 12, "panels": [], @@ -2161,7 +1714,7 @@ "h": 8, "w": 6, "x": 0, - "y": 55 + "y": 42 }, "id": 17, "options": { @@ -2258,7 +1811,7 @@ "h": 8, "w": 6, "x": 6, - "y": 55 + "y": 42 }, "id": 57, "maxDataPoints": 25, @@ -2355,8 +1908,8 @@ "gridPos": { "h": 8, "w": 6, - "x": 0, - "y": 63 + "x": 12, + "y": 42 }, "id": 74, "maxDataPoints": 25, @@ -2466,8 +2019,8 @@ "gridPos": { "h": 8, "w": 6, - "x": 6, - "y": 63 + "x": 18, + "y": 42 }, "id": 75, "maxDataPoints": 25, @@ -2514,120 +2067,6 @@ "title": "Overall SQL query 95-th latency", "type": "timeseries" }, - { - "collapsed": true, - "datasource": { - "type": "datasource", - "uid": "grafana" - }, - "gridPos": { - "h": 1, - "w": 24, - "x": 0, - "y": 71 - }, - "id": 60, - "panels": [ - { - "datasource": { - "type": "prometheus", - "uid": "P1809F7CD0C75ACF3" - }, - "fieldConfig": { - "defaults": { - "color": { - "mode": "palette-classic" - }, - "custom": { - "axisLabel": "", - "axisPlacement": "auto", - "barAlignment": 0, - "drawStyle": "line", - "fillOpacity": 0, - "gradientMode": "none", - "hideFrom": { - "legend": false, - "tooltip": false, - "viz": false - }, - "lineInterpolation": "linear", - "lineWidth": 1, - "pointSize": 5, - "scaleDistribution": { - "type": "linear" - }, - "showPoints": "auto", - "spanNulls": false, - "stacking": { - "group": "A", - "mode": "none" - }, - "thresholdsStyle": { - "mode": "off" - } - }, - "displayName": "${__series.name}", - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green" - } - ] - } - }, - "overrides": [] - }, - "gridPos": { - "h": 7, - "w": 6, - "x": 0, - "y": 34 - }, - "id": 66, - "options": { - "legend": { - "calcs": [], - "displayMode": "list", - "placement": "bottom", - "showLegend": true - }, - "tooltip": { - "mode": "single", - "sort": "none" - } - }, - "pluginVersion": "8.2.0", - "targets": [ - { - "datasource": { - "type": "prometheus", - "uid": "P1809F7CD0C75ACF3" - }, - "exemplar": true, - "expr": "sum by(chain_id) (tableland_wallettracker_nonce{service_name=\"tableland:api\"})", - "interval": "", - "legendFormat": "ChainID {{chain_id}}", - "refId": "A" - } - ], - "title": "Nonces", - "type": "timeseries" - } - ], - "targets": [ - { - "datasource": { - "type": "datasource", - "uid": "grafana" - }, - "refId": "A" - } - ], - "title": "Wallet", - "type": "row" - }, { "collapsed": false, "datasource": { @@ -2638,7 +2077,7 @@ "h": 1, "w": 24, "x": 0, - "y": 72 + "y": 50 }, "id": 4, "panels": [], @@ -2717,7 +2156,7 @@ "h": 8, "w": 6, "x": 0, - "y": 73 + "y": 51 }, "id": 72, "options": { @@ -2813,7 +2252,7 @@ "h": 8, "w": 6, "x": 6, - "y": 73 + "y": 51 }, "id": 76, "options": { @@ -2909,7 +2348,7 @@ "h": 8, "w": 6, "x": 12, - "y": 73 + "y": 51 }, "id": 77, "options": { @@ -3005,7 +2444,7 @@ "h": 8, "w": 6, "x": 18, - "y": 73 + "y": 51 }, "id": 78, "options": { @@ -3054,6 +2493,6 @@ "timezone": "", "title": "Validator", "uid": "2Le7qt_7z", - "version": 9, + "version": 8, "weekStart": "" } \ No newline at end of file diff --git a/go.mod b/go.mod index 95fd03ae..f997ab6f 100644 --- a/go.mod +++ b/go.mod @@ -10,7 +10,6 @@ require ( github.com/golang-migrate/migrate/v4 v4.15.2 github.com/google/uuid v1.3.0 github.com/gorilla/mux v1.8.0 - github.com/hetiansu5/urlquery v1.2.7 github.com/json-iterator/go v1.1.12 github.com/klauspost/compress v1.16.3 github.com/mattn/go-sqlite3 v1.14.16 @@ -20,7 +19,6 @@ require ( github.com/rs/zerolog v1.29.0 github.com/sethvargo/go-limiter v0.7.2 github.com/spf13/cobra v1.6.1 - github.com/spruceid/siwe-go v0.2.1-0.20220804171946-fc1b0374f4ff github.com/stretchr/testify v1.8.2 github.com/tablelandnetwork/sqlparser v0.0.0-20221230162331-b318f234cefd github.com/textileio/cli v1.0.2 @@ -53,7 +51,6 @@ require ( github.com/cockroachdb/pebble v0.0.0-20230209160836-829675f94811 // indirect github.com/cockroachdb/redact v1.1.3 // indirect github.com/davecgh/go-spew v1.1.1 // indirect - github.com/dchest/uniuri v0.0.0-20200228104902-7aecb25e1fe5 // indirect github.com/deckarep/golang-set/v2 v2.1.0 // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 // indirect github.com/edsrzf/mmap-go v1.0.0 // indirect @@ -105,7 +102,6 @@ require ( github.com/prometheus/client_model v0.3.0 // indirect github.com/prometheus/common v0.39.0 // indirect github.com/prometheus/procfs v0.9.0 // indirect - github.com/relvacode/iso8601 v1.1.0 // indirect github.com/rogpeppe/go-internal v1.9.0 // indirect github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible // indirect github.com/spf13/afero v1.6.0 // indirect diff --git a/go.sum b/go.sum index 31f52859..caab37bc 100644 --- a/go.sum +++ b/go.sum @@ -406,8 +406,6 @@ github.com/d2g/hardwareaddr v0.0.0-20190221164911-e7d9fbe030e4/go.mod h1:bMl4RjI github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/dchest/uniuri v0.0.0-20200228104902-7aecb25e1fe5 h1:RAV05c0xOkJ3dZGS0JFybxFKZ2WMLabgx3uXnd7rpGs= -github.com/dchest/uniuri v0.0.0-20200228104902-7aecb25e1fe5/go.mod h1:GgB8SF9nRG+GqaDtLcwJZsQFhcogVCJ79j4EdT0c2V4= github.com/deckarep/golang-set/v2 v2.1.0 h1:g47V4Or+DUdzbs8FxCCmgb6VYd+ptPAngjM6dtGktsI= github.com/deckarep/golang-set/v2 v2.1.0/go.mod h1:VAky9rY/yGXJOLEDv3OMci+7wtDpOF4IN+y82NBOac4= github.com/decred/dcrd/crypto/blake256 v1.0.0 h1:/8DMNYp9SGi5f0w7uCm6d6M4OU2rGFK09Y2A4Xv7EE0= @@ -772,8 +770,6 @@ github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= -github.com/hetiansu5/urlquery v1.2.7 h1:jn0h+9pIRqUziSPnRdK/gJK8S5TCnk+HZZx5fRHf8K0= -github.com/hetiansu5/urlquery v1.2.7/go.mod h1:wFpZdTHRdwt7mk0EM/DdZEWtEN4xf8HJoH/BLXm/PG0= github.com/holiman/bloomfilter/v2 v2.0.3 h1:73e0e/V0tCydx14a0SCYS/EWCxgwLZ18CZcZKVu0fao= github.com/holiman/bloomfilter/v2 v2.0.3/go.mod h1:zpoh+gs7qcpqrHr3dB55AMiJwo0iURXE7ZOP9L9hSkA= github.com/holiman/uint256 v1.2.0 h1:gpSYcPLWGv4sG43I2mVLiDZCNDh/EpGjSk8tmtxitHM= @@ -1177,8 +1173,6 @@ github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1 github.com/prometheus/procfs v0.9.0 h1:wzCHvIvM5SxWqYvwgVL7yJY8Lz3PKn49KQtpgMYJfhI= github.com/prometheus/procfs v0.9.0/go.mod h1:+pB4zwohETzFnmlpe6yd2lSc+0/46IYZRB/chUwxUZY= github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= -github.com/relvacode/iso8601 v1.1.0 h1:2nV8sp0eOjpoKQ2vD3xSDygsjAx37NHG2UlZiCkDH4I= -github.com/relvacode/iso8601 v1.1.0/go.mod h1:FlNp+jz+TXpyRqgmM7tnzHHzBnz776kmAH2h3sZCn0I= github.com/remyoudompheng/bigfft v0.0.0-20190728182440-6a916e37a237/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= @@ -1268,8 +1262,6 @@ github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/y github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg= github.com/spf13/viper v1.8.1 h1:Kq1fyeebqsBfbjZj4EL7gj2IO0mMaiyjYUWcUsl2O44= github.com/spf13/viper v1.8.1/go.mod h1:o0Pch8wJ9BVSWGQMbra6iw0oQ5oktSIBaujf1rJH9Ns= -github.com/spruceid/siwe-go v0.2.1-0.20220804171946-fc1b0374f4ff h1:jBz/gRYeRf2MjtxtdTHUX3PbIYFJD2S7TViBrhGWbPA= -github.com/spruceid/siwe-go v0.2.1-0.20220804171946-fc1b0374f4ff/go.mod h1:X+Fj7GsCyLAjfrjkGzgoiRsoAh8+bz4fSz2N3jwN9bc= github.com/status-im/keycard-go v0.2.0 h1:QDLFswOQu1r5jsycloeQh3bVU8n/NatHHaZobtDnDzA= github.com/status-im/keycard-go v0.2.0/go.mod h1:wlp8ZLbsmrF6g6WjugPAx+IzoLrkdf9+mHxBEeo3Hbg= github.com/stefanberger/go-pkcs11uri v0.0.0-20201008174630-78d3cae3a980/go.mod h1:AO3tvPzVZ/ayst6UlUKUv6rcPQInYe3IknH3jYhAKu8= diff --git a/internal/chains/chains.go b/internal/chains/chains.go index e547abfe..c753f28f 100644 --- a/internal/chains/chains.go +++ b/internal/chains/chains.go @@ -5,16 +5,12 @@ import ( "github.com/textileio/go-tableland/pkg/eventprocessor" "github.com/textileio/go-tableland/pkg/sqlstore" - "github.com/textileio/go-tableland/pkg/tables" ) // ChainStack contains components running for a specific ChainID. type ChainStack struct { - Store sqlstore.SystemStore - Registry tables.TablelandTables - EventProcessor eventprocessor.EventProcessor - AllowTransactionRelay bool - + Store sqlstore.SystemStore + EventProcessor eventprocessor.EventProcessor // close gracefully closes all the chain stack components. Close func(ctx context.Context) error } diff --git a/internal/gateway/gateway.go b/internal/gateway/gateway.go new file mode 100644 index 00000000..2c393e80 --- /dev/null +++ b/internal/gateway/gateway.go @@ -0,0 +1,225 @@ +package gateway + +import ( + "context" + "database/sql" + "encoding/base64" + "errors" + "fmt" + "net/url" + "strings" + + "github.com/ethereum/go-ethereum/common" + logger "github.com/rs/zerolog/log" + "github.com/textileio/go-tableland/internal/router/middlewares" + "github.com/textileio/go-tableland/internal/tableland" + "github.com/textileio/go-tableland/pkg/parsing" + "github.com/textileio/go-tableland/pkg/sqlstore" + "github.com/textileio/go-tableland/pkg/tables" +) + +// ErrTableNotFound indicates that the table doesn't exist. +var ErrTableNotFound = errors.New("table not found") + +// Gateway defines the gateway operations. +type Gateway interface { + RunReadQuery(ctx context.Context, stmt string) (*tableland.TableData, error) + GetTableMetadata(context.Context, tables.TableID) (sqlstore.TableMetadata, error) + GetReceiptByTransactionHash(context.Context, common.Hash) (sqlstore.Receipt, bool, error) +} + +var log = logger.With().Str("component", "gateway").Logger() + +const ( + // DefaultMetadataImage is the default image for table's metadata. + DefaultMetadataImage = "https://bafkreifhuhrjhzbj4onqgbrmhpysk2mop2jimvdvfut6taiyzt2yqzt43a.ipfs.dweb.link" + + // DefaultAnimationURL is an empty string. It means that the attribute will not appear in the JSON metadata. + DefaultAnimationURL = "" +) + +// GatewayService implements the Gateway interface using SQLStore. +type GatewayService struct { + parser parsing.SQLValidator + extURLPrefix string + metadataRendererURI string + animationRendererURI string + stores map[tableland.ChainID]sqlstore.SystemStore +} + +var _ (Gateway) = (*GatewayService)(nil) + +// NewGateway creates a new gateway service. +func NewGateway( + parser parsing.SQLValidator, + stores map[tableland.ChainID]sqlstore.SystemStore, + extURLPrefix string, + metadataRendererURI string, + animationRendererURI string, +) (Gateway, error) { + if _, err := url.ParseRequestURI(extURLPrefix); err != nil { + return nil, fmt.Errorf("invalid external url prefix: %s", err) + } + + metadataRendererURI = strings.TrimRight(metadataRendererURI, "/") + if metadataRendererURI != "" { + if _, err := url.ParseRequestURI(metadataRendererURI); err != nil { + return nil, fmt.Errorf("metadata renderer uri could not be parsed: %s", err) + } + } + + animationRendererURI = strings.TrimRight(animationRendererURI, "/") + if animationRendererURI != "" { + if _, err := url.ParseRequestURI(animationRendererURI); err != nil { + return nil, fmt.Errorf("animation renderer uri could not be parsed: %s", err) + } + } + + return &GatewayService{ + parser: parser, + extURLPrefix: extURLPrefix, + metadataRendererURI: metadataRendererURI, + animationRendererURI: animationRendererURI, + stores: stores, + }, nil +} + +// GetTableMetadata returns table's metadata fetched from SQLStore. +func (g *GatewayService) GetTableMetadata(ctx context.Context, id tables.TableID) (sqlstore.TableMetadata, error) { + chainID, store, err := g.getStore(ctx) + if err != nil { + return sqlstore.TableMetadata{ + ExternalURL: fmt.Sprintf("%s/chain/%d/tables/%s", g.extURLPrefix, chainID, id), + Image: g.emptyMetadataImage(), + Message: "Chain isn't supported", + }, nil + } + table, err := store.GetTable(ctx, id) + if err != nil { + if !errors.Is(err, sql.ErrNoRows) { + log.Error().Err(err).Msg("error fetching the table") + return sqlstore.TableMetadata{ + ExternalURL: fmt.Sprintf("%s/chain/%d/tables/%s", g.extURLPrefix, chainID, id), + Image: g.emptyMetadataImage(), + Message: "Failed to fetch the table", + }, nil + } + + return sqlstore.TableMetadata{ + ExternalURL: fmt.Sprintf("%s/chain/%d/tables/%s", g.extURLPrefix, chainID, id), + Image: g.emptyMetadataImage(), + Message: "Table not found", + }, ErrTableNotFound + } + tableName := fmt.Sprintf("%s_%d_%s", table.Prefix, table.ChainID, table.ID) + schema, err := store.GetSchemaByTableName(ctx, tableName) + if err != nil { + return sqlstore.TableMetadata{}, fmt.Errorf("get table schema information: %s", err) + } + + return sqlstore.TableMetadata{ + Name: tableName, + ExternalURL: fmt.Sprintf("%s/chain/%d/tables/%s", g.extURLPrefix, table.ChainID, table.ID), + Image: g.getMetadataImage(table.ChainID, table.ID), + AnimationURL: g.getAnimationURL(table.ChainID, table.ID), + Attributes: []sqlstore.TableMetadataAttribute{ + { + DisplayType: "date", + TraitType: "created", + Value: table.CreatedAt.Unix(), + }, + }, + Schema: schema, + }, nil +} + +// GetReceiptByTransactionHash returns a receipt by transaction hash. +func (g *GatewayService) GetReceiptByTransactionHash( + ctx context.Context, + txnHash common.Hash, +) (sqlstore.Receipt, bool, error) { + _, store, err := g.getStore(ctx) + if err != nil { + return sqlstore.Receipt{}, false, fmt.Errorf("chain not found: %s", err) + } + + receipt, exists, err := store.GetReceipt(ctx, txnHash.Hex()) + if err != nil { + return sqlstore.Receipt{}, false, fmt.Errorf("transaction receipt lookup: %s", err) + } + if !exists { + return sqlstore.Receipt{}, false, nil + } + return sqlstore.Receipt{ + ChainID: receipt.ChainID, + BlockNumber: receipt.BlockNumber, + IndexInBlock: receipt.IndexInBlock, + TxnHash: receipt.TxnHash, + TableID: receipt.TableID, + Error: receipt.Error, + ErrorEventIdx: receipt.ErrorEventIdx, + }, true, nil +} + +// RunReadQuery allows the user to run SQL. +func (g *GatewayService) RunReadQuery(ctx context.Context, statement string) (*tableland.TableData, error) { + readStmt, err := g.parser.ValidateReadQuery(statement) + if err != nil { + return nil, fmt.Errorf("validating query: %s", err) + } + + queryResult, err := g.runSelect(ctx, readStmt) + if err != nil { + return nil, fmt.Errorf("running read statement: %s", err) + } + return queryResult, nil +} + +func (g *GatewayService) runSelect(ctx context.Context, stmt parsing.ReadStmt) (*tableland.TableData, error) { + var store sqlstore.SystemStore + for _, store = range g.stores { + break + } + + queryResult, err := store.Read(ctx, stmt) + if err != nil { + return nil, fmt.Errorf("executing read-query: %s", err) + } + + return queryResult, nil +} + +func (g *GatewayService) getStore(ctx context.Context) (tableland.ChainID, sqlstore.SystemStore, error) { + ctxChainID := ctx.Value(middlewares.ContextKeyChainID) + chainID, ok := ctxChainID.(tableland.ChainID) + if !ok { + return 0, nil, errors.New("no chain id found in context") + } + store, ok := g.stores[chainID] + if !ok { + return 0, nil, fmt.Errorf("chain id %d isn't supported in the validator", chainID) + } + return chainID, store, nil +} + +func (g *GatewayService) getMetadataImage(chainID tableland.ChainID, tableID tables.TableID) string { + if g.metadataRendererURI == "" { + return DefaultMetadataImage + } + + return fmt.Sprintf("%s/%d/%s", g.metadataRendererURI, chainID, tableID) +} + +func (g *GatewayService) getAnimationURL(chainID tableland.ChainID, tableID tables.TableID) string { + if g.animationRendererURI == "" { + return DefaultAnimationURL + } + + return fmt.Sprintf("%s/?chain=%d&id=%s", g.animationRendererURI, chainID, tableID) +} + +func (g *GatewayService) emptyMetadataImage() string { + svg := `` //nolint + svgEncoded := base64.StdEncoding.EncodeToString([]byte(svg)) + return fmt.Sprintf("data:image/svg+xml;base64,%s", svgEncoded) +} diff --git a/internal/gateway/gateway_instrumented.go b/internal/gateway/gateway_instrumented.go new file mode 100644 index 00000000..edf4a3eb --- /dev/null +++ b/internal/gateway/gateway_instrumented.go @@ -0,0 +1,105 @@ +package gateway + +import ( + "context" + "fmt" + "time" + + "github.com/ethereum/go-ethereum/common" + "github.com/textileio/go-tableland/internal/router/middlewares" + "github.com/textileio/go-tableland/internal/tableland" + "github.com/textileio/go-tableland/pkg/metrics" + "github.com/textileio/go-tableland/pkg/sqlstore" + "github.com/textileio/go-tableland/pkg/tables" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/metric/global" + "go.opentelemetry.io/otel/metric/instrument" +) + +// InstrumentedGateway implements the Gateway interface using SQLStore. +type InstrumentedGateway struct { + gateway Gateway + callCount instrument.Int64Counter + latencyHistogram instrument.Int64Histogram +} + +var _ (Gateway) = (*InstrumentedGateway)(nil) + +// NewInstrumentedGateway creates a new InstrumentedGateway. +func NewInstrumentedGateway(gateway Gateway) (Gateway, error) { + meter := global.MeterProvider().Meter("tableland") + callCount, err := meter.Int64Counter("tableland.system.call.count") + if err != nil { + return &InstrumentedGateway{}, fmt.Errorf("registering call counter: %s", err) + } + latencyHistogram, err := meter.Int64Histogram("tableland.system.call.latency") + if err != nil { + return &InstrumentedGateway{}, fmt.Errorf("registering latency histogram: %s", err) + } + + return &InstrumentedGateway{gateway, callCount, latencyHistogram}, nil +} + +// GetReceiptByTransactionHash implements system.SystemService. +func (g *InstrumentedGateway) GetReceiptByTransactionHash( + ctx context.Context, + hash common.Hash, +) (sqlstore.Receipt, bool, error) { + start := time.Now() + receipt, exists, err := g.gateway.GetReceiptByTransactionHash(ctx, hash) + latency := time.Since(start).Milliseconds() + chainID, _ := ctx.Value(middlewares.ContextKeyChainID).(tableland.ChainID) + + attributes := append([]attribute.KeyValue{ + {Key: "method", Value: attribute.StringValue("GetReceiptByTransactionHash")}, + {Key: "success", Value: attribute.BoolValue(err == nil)}, + {Key: "chainID", Value: attribute.Int64Value(int64(chainID))}, + }, metrics.BaseAttrs...) + + g.callCount.Add(ctx, 1, attributes...) + g.latencyHistogram.Record(ctx, latency, attributes...) + + return receipt, exists, err +} + +// GetTableMetadata returns table's metadata fetched from SQLStore. +func (g *InstrumentedGateway) GetTableMetadata( + ctx context.Context, + id tables.TableID, +) (sqlstore.TableMetadata, error) { + start := time.Now() + metadata, err := g.gateway.GetTableMetadata(ctx, id) + latency := time.Since(start).Milliseconds() + chainID, _ := ctx.Value(middlewares.ContextKeyChainID).(tableland.ChainID) + + // NOTE: we may face a risk of high-cardilatity in the future. This should be revised. + attributes := append([]attribute.KeyValue{ + {Key: "method", Value: attribute.StringValue("GetTableMetadata")}, + {Key: "success", Value: attribute.BoolValue(err == nil)}, + {Key: "chainID", Value: attribute.Int64Value(int64(chainID))}, + }, metrics.BaseAttrs...) + + g.callCount.Add(ctx, 1, attributes...) + g.latencyHistogram.Record(ctx, latency, attributes...) + + return metadata, err +} + +// RunReadQuery allows the user to run SQL. +func (g *InstrumentedGateway) RunReadQuery(ctx context.Context, statement string) (*tableland.TableData, error) { + start := time.Now() + data, err := g.gateway.RunReadQuery(ctx, statement) + latency := time.Since(start).Milliseconds() + chainID, _ := ctx.Value(middlewares.ContextKeyChainID).(tableland.ChainID) + + attributes := append([]attribute.KeyValue{ + {Key: "method", Value: attribute.StringValue("RunReadQuery")}, + {Key: "success", Value: attribute.BoolValue(err == nil)}, + {Key: "chainID", Value: attribute.Int64Value(int64(chainID))}, + }, metrics.BaseAttrs...) + + g.callCount.Add(ctx, 1, attributes...) + g.latencyHistogram.Record(ctx, latency, attributes...) + + return data, err +} diff --git a/internal/system/impl/sqlstore_test.go b/internal/gateway/gateway_test.go similarity index 56% rename from internal/system/impl/sqlstore_test.go rename to internal/gateway/gateway_test.go index fa0440db..02212eb1 100644 --- a/internal/system/impl/sqlstore_test.go +++ b/internal/gateway/gateway_test.go @@ -1,4 +1,4 @@ -package impl +package gateway import ( "context" @@ -11,10 +11,10 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/stretchr/testify/require" "github.com/textileio/go-tableland/internal/router/middlewares" - sys "github.com/textileio/go-tableland/internal/system" "github.com/textileio/go-tableland/internal/tableland" "github.com/textileio/go-tableland/pkg/eventprocessor/eventfeed" executor "github.com/textileio/go-tableland/pkg/eventprocessor/impl/executor/impl" + "github.com/textileio/go-tableland/pkg/parsing" parserimpl "github.com/textileio/go-tableland/pkg/parsing/impl" "github.com/textileio/go-tableland/pkg/sqlstore" "github.com/textileio/go-tableland/pkg/sqlstore/impl/system" @@ -25,9 +25,35 @@ import ( var chainID = tableland.ChainID(1337) -func TestSystemSQLStoreService(t *testing.T) { +func TestGatewayInitialization(t *testing.T) { t.Parallel() + t.Run("invalid external uri", func(t *testing.T) { + t.Parallel() + + _, err := NewGateway(nil, nil, "invalid uri", "", "") + require.Error(t, err) + require.ErrorContains(t, err, "invalid external url prefix") + }) + + t.Run("invalid metadata uri", func(t *testing.T) { + t.Parallel() + + _, err := NewGateway(nil, nil, "https://tableland.network/tables", "invalid uri", "") + require.Error(t, err) + require.ErrorContains(t, err, "metadata renderer uri could not be parsed") + }) + + t.Run("invalid animation uri", func(t *testing.T) { + t.Parallel() + + _, err := NewGateway(nil, nil, "https://tableland.network/tables", "https://render.tableland.xyz", "invalid uri") + require.Error(t, err) + require.ErrorContains(t, err, "animation renderer uri could not be parsed") + }) +} + +func TestGateway(t *testing.T) { dbURI := tests.Sqlite3URI(t) ctx := context.WithValue(context.Background(), middlewares.ContextKeyChainID, tableland.ChainID(1337)) @@ -60,14 +86,18 @@ func TestSystemSQLStoreService(t *testing.T) { }, }, }) + require.NoError(t, err) require.Nil(t, res.Error) require.Nil(t, res.ErrorEventIdx) require.NoError(t, bs.Commit()) require.NoError(t, bs.Close()) + parser, err = parserimpl.New([]string{"system_", "registry", "sqlite_"}) + require.NoError(t, err) + stack := map[tableland.ChainID]sqlstore.SystemStore{1337: store} - svc, err := NewSystemSQLStoreService(stack, "https://tableland.network/tables", "https://render.tableland.xyz", "") + svc, err := NewGateway(parser, stack, "https://tableland.network/tables", "https://render.tableland.xyz", "") require.NoError(t, err) metadata, err := svc.GetTableMetadata(ctx, id) require.NoError(t, err) @@ -80,86 +110,6 @@ func TestSystemSQLStoreService(t *testing.T) { // this is hard to test because the created_at comes from the database. just testing is not the 1970 value require.NotEqual(t, new(time.Time).Unix(), metadata.Attributes[0].Value) - - tables, err := svc.GetTablesByController(ctx, "0xb451cee4A42A652Fe77d373BAe66D42fd6B8D8FF") - require.NoError(t, err) - require.Equal(t, 1, len(tables)) - require.Equal(t, id, tables[0].ID) - require.Equal(t, "0xb451cee4A42A652Fe77d373BAe66D42fd6B8D8FF", tables[0].Controller) - require.Equal(t, "foo", tables[0].Prefix) - // echo -n bar:INT| shasum -a 256 - require.Equal(t, "5d70b398f938650871dd0d6d421e8d1d0c89fe9ed6c8a817c97e951186da7172", tables[0].Structure) - - tables, err = svc.GetTablesByStructure(ctx, "5d70b398f938650871dd0d6d421e8d1d0c89fe9ed6c8a817c97e951186da7172") - require.NoError(t, err) - require.Equal(t, 1, len(tables)) - require.Equal(t, id, tables[0].ID) - require.Equal(t, "0xb451cee4A42A652Fe77d373BAe66D42fd6B8D8FF", tables[0].Controller) - require.Equal(t, "foo", tables[0].Prefix) - // echo -n bar:INT| shasum -a 256 - require.Equal(t, "5d70b398f938650871dd0d6d421e8d1d0c89fe9ed6c8a817c97e951186da7172", tables[0].Structure) -} - -func TestGetSchemaByTableName(t *testing.T) { - t.Parallel() - - dbURI := tests.Sqlite3URI(t) - - ctx := context.WithValue(context.Background(), middlewares.ContextKeyChainID, tableland.ChainID(1337)) - store, err := system.New(dbURI, chainID) - require.NoError(t, err) - - parser, err := parserimpl.New([]string{"system_", "registry"}) - require.NoError(t, err) - - db, err := sql.Open("sqlite3", dbURI) - require.NoError(t, err) - db.SetMaxOpenConns(1) - - // populate the registry with a table - ex, err := executor.NewExecutor(1337, db, parser, 0, nil) - require.NoError(t, err) - bs, err := ex.NewBlockScope(ctx, 0) - require.NoError(t, err) - - res, err := bs.ExecuteTxnEvents(ctx, eventfeed.TxnEvents{ - TxnHash: common.HexToHash("0x0"), - Events: []interface{}{ - ðereum.ContractCreateTable{ - TableId: big.NewInt(42), - Owner: common.HexToAddress("0xb451cee4A42A652Fe77d373BAe66D42fd6B8D8FF"), - Statement: "create table foo_1337 (a integer primary key, b text not null default 'foo' unique, check (a > 0))", - }, - }, - }) - require.NoError(t, err) - require.Nil(t, res.Error) - require.Nil(t, res.ErrorEventIdx) - require.NoError(t, bs.Commit()) - require.NoError(t, bs.Close()) - - stack := map[tableland.ChainID]sqlstore.SystemStore{1337: store} - svc, err := NewSystemSQLStoreService(stack, "https://tableland.network/tables", "https://render.tableland.xyz", "") - require.NoError(t, err) - - schema, err := svc.GetSchemaByTableName(ctx, "foo_1337_42") - require.NoError(t, err) - require.Len(t, schema.Columns, 2) - require.Len(t, schema.TableConstraints, 1) - - require.Equal(t, "a", schema.Columns[0].Name) - require.Equal(t, "integer", schema.Columns[0].Type) - require.Len(t, schema.Columns[0].Constraints, 1) - require.Equal(t, "primary key autoincrement", schema.Columns[0].Constraints[0]) - - require.Equal(t, "b", schema.Columns[1].Name) - require.Equal(t, "text", schema.Columns[1].Type) - require.Len(t, schema.Columns[1].Constraints, 3) - require.Equal(t, "not null", schema.Columns[1].Constraints[0]) - require.Equal(t, "default 'foo'", schema.Columns[1].Constraints[1]) - require.Equal(t, "unique", schema.Columns[1].Constraints[2]) - - require.Equal(t, "check(a > 0)", schema.TableConstraints[0]) } func TestGetMetadata(t *testing.T) { @@ -208,7 +158,10 @@ func TestGetMetadata(t *testing.T) { t.Run("empty metadata uri", func(t *testing.T) { t.Parallel() - svc, err := NewSystemSQLStoreService(stack, "https://tableland.network/tables", "", "") + parser, err := parserimpl.New([]string{"system_", "registry", "sqlite_"}) + require.NoError(t, err) + + svc, err := NewGateway(parser, stack, "https://tableland.network/tables", "", "") require.NoError(t, err) metadata, err := svc.GetTableMetadata(ctx, id) @@ -224,7 +177,10 @@ func TestGetMetadata(t *testing.T) { t.Run("with metadata uri", func(t *testing.T) { t.Parallel() - svc, err := NewSystemSQLStoreService(stack, "https://tableland.network/tables", "https://render.tableland.xyz", "") + parser, err := parserimpl.New([]string{"system_", "registry", "sqlite_"}) + require.NoError(t, err) + + svc, err := NewGateway(parser, stack, "https://tableland.network/tables", "https://render.tableland.xyz", "") require.NoError(t, err) metadata, err := svc.GetTableMetadata(ctx, id) @@ -240,7 +196,10 @@ func TestGetMetadata(t *testing.T) { t.Run("with metadata uri trailing slash", func(t *testing.T) { t.Parallel() - svc, err := NewSystemSQLStoreService(stack, "https://tableland.network/tables", "https://render.tableland.xyz/", "") + parser, err := parserimpl.New([]string{"system_", "registry", "sqlite_"}) + require.NoError(t, err) + + svc, err := NewGateway(parser, stack, "https://tableland.network/tables", "https://render.tableland.xyz/", "") require.NoError(t, err) metadata, err := svc.GetTableMetadata(ctx, id) @@ -256,7 +215,10 @@ func TestGetMetadata(t *testing.T) { t.Run("with wrong metadata uri", func(t *testing.T) { t.Parallel() - _, err := NewSystemSQLStoreService(stack, "https://tableland.network/tables", "foo", "") + parser, err := parserimpl.New([]string{"system_", "registry", "sqlite_"}) + require.NoError(t, err) + + _, err = NewGateway(parser, stack, "https://tableland.network/tables", "foo", "") require.Error(t, err) require.ErrorContains(t, err, "metadata renderer uri could not be parsed") }) @@ -264,14 +226,17 @@ func TestGetMetadata(t *testing.T) { t.Run("non existent table", func(t *testing.T) { t.Parallel() - svc, err := NewSystemSQLStoreService(stack, "https://tableland.network/tables", "https://render.tableland.xyz", "") + parser, err := parserimpl.New([]string{"system_", "registry", "sqlite_"}) + require.NoError(t, err) + + svc, err := NewGateway(parser, stack, "https://tableland.network/tables", "https://render.tableland.xyz", "") require.NoError(t, err) id, _ := tables.NewTableID("43") require.NoError(t, err) metadata, err := svc.GetTableMetadata(ctx, id) - require.ErrorIs(t, err, sys.ErrTableNotFound) + require.ErrorIs(t, err, ErrTableNotFound) require.Equal(t, fmt.Sprintf("https://tableland.network/tables/chain/%d/tables/%s", 1337, id), metadata.ExternalURL) require.Equal(t, "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0nNTEyJyBoZWlnaHQ9JzUxMicgeG1sbnM9J2h0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnJz48cmVjdCB3aWR0aD0nNTEyJyBoZWlnaHQ9JzUxMicgZmlsbD0nIzAwMCcvPjwvc3ZnPg==", metadata.Image) // nolint require.Equal(t, "Table not found", metadata.Message) @@ -280,7 +245,11 @@ func TestGetMetadata(t *testing.T) { t.Run("with metadata uri and animation uri", func(t *testing.T) { t.Parallel() - svc, err := NewSystemSQLStoreService( + parser, err := parserimpl.New([]string{"system_", "registry", "sqlite_"}) + require.NoError(t, err) + + svc, err := NewGateway( + parser, stack, "https://tableland.network/tables", "https://render.tableland.xyz", @@ -300,74 +269,37 @@ func TestGetMetadata(t *testing.T) { }) } -func TestEVMEventPersistence(t *testing.T) { +func TestQueryConstraints(t *testing.T) { t.Parallel() - ctx := context.Background() dbURI := tests.Sqlite3URI(t) + ctx := context.WithValue(context.Background(), middlewares.ContextKeyChainID, tableland.ChainID(1337)) store, err := system.New(dbURI, chainID) require.NoError(t, err) + stack := map[tableland.ChainID]sqlstore.SystemStore{1337: store} - testData := []tableland.EVMEvent{ - { - Address: common.HexToAddress("0x10"), - Topics: []byte(`["0x111,"0x122"]`), - Data: []byte("data1"), - BlockNumber: 1, - TxHash: common.HexToHash("0x11"), - TxIndex: 11, - BlockHash: common.HexToHash("0x12"), - Index: 12, - ChainID: chainID, - EventJSON: []byte("eventjson1"), - EventType: "Type1", - }, - { - Address: common.HexToAddress("0x20"), - Topics: []byte(`["0x211,"0x222"]`), - Data: []byte("data2"), - BlockNumber: 2, - TxHash: common.HexToHash("0x21"), - TxIndex: 11, - BlockHash: common.HexToHash("0x22"), - Index: 12, - ChainID: chainID, - EventJSON: []byte("eventjson2"), - EventType: "Type2", - }, - } - - // Check that AreEVMEventsPersisted for the future txn hashes aren't found. - for _, event := range testData { - exists, err := store.AreEVMEventsPersisted(ctx, event.TxHash) - require.NoError(t, err) - require.False(t, exists) + parsingOpts := []parsing.Option{ + parsing.WithMaxReadQuerySize(44), } - err = store.SaveEVMEvents(ctx, testData) + parser, err := parserimpl.New([]string{"system_", "registry", "sqlite_"}, parsingOpts...) require.NoError(t, err) - // Check that AreEVMEventsPersisted for the future txn hashes are found, and the data matches. - for _, event := range testData { - exists, err := store.AreEVMEventsPersisted(ctx, event.TxHash) - require.NoError(t, err) - require.True(t, exists) + t.Run("read-query-size-nok", func(t *testing.T) { + t.Parallel() - events, err := store.GetEVMEvents(ctx, event.TxHash) + gateway, err := NewGateway( + parser, + stack, + "https://tableland.network/tables", + "https://render.tableland.xyz", + "https://render.tableland.xyz/anim", + ) require.NoError(t, err) - require.Len(t, events, 1) - - require.Equal(t, events[0].Address, event.Address) - require.Equal(t, events[0].Topics, event.Topics) - require.Equal(t, events[0].Data, event.Data) - require.Equal(t, events[0].BlockNumber, event.BlockNumber) - require.Equal(t, events[0].TxHash, event.TxHash) - require.Equal(t, events[0].TxIndex, event.TxIndex) - require.Equal(t, events[0].BlockHash, event.BlockHash) - require.Equal(t, events[0].Index, event.Index) - require.Equal(t, events[0].ChainID, chainID) - require.Equal(t, events[0].EventJSON, event.EventJSON) - require.Equal(t, events[0].EventType, event.EventType) - } + + _, err = gateway.RunReadQuery(ctx, "SELECT * FROM foo_1337_1 WHERE bar = 'hello2'") // length of 45 bytes + require.Error(t, err) + require.ErrorContains(t, err, "read query size is too long") + }) } diff --git a/internal/router/controllers/controller.go b/internal/router/controllers/controller.go index 2999831b..1c152c3b 100644 --- a/internal/router/controllers/controller.go +++ b/internal/router/controllers/controller.go @@ -6,18 +6,16 @@ import ( "fmt" "net/http" "strconv" - "strings" "time" "github.com/ethereum/go-ethereum/common" "github.com/gorilla/mux" - "github.com/hetiansu5/urlquery" "github.com/rs/zerolog/log" "github.com/textileio/go-tableland/buildinfo" "github.com/textileio/go-tableland/internal/formatter" + "github.com/textileio/go-tableland/internal/gateway" "github.com/textileio/go-tableland/internal/router/controllers/apiv1" "github.com/textileio/go-tableland/internal/router/middlewares" - "github.com/textileio/go-tableland/internal/system" "github.com/textileio/go-tableland/internal/tableland" "github.com/textileio/go-tableland/pkg/errors" "github.com/textileio/go-tableland/pkg/tables" @@ -31,15 +29,13 @@ type SQLRunner interface { // Controller defines the HTTP handlers for interacting with user tables. type Controller struct { - runner SQLRunner - systemService system.SystemService + gateway gateway.Gateway } // NewController creates a new Controller. -func NewController(runner SQLRunner, svc system.SystemService) *Controller { +func NewController(gateway gateway.Gateway) *Controller { return &Controller{ - runner: runner, - systemService: svc, + gateway: gateway, } } @@ -88,137 +84,6 @@ type Attribute struct { Value interface{} `json:"value"` } -func metadataConfigToMetadata(row map[string]*tableland.ColumnValue, config MetadataConfig) Metadata { - var md Metadata - if v, ok := row[config.Image]; ok { - md.Image = v - } - if v, ok := row[config.ImageTransparent]; ok { - md.ImageTransparent = v - } - if v, ok := row[config.ImageData]; ok { - md.ImageData = v - } - if v, ok := row[config.ExternalURL]; ok { - md.ExternalURL = v - } - if v, ok := row[config.Description]; ok { - md.Description = v - } - if v, ok := row[config.Name]; ok { - // Handle the special case where the source column for name is a number - if x, ok := v.Value().(int); ok { - md.Name = "#" + strconv.Itoa(x) - } else if y, ok := v.Value().(**int); ok { - md.Name = "#" + strconv.Itoa(*(*y)) - } else { - md.Name = v - } - } - if v, ok := row[config.BackgroundColor]; ok { - md.BackgroundColor = v - } - if v, ok := row[config.AnimationURL]; ok { - md.AnimationURL = v - } - if v, ok := row[config.YoutubeURL]; ok { - md.YoutubeURL = v - } - return md -} - -func userRowToMap(cols []tableland.Column, row []*tableland.ColumnValue) map[string]*tableland.ColumnValue { - m := make(map[string]*tableland.ColumnValue) - for i := range cols { - m[cols[i].Name] = row[i] - } - return m -} - -// GetTableRow handles the GET /chain/{chainID}/tables/{id}/{key}/{value} call. -// Use format=erc721 query param to generate JSON for ERC721 metadata. -// TODO(json-rpc): delete method when dropping support. -func (c *Controller) GetTableRow(rw http.ResponseWriter, r *http.Request) { - rw.Header().Set("Content-Type", "application/json") - vars := mux.Vars(r) - format := r.URL.Query().Get("format") - - id, err := tables.NewTableID(vars["id"]) - if err != nil { - rw.WriteHeader(http.StatusBadRequest) - _ = json.NewEncoder(rw).Encode(errors.ServiceError{Message: "Invalid id format"}) - log.Ctx(r.Context()).Error().Err(err).Msg("invalid id format") - return - } - - chainID := vars["chainID"] - stm := fmt.Sprintf("select prefix from registry where id=%s and chain_id=%s LIMIT 1", id.String(), chainID) - prefixRes, ok := c.runReadRequest(r.Context(), stm, rw) - if !ok { - return - } - - prefix := prefixRes.Rows[0][0].Value().(string) - - stm = fmt.Sprintf( - "SELECT * FROM %s_%s_%s WHERE %s=%s LIMIT 1", prefix, chainID, id.String(), vars["key"], vars["value"]) - res, ok := c.runReadRequest(r.Context(), stm, rw) - if !ok { - return - } - - switch format { - case "erc721": - var mdc MetadataConfig - if err := urlquery.Unmarshal([]byte(r.URL.RawQuery), &mdc); err != nil { - rw.WriteHeader(http.StatusBadRequest) - log.Ctx(r.Context()). - Error(). - Err(err). - Msg("invalid metadata config") - - _ = json.NewEncoder(rw).Encode(errors.ServiceError{Message: "Invalid metadata config"}) - return - } - - row := userRowToMap(res.Columns, res.Rows[0]) - md := metadataConfigToMetadata(row, mdc) - for i, ac := range mdc.Attributes { - if v, ok := row[mdc.Attributes[i].Column]; ok { - md.Attributes = append(md.Attributes, Attribute{ - DisplayType: ac.DisplayType, - TraitType: ac.TraitType, - Value: v, - }) - } - } - rw.WriteHeader(http.StatusOK) - _ = json.NewEncoder(rw).Encode(md) - default: - opts, err := formatterOptions(r) - if err != nil { - rw.WriteHeader(http.StatusBadRequest) - msg := fmt.Sprintf("Invalid formatting params: %v", err) - _ = json.NewEncoder(rw).Encode(errors.ServiceError{Message: msg}) - log.Ctx(r.Context()).Error().Err(err).Msg(msg) - return - } - formatted, config, err := formatter.Format(res, opts...) - if err != nil { - rw.WriteHeader(http.StatusInternalServerError) - msg := fmt.Sprintf("Error formatting data: %v", err) - _ = json.NewEncoder(rw).Encode(errors.ServiceError{Message: msg}) - log.Ctx(r.Context()).Error().Err(err).Msg(msg) - return - } - if config.Unwrap && len(res.Rows) > 1 { - rw.Header().Set("Content-Type", "application/jsonl+json") - } - rw.WriteHeader(http.StatusOK) - _, _ = rw.Write(formatted) - } -} - // Version returns git information of the running binary. func (c *Controller) Version(rw http.ResponseWriter, _ *http.Request) { rw.Header().Set("Content-type", "application/json") @@ -250,7 +115,7 @@ func (c *Controller) GetReceiptByTransactionHash(rw http.ResponseWriter, r *http } txnHash := common.HexToHash(paramTxnHash) - receipt, exists, err := c.systemService.GetReceiptByTransactionHash(ctx, txnHash) + receipt, exists, err := c.gateway.GetReceiptByTransactionHash(ctx, txnHash) if err != nil { rw.Header().Set("Content-Type", "application/json") rw.WriteHeader(http.StatusBadRequest) @@ -299,16 +164,8 @@ func (c *Controller) GetTable(rw http.ResponseWriter, r *http.Request) { return } - isAPIV1 := strings.HasPrefix(r.RequestURI, "/api/v1/tables") - - metadata, err := c.systemService.GetTableMetadata(ctx, id) - if err == system.ErrTableNotFound { - if !isAPIV1 { - rw.Header().Set("Content-type", "application/json") - rw.WriteHeader(http.StatusOK) - _ = json.NewEncoder(rw).Encode(metadata) - return - } + metadata, err := c.gateway.GetTableMetadata(ctx, id) + if err == gateway.ErrTableNotFound { rw.WriteHeader(http.StatusNotFound) return } @@ -360,163 +217,17 @@ func (c *Controller) GetTable(rw http.ResponseWriter, r *http.Request) { _ = enc.Encode(metadataV1) } -// GetTablesByController handles the GET /chain/{chainID}/tables/controller/{address} call. -// TODO(json-rpc): delete when dropping support. -func (c *Controller) GetTablesByController(rw http.ResponseWriter, r *http.Request) { - ctx := r.Context() - rw.Header().Set("Content-type", "application/json") - vars := mux.Vars(r) - - controller := vars["address"] - tables, err := c.systemService.GetTablesByController(ctx, controller) - if err != nil { - rw.WriteHeader(http.StatusInternalServerError) - log.Ctx(ctx). - Error(). - Err(err). - Str("request_address", controller). - Msg("failed to fetch tables") - - _ = json.NewEncoder(rw).Encode(errors.ServiceError{Message: "Failed to fetch tables"}) - return - } - - // This struct is used since we don't want to return an ID field. - // The Name will be {optional-prefix}_{chainId}_{tableId}. - // Not doing `omitempty` in tableland.Table since - // that feels hacky. Looks safer to define a separate type here at the handler level. - type tableNameIDUnified struct { - Controller string `json:"controller"` - Name string `json:"name"` - Structure string `json:"structure"` - } - retTables := make([]tableNameIDUnified, len(tables)) - for i, t := range tables { - retTables[i] = tableNameIDUnified{ - Controller: t.Controller, - Name: t.Name(), - Structure: t.Structure, - } - } - - rw.WriteHeader(http.StatusOK) - _ = json.NewEncoder(rw).Encode(retTables) -} - -// GetTablesByStructureHash handles the GET /chain/{id}/tables/structure/{hash} call. -// TODO(json-rpc): delete when dropping support. -func (c *Controller) GetTablesByStructureHash(rw http.ResponseWriter, r *http.Request) { - ctx := r.Context() - - rw.Header().Set("Content-type", "application/json") - vars := mux.Vars(r) - - hash := vars["hash"] - tables, err := c.systemService.GetTablesByStructure(ctx, hash) - if err != nil { - rw.WriteHeader(http.StatusInternalServerError) - log.Ctx(ctx). - Error(). - Err(err). - Str("hash", hash). - Msg("failed to fetch tables") - - _ = json.NewEncoder(rw).Encode(errors.ServiceError{Message: "Failed to fetch tables"}) - return - } - - type tableNameIDUnified struct { - Controller string `json:"controller"` - Name string `json:"name"` - Structure string `json:"structure"` - } - retTables := make([]tableNameIDUnified, len(tables)) - for i, t := range tables { - retTables[i] = tableNameIDUnified{ - Controller: t.Controller, - Name: t.Name(), - Structure: t.Structure, - } - } - - rw.WriteHeader(http.StatusOK) - _ = json.NewEncoder(rw).Encode(retTables) -} - -// GetSchemaByTableName handles the GET /schema/{table_name} call. -// TODO(json-rpc): delete when droppping support. -func (c *Controller) GetSchemaByTableName(rw http.ResponseWriter, r *http.Request) { - ctx := r.Context() - - rw.Header().Set("Content-type", "application/json") - vars := mux.Vars(r) - - name := vars["table_name"] - schema, err := c.systemService.GetSchemaByTableName(ctx, name) - if err != nil { - rw.WriteHeader(http.StatusInternalServerError) - log.Ctx(ctx). - Error(). - Err(err). - Str("table_name", name). - Msg("failed to fetch tables") - - _ = json.NewEncoder(rw).Encode(errors.ServiceError{Message: "Failed to get schema from table"}) - return - } - - if len(schema.Columns) == 0 { - rw.WriteHeader(http.StatusInternalServerError) - log.Ctx(ctx). - Warn(). - Str("name", name). - Msg("table does not exist") - - _ = json.NewEncoder(rw).Encode(errors.ServiceError{Message: "Table does not exist"}) - return - } - - type Column struct { - Name string `json:"name"` - Type string `json:"type"` - Constraints []string `json:"constraints"` - } - - type response struct { - Columns []Column `json:"columns"` - TableConstraints []string `json:"table_constraints"` - } - - columns := make([]Column, len(schema.Columns)) - for i, col := range schema.Columns { - columns[i] = Column{ - Name: col.Name, - Type: col.Type, - Constraints: col.Constraints, - } - } - - rw.WriteHeader(http.StatusOK) - _ = json.NewEncoder(rw).Encode(response{ - Columns: columns, - TableConstraints: schema.TableConstraints, - }) -} - // HealthHandler serves health check requests. func HealthHandler(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusOK) } -// GetTableQuery handles the GET /query?s=[statement] call. -// Use mode=columns|rows|json|lines query param to control output format. +// GetTableQuery handles the GET /query?statement=[statement] call. +// Use format=objects|table query param to control output format. func (c *Controller) GetTableQuery(rw http.ResponseWriter, r *http.Request) { rw.Header().Set("Content-Type", "application/json") - stm := r.URL.Query().Get("s") // TODO(json-rpc): remove query parameter "s" when dropping support. - if stm == "" { - stm = r.URL.Query().Get("statement") - } + stm := r.URL.Query().Get("statement") start := time.Now() res, ok := c.runReadRequest(r.Context(), stm, rw) @@ -542,7 +253,7 @@ func (c *Controller) GetTableQuery(rw http.ResponseWriter, r *http.Request) { return } - CollectReadQueryMetric(r.Context(), stm, config, took) + collectReadQueryMetric(r.Context(), stm, config, took) rw.WriteHeader(http.StatusOK) if config.Unwrap && len(res.Rows) > 1 { @@ -556,7 +267,7 @@ func (c *Controller) runReadRequest( stm string, rw http.ResponseWriter, ) (*tableland.TableData, bool) { - res, err := c.runner.RunReadQuery(ctx, stm) + res, err := c.gateway.RunReadQuery(ctx, stm) if err != nil { rw.WriteHeader(http.StatusBadRequest) log.Ctx(ctx). @@ -602,13 +313,11 @@ type formatterParams struct { func getFormatterParams(r *http.Request) (formatterParams, error) { c := formatterParams{} - output := r.URL.Query().Get("output") // TODO(json-rpc): drop "output" when dropping support. - if output == "" { - output = r.URL.Query().Get("format") - } + output := r.URL.Query().Get("format") extract := r.URL.Query().Get("extract") unwrap := r.URL.Query().Get("unwrap") + if output != "" { output, ok := formatter.OutputFromString(output) if !ok { @@ -631,23 +340,11 @@ func getFormatterParams(r *http.Request) (formatterParams, error) { c.unwrap = &unwrap } - // Special handling for old mode param - mode := r.URL.Query().Get("mode") - if mode == "list" { - v := true - c.unwrap = &v - c.extract = &v - } else if mode == "json" { - v := formatter.Objects - c.output = &v - } - return c, nil } -// CollectReadQueryMetric collects read query metric. -// It is used for JSON-RPC service. When that is deleted we can make this private. -func CollectReadQueryMetric(ctx context.Context, statement string, config formatter.FormatConfig, took time.Duration) { +// collectReadQueryMetric collects read query metric. +func collectReadQueryMetric(ctx context.Context, statement string, config formatter.FormatConfig, took time.Duration) { value := ctx.Value(middlewares.ContextIPAddress) ipAddress, ok := value.(string) if ok && ipAddress != "" { diff --git a/internal/router/controllers/controller_test.go b/internal/router/controllers/controller_test.go index 7fb8ecd0..0d907502 100644 --- a/internal/router/controllers/controller_test.go +++ b/internal/router/controllers/controller_test.go @@ -1,127 +1,26 @@ package controllers import ( + "context" "errors" "fmt" "net/http" "net/http/httptest" + "strconv" "strings" "testing" "github.com/gorilla/mux" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" - systemimpl "github.com/textileio/go-tableland/internal/system/impl" + "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/sqlstore" ) -func TestGetTableRow(t *testing.T) { - t.Parallel() - - req, err := http.NewRequest("GET", "/chain/69/tables/100/id/1", nil) - require.NoError(t, err) - - ctrl := NewController(newTableRowRunnerMock(t), nil) - - router := mux.NewRouter() - router.HandleFunc("/chain/{chainID}/tables/{id}/{key}/{value}", ctrl.GetTableRow) - - rr := httptest.NewRecorder() - router.ServeHTTP(rr, req) - require.Equal(t, http.StatusOK, rr.Code) - - expJSON := `[{"id":1,"description":"Friendly OpenSea Creature that enjoys long swims in the ocean.","image":"https://storage.googleapis.com/opensea-prod.appspot.com/creature/3.png","external_url":"https://example.com/?token_id=3","base":"Starfish","eyes":"Big","mouth":"Surprised","level":5,"stamina":1.4,"personality":"Sad","aqua_power":40,"stamina_increase":10,"generation":2}]` // nolint - require.JSONEq(t, expJSON, rr.Body.String()) -} - -func TestERC721Metadata(t *testing.T) { - t.Parallel() - - req, err := http.NewRequest("GET", "/chain/69/tables/100/id/1?format=erc721&name=id&image=image&description=description&external_url=external_url&attributes[0][column]=base&attributes[0][trait_type]=Base&attributes[1][column]=eyes&attributes[1][trait_type]=Eyes&attributes[2][column]=mouth&attributes[2][trait_type]=Mouth&attributes[3][column]=level&attributes[3][trait_type]=Level&attributes[4][column]=stamina&attributes[4][trait_type]=Stamina&attributes[5][column]=personality&attributes[5][trait_type]=Personality&attributes[6][column]=aqua_power&attributes[6][display_type]=boost_number&attributes[6][trait_type]=Aqua%20Power&attributes[7][column]=stamina_increase&attributes[7][display_type]=boost_percentage&attributes[7][trait_type]=Stamina%20Increase&attributes[8][column]=generation&attributes[8][display_type]=number&attributes[8][trait_type]=Generation", nil) // nolint - require.NoError(t, err) - - ctrl := NewController(newTableRowRunnerMock(t), nil) - - router := mux.NewRouter() - router.HandleFunc("/chain/{chainID}/tables/{id}/{key}/{value}", ctrl.GetTableRow) - - rr := httptest.NewRecorder() - router.ServeHTTP(rr, req) - require.Equal(t, http.StatusOK, rr.Code) - - expJSON := `{"image":"https://storage.googleapis.com/opensea-prod.appspot.com/creature/3.png","external_url":"https://example.com/?token_id=3","description":"Friendly OpenSea Creature that enjoys long swims in the ocean.","name":"#1","attributes":[{"trait_type":"Base","value":"Starfish"},{"trait_type":"Eyes","value":"Big"},{"trait_type":"Mouth","value":"Surprised"},{"trait_type":"Level","value":5},{"trait_type":"Stamina","value":1.4},{"trait_type":"Personality","value":"Sad"},{"display_type":"boost_number","trait_type":"Aqua Power","value":40},{"display_type":"boost_percentage","trait_type":"Stamina Increase","value":10},{"display_type":"number","trait_type":"Generation","value":2}]}` // nolint - require.JSONEq(t, expJSON, rr.Body.String()) -} - -func TestBadQuery(t *testing.T) { - t.Parallel() - - r := mocks.NewSQLRunner(t) - r.EXPECT().RunReadQuery(mock.Anything, mock.Anything).Return(nil, errors.New("bad query error message")) - - req, err := http.NewRequest("GET", "/chain/69/tables/100/invalid_column/0", nil) - require.NoError(t, err) - - ctrl := NewController(r, nil) - - router := mux.NewRouter() - router.HandleFunc("/chain/{chainID}/tables/{id}/{key}/{value}", ctrl.GetTableRow) - - rr := httptest.NewRecorder() - router.ServeHTTP(rr, req) - - require.Equal(t, http.StatusBadRequest, rr.Code) - - expJSON := `{"message": "bad query error message"}` - require.JSONEq(t, expJSON, rr.Body.String()) -} - -func TestRowNotFound(t *testing.T) { - t.Parallel() - - r := mocks.NewSQLRunner(t) - r.EXPECT().RunReadQuery(mock.Anything, mock.Anything).Return( - &tableland.TableData{ - Columns: []tableland.Column{ - {Name: "id"}, - {Name: "description"}, - {Name: "image"}, - {Name: "external_url"}, - {Name: "base"}, - {Name: "eyes"}, - {Name: "mouth"}, - {Name: "level"}, - {Name: "stamina"}, - {Name: "personality"}, - {Name: "aqua_power"}, - {Name: "stamina_increase"}, - {Name: "generation"}, - }, - Rows: [][]*tableland.ColumnValue{}, - }, - nil, - ) - - req, err := http.NewRequest("GET", "/chain/69/tables/100/id/1", nil) - require.NoError(t, err) - - ctrl := NewController(r, nil) - - router := mux.NewRouter() - router.HandleFunc("/chain/{chainID}/tables/{id}/{key}/{value}", ctrl.GetTableRow) - - rr := httptest.NewRecorder() - router.ServeHTTP(rr, req) - - require.Equal(t, http.StatusNotFound, rr.Code) - - expJSON := `{"message": "Row not found"}` - require.JSONEq(t, expJSON, rr.Body.String()) -} - func TestQuery(t *testing.T) { - r := mocks.NewSQLRunner(t) + r := mocks.NewGateway(t) r.EXPECT().RunReadQuery(mock.Anything, mock.AnythingOfType("string")).Return( &tableland.TableData{ Columns: []tableland.Column{ @@ -150,13 +49,14 @@ func TestQuery(t *testing.T) { nil, ) - ctrl := NewController(r, nil) + ctrl := NewController(r) router := mux.NewRouter() router.HandleFunc("/query", ctrl.GetTableQuery) - // Table output - req, err := http.NewRequest("GET", "/query?s=select%20*%20from%20foo%3B&output=table", nil) + ctx := context.WithValue(context.Background(), middlewares.ContextIPAddress, strconv.Itoa(1)) + // Table format + req, err := http.NewRequestWithContext(ctx, "GET", "/query?statement=select%20*%20from%20foo%3B&format=table", nil) require.NoError(t, err) rr := httptest.NewRecorder() router.ServeHTTP(rr, req) @@ -164,8 +64,8 @@ func TestQuery(t *testing.T) { exp := `{"columns":[{"name":"id"},{"name":"eyes"},{"name":"mouth"}],"rows":[[1,"Big","Surprised"],[2,"Medium","Sad"],[3,"Small","Happy"]]}` // nolint require.JSONEq(t, exp, rr.Body.String()) - // Object output - req, err = http.NewRequest("GET", "/query?s=select%20*%20from%20foo%3B&output=objects", nil) + // Object format + req, err = http.NewRequest("GET", "/query?statement=select%20*%20from%20foo%3B&format=objects", nil) require.NoError(t, err) rr = httptest.NewRecorder() router.ServeHTTP(rr, req) @@ -173,8 +73,8 @@ func TestQuery(t *testing.T) { exp = `[{"eyes":"Big","id":1,"mouth":"Surprised"},{"eyes":"Medium","id":2,"mouth":"Sad"},{"eyes":"Small","id":3,"mouth":"Happy"}]` // nolint require.JSONEq(t, exp, rr.Body.String()) - // Unwrapped object output - req, err = http.NewRequest("GET", "/query?s=select%20*%20from%20foo%3B&output=objects&unwrap=true", nil) + // Unwrapped object format + req, err = http.NewRequest("GET", "/query?statement=select%20*%20from%20foo%3B&format=objects&unwrap=true", nil) require.NoError(t, err) rr = httptest.NewRecorder() router.ServeHTTP(rr, req) @@ -188,59 +88,8 @@ func TestQuery(t *testing.T) { } } -func TestLegacyQuery(t *testing.T) { - r := mocks.NewSQLRunner(t) - r.EXPECT().RunReadQuery(mock.Anything, mock.AnythingOfType("string")).Return( - &tableland.TableData{ - Columns: []tableland.Column{ - {Name: "name"}, - }, - Rows: [][]*tableland.ColumnValue{ - { - tableland.OtherColValue("Bob"), - }, - { - tableland.OtherColValue("John"), - }, - { - tableland.OtherColValue("Jane"), - }, - }, - }, - nil, - ) - - ctrl := NewController(r, nil) - - router := mux.NewRouter() - router.HandleFunc("/query", ctrl.GetTableQuery) - - // Mode = json - req, err := http.NewRequest("GET", "/query?s=select%20*%20from%20foo%3B&mode=json", nil) - require.NoError(t, err) - rr := httptest.NewRecorder() - router.ServeHTTP(rr, req) - require.Equal(t, http.StatusOK, rr.Code) - exp := `[{"name":"Bob"},{"name":"John"},{"name":"Jane"}]` - require.JSONEq(t, exp, rr.Body.String()) - - // Mode = list - req, err = http.NewRequest("GET", "/query?s=select%20*%20from%20foo%3B&mode=list", nil) - require.NoError(t, err) - rr = httptest.NewRecorder() - router.ServeHTTP(rr, req) - require.Equal(t, http.StatusOK, rr.Code) - exp = "\"Bob\"\n\"John\"\n\"Jane\"\n" - wantStrings := parseJSONLString(exp) - gotStrings := parseJSONLString(rr.Body.String()) - require.Equal(t, len(wantStrings), len(gotStrings)) - for i, wantString := range wantStrings { - require.JSONEq(t, wantString, gotStrings[i]) - } -} - func TestQueryExtracted(t *testing.T) { - r := mocks.NewSQLRunner(t) + r := mocks.NewGateway(t) r.EXPECT().RunReadQuery(mock.Anything, mock.AnythingOfType("string")).Return( &tableland.TableData{ Columns: []tableland.Column{{Name: "name"}}, @@ -253,13 +102,13 @@ func TestQueryExtracted(t *testing.T) { nil, ) - ctrl := NewController(r, nil) + ctrl := NewController(r) router := mux.NewRouter() router.HandleFunc("/query", ctrl.GetTableQuery) - // Extracted object output - req, err := http.NewRequest("GET", "/query?s=select%20*%20from%20foo%3B&output=objects&extract=true", nil) + // Extracted object format + req, err := http.NewRequest("GET", "/query?statement=select%20*%20from%20foo%3B&format=objects&extract=true", nil) require.NoError(t, err) rr := httptest.NewRecorder() router.ServeHTTP(rr, req) @@ -267,8 +116,12 @@ func TestQueryExtracted(t *testing.T) { exp := `["bob","jane","alex"]` require.JSONEq(t, exp, rr.Body.String()) - // Extracted unwrapped object output - req, err = http.NewRequest("GET", "/query?s=select%20*%20from%20foo%3B&output=objects&unwrap=true&extract=true", nil) + // Extracted unwrapped object format + req, err = http.NewRequest( + "GET", + "/query?statement=select%20*%20from%20foo%3B&format=objects&unwrap=true&extract=true", + nil, + ) require.NoError(t, err) rr = httptest.NewRecorder() router.ServeHTTP(rr, req) @@ -285,8 +138,32 @@ func TestQueryExtracted(t *testing.T) { func TestGetTablesByMocked(t *testing.T) { t.Parallel() - systemService := systemimpl.NewSystemMockService() - ctrl := NewController(nil, systemService) + gateway := mocks.NewGateway(t) + gateway.EXPECT().GetTableMetadata(mock.Anything, mock.Anything).Return( + sqlstore.TableMetadata{ + Name: "name-1", + ExternalURL: "https://tableland.network/tables/100", + Image: "https://bafkreifhuhrjhzbj4onqgbrmhpysk2mop2jimvdvfut6taiyzt2yqzt43a.ipfs.dweb.link", + Attributes: []sqlstore.TableMetadataAttribute{ + { + DisplayType: "date", + TraitType: "created", + Value: 1546360800, + }, + }, + Schema: sqlstore.TableSchema{ + Columns: []sqlstore.ColumnSchema{ + { + Name: "foo", + Type: "text", + }, + }, + }, + }, + nil, + ) + + ctrl := NewController(gateway) t.Run("get table metadata", func(t *testing.T) { t.Parallel() @@ -310,91 +187,6 @@ func TestGetTablesByMocked(t *testing.T) { }` require.JSONEq(t, expJSON, rr.Body.String()) }) - - t.Run("get tables by controller", func(t *testing.T) { - t.Parallel() - req, err := http.NewRequest("GET", "/chain/1337/tables/controller/0x2a891118Cf3a8FdeBb00109ea3ed4E33B82D960f", nil) - require.NoError(t, err) - - router := mux.NewRouter() - router.HandleFunc("/chain/{chainID}/tables/controller/{hash}", ctrl.GetTablesByController) - - rr := httptest.NewRecorder() - router.ServeHTTP(rr, req) - require.Equal(t, http.StatusOK, rr.Code) - - //nolint - expJSON := `[ - { - "controller":"0x2a891118Cf3a8FdeBb00109ea3ed4E33B82D960f", - "name":"test_1337_0", - "structure":"0605f6c6705c7c1257edb2d61d94a03ad15f1d253a5a75525c6da8cda34a99ee" - }, - { - "controller":"0x2a891118Cf3a8FdeBb00109ea3ed4E33B82D960f", - "name":"test2_1337_1", - "structure":"0605f6c6705c7c1257edb2d61d94a03ad15f1d253a5a75525c6da8cda34a99ee" - }]` - require.JSONEq(t, expJSON, rr.Body.String()) - }) - - t.Run("get tables by structure", func(t *testing.T) { - t.Parallel() - req, err := http.NewRequest("GET", "/chain/1337/tables/structure/0605f6c6705c7c1257edb2d61d94a03ad15f1d253a5a75525c6da8cda34a99eek", nil) // nolint - require.NoError(t, err) - - router := mux.NewRouter() - router.HandleFunc("/chain/{chainID}/tables/structure/{hash}", ctrl.GetTablesByStructureHash) - - rr := httptest.NewRecorder() - router.ServeHTTP(rr, req) - require.Equal(t, http.StatusOK, rr.Code) - - //nolint - expJSON := `[ - { - "controller":"0x2a891118Cf3a8FdeBb00109ea3ed4E33B82D960f", - "name":"test_1337_0", - "structure":"0605f6c6705c7c1257edb2d61d94a03ad15f1d253a5a75525c6da8cda34a99ee" - }, - { - "controller":"0x2a891118Cf3a8FdeBb00109ea3ed4E33B82D960f", - "name":"test2_1337_1", - "structure":"0605f6c6705c7c1257edb2d61d94a03ad15f1d253a5a75525c6da8cda34a99ee" - }]` - require.JSONEq(t, expJSON, rr.Body.String()) - }) - - t.Run("get schema by table name", func(t *testing.T) { - t.Parallel() - req, err := http.NewRequest("GET", "/schema/test_1337_0", nil) // nolint - require.NoError(t, err) - - router := mux.NewRouter() - router.HandleFunc("/schema/{table_name}", ctrl.GetSchemaByTableName) - - rr := httptest.NewRecorder() - router.ServeHTTP(rr, req) - require.Equal(t, http.StatusOK, rr.Code) - - //nolint - expJSON := `{ - "columns": [ - { - "name" : "a", - "type" : "int", - "constraints" : ["PRIMARY KEY"] - }, - { - "name" : "b", - "type" : "text", - "constraints" : ["DEFAULT ''"] - } - ], - "table_constraints": ["CHECK check (a > 0)"] - }` - require.JSONEq(t, expJSON, rr.Body.String()) - }) } func TestGetTableWithInvalidID(t *testing.T) { @@ -405,11 +197,11 @@ func TestGetTableWithInvalidID(t *testing.T) { req, err := http.NewRequest("GET", path, nil) require.NoError(t, err) - systemService := systemimpl.NewSystemMockService() - systemController := NewController(nil, systemService) + gateway := mocks.NewGateway(t) + ctrl := NewController(gateway) router := mux.NewRouter() - router.HandleFunc("/tables/{id}", systemController.GetTable) + router.HandleFunc("/tables/{id}", ctrl.GetTable) rr := httptest.NewRecorder() router.ServeHTTP(rr, req) @@ -426,11 +218,16 @@ func TestTableNotFoundMock(t *testing.T) { req, err := http.NewRequest("GET", "/tables/100", nil) require.NoError(t, err) - systemService := systemimpl.NewSystemMockErrService() - systemController := NewController(nil, systemService) + gateway := mocks.NewGateway(t) + gateway.EXPECT().GetTableMetadata(mock.Anything, mock.Anything).Return( + sqlstore.TableMetadata{}, + errors.New("failed"), + ) + + ctrl := NewController(gateway) router := mux.NewRouter() - router.HandleFunc("/tables/{tableId}", systemController.GetTable) + router.HandleFunc("/tables/{tableId}", ctrl.GetTable) rr := httptest.NewRecorder() router.ServeHTTP(rr, req) @@ -441,55 +238,6 @@ func TestTableNotFoundMock(t *testing.T) { require.JSONEq(t, expJSON, rr.Body.String()) } -func newTableRowRunnerMock(t *testing.T) SQLRunner { - r := mocks.NewSQLRunner(t) - r.EXPECT().RunReadQuery(mock.Anything, mock.Anything).Return( - &tableland.TableData{ - Columns: []tableland.Column{{Name: "prefix"}}, - Rows: [][]*tableland.ColumnValue{{tableland.OtherColValue("foo")}}, - }, - nil, - ).Once() - r.EXPECT().RunReadQuery(mock.Anything, mock.Anything).Return( - &tableland.TableData{ - Columns: []tableland.Column{ - {Name: "id"}, - {Name: "description"}, - {Name: "image"}, - {Name: "external_url"}, - {Name: "base"}, - {Name: "eyes"}, - {Name: "mouth"}, - {Name: "level"}, - {Name: "stamina"}, - {Name: "personality"}, - {Name: "aqua_power"}, - {Name: "stamina_increase"}, - {Name: "generation"}, - }, - Rows: [][]*tableland.ColumnValue{ - { - tableland.OtherColValue(1), - tableland.OtherColValue("Friendly OpenSea Creature that enjoys long swims in the ocean."), - tableland.OtherColValue("https://storage.googleapis.com/opensea-prod.appspot.com/creature/3.png"), - tableland.OtherColValue("https://example.com/?token_id=3"), - tableland.OtherColValue("Starfish"), - tableland.OtherColValue("Big"), - tableland.OtherColValue("Surprised"), - tableland.OtherColValue(5), - tableland.OtherColValue(1.4), - tableland.OtherColValue("Sad"), - tableland.OtherColValue(40), - tableland.OtherColValue(10), - tableland.OtherColValue(2), - }, - }, - }, - nil, - ).Once() - return r -} - func parseJSONLString(val string) []string { s := strings.TrimRight(val, "\n") return strings.Split(s, "\n") diff --git a/internal/router/controllers/legacy/rpcservice.go b/internal/router/controllers/legacy/rpcservice.go deleted file mode 100644 index f3a44e8d..00000000 --- a/internal/router/controllers/legacy/rpcservice.go +++ /dev/null @@ -1,278 +0,0 @@ -package legacy - -import ( - "context" - "encoding/json" - "errors" - "fmt" - "time" - - "github.com/ethereum/go-ethereum/common" - "github.com/textileio/go-tableland/internal/formatter" - "github.com/textileio/go-tableland/internal/router/controllers" - "github.com/textileio/go-tableland/internal/router/middlewares" - "github.com/textileio/go-tableland/internal/tableland" - "github.com/textileio/go-tableland/pkg/tables" -) - -// RelayWriteQueryRequest is a user RelayWriteQuery request. -type RelayWriteQueryRequest struct { - Statement string `json:"statement"` -} - -// RelayWriteQueryResponse is a RelayWriteQuery response. -type RelayWriteQueryResponse struct { - Transaction struct { - Hash string `json:"hash"` - } `json:"tx"` -} - -// RunReadQueryRequest is a user RunReadQuery request. -type RunReadQueryRequest struct { - Statement string `json:"statement"` - Output *string `json:"output"` - Unwrap *bool `json:"unwrap"` - Extract *bool `json:"extract"` -} - -// FormatOpts extracts formatter options from a request. -func (rrqr *RunReadQueryRequest) FormatOpts() ([]formatter.FormatOption, error) { - var opts []formatter.FormatOption - if rrqr.Output != nil { - output, ok := formatter.OutputFromString(*rrqr.Output) - if !ok { - return nil, fmt.Errorf("%s is not a valid output", *rrqr.Output) - } - opts = append(opts, formatter.WithOutput(output)) - } - if rrqr.Extract != nil { - opts = append(opts, formatter.WithExtract(*rrqr.Extract)) - } - if rrqr.Unwrap != nil { - opts = append(opts, formatter.WithUnwrap(*rrqr.Unwrap)) - } - return opts, nil -} - -// RunReadQueryResponse is a RunReadQuery response. -type RunReadQueryResponse struct { - Result interface{} `json:"data"` -} - -// GetReceiptRequest is a GetTxnReceipt request. -type GetReceiptRequest struct { - TxnHash string `json:"txn_hash"` -} - -// TxnReceipt is a Tableland event processing receipt. -type TxnReceipt struct { - ChainID int64 `json:"chain_id"` - TxnHash string `json:"txn_hash"` - BlockNumber int64 `json:"block_number"` - - TableID *string `json:"table_id,omitempty"` - Error string `json:"error"` - ErrorEventIdx int `json:"error_event_idx"` -} - -// GetReceiptResponse is a GetTxnReceipt response. -type GetReceiptResponse struct { - Ok bool `json:"ok"` - Receipt *TxnReceipt `json:"receipt,omitempty"` -} - -// ValidateCreateTableRequest is a ValidateCreateTable request. -type ValidateCreateTableRequest struct { - CreateStatement string `json:"create_statement"` -} - -// ValidateCreateTableResponse is a ValidateCreateTable response. -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"` - TokenID string `json:"token_id"` -} - -// SetControllerResponse is a RunSQL response. -type SetControllerResponse struct { - Transaction struct { - Hash string `json:"hash"` - } `json:"tx"` -} - -// RPCService provides the JSON RPC API. -type RPCService struct { - tbl tableland.Tableland -} - -// NewRPCService creates a new RPCService. -func NewRPCService(tbl tableland.Tableland) *RPCService { - return &RPCService{ - tbl: tbl, - } -} - -// ValidateCreateTable allows to validate a CREATE TABLE statement and also return the structure hash of it. -// This RPC method is stateless. -func (rs *RPCService) ValidateCreateTable( - ctx context.Context, - req ValidateCreateTableRequest, -) (ValidateCreateTableResponse, error) { - ctxChainID := ctx.Value(middlewares.ContextKeyChainID) - chainID, ok := ctxChainID.(tableland.ChainID) - if !ok { - return ValidateCreateTableResponse{}, errors.New("no chain id found in context") - } - hash, err := rs.tbl.ValidateCreateTable(ctx, chainID, req.CreateStatement) - if err != nil { - return ValidateCreateTableResponse{}, fmt.Errorf("calling ValidateCreateTable %v", err) - } - return ValidateCreateTableResponse{StructureHash: hash}, nil -} - -// ValidateWriteQuery allows the user to validate a write query. -func (rs *RPCService) ValidateWriteQuery( - ctx context.Context, - req ValidateWriteQueryRequest, -) (ValidateWriteQueryResponse, error) { - ctxChainID := ctx.Value(middlewares.ContextKeyChainID) - chainID, ok := ctxChainID.(tableland.ChainID) - if !ok { - return ValidateWriteQueryResponse{}, errors.New("no chain id found in context") - } - tableID, err := rs.tbl.ValidateWriteQuery(ctx, chainID, req.Statement) - if err != nil { - return ValidateWriteQueryResponse{}, fmt.Errorf("calling ValidateWriteQuery: %v", err) - } - return ValidateWriteQueryResponse{TableID: tableID.String()}, nil -} - -// RelayWriteQuery allows the user to rely on the validator wrapping the query in a chain transaction. -func (rs *RPCService) RelayWriteQuery( - ctx context.Context, - req RelayWriteQueryRequest, -) (RelayWriteQueryResponse, error) { - ctxChainID := ctx.Value(middlewares.ContextKeyChainID) - chainID, ok := ctxChainID.(tableland.ChainID) - if !ok { - return RelayWriteQueryResponse{}, errors.New("no chain id found in context") - } - ctxCaller := ctx.Value(middlewares.ContextKeyAddress) - caller, ok := ctxCaller.(string) - if !ok || caller == "" { - return RelayWriteQueryResponse{}, errors.New("no controller address found in context") - } - txn, err := rs.tbl.RelayWriteQuery(ctx, chainID, common.HexToAddress(caller), req.Statement) - if err != nil { - return RelayWriteQueryResponse{}, fmt.Errorf("calling RelayWriteQuery: %v", err) - } - ret := RelayWriteQueryResponse{} - ret.Transaction.Hash = txn.Hash().Hex() - return ret, nil -} - -// RunReadQuery allows the user to run SQL. -func (rs *RPCService) RunReadQuery( - ctx context.Context, - req RunReadQueryRequest, -) (RunReadQueryResponse, error) { - start := time.Now() - res, err := rs.tbl.RunReadQuery(ctx, req.Statement) - if err != nil { - return RunReadQueryResponse{}, fmt.Errorf("calling RunReadQuery: %v", err) - } - took := time.Since(start) - - opts, err := req.FormatOpts() - if err != nil { - return RunReadQueryResponse{}, fmt.Errorf("getting format opts from request: %v", err) - } - - formatted, config, err := formatter.Format(res, opts...) - if err != nil { - return RunReadQueryResponse{}, fmt.Errorf("formatting result: %v", err) - } - - if config.Unwrap && len(res.Rows) > 1 { - return RunReadQueryResponse{}, errors.New("unwrapped results with more than one row aren't supported in JSON RPC API") - } - - controllers.CollectReadQueryMetric(ctx, req.Statement, config, took) - - return RunReadQueryResponse{Result: json.RawMessage(formatted)}, nil -} - -// GetReceipt returns the receipt of a processed event by txn hash. -func (rs *RPCService) GetReceipt( - ctx context.Context, - req GetReceiptRequest, -) (GetReceiptResponse, error) { - ctxChainID := ctx.Value(middlewares.ContextKeyChainID) - chainID, ok := ctxChainID.(tableland.ChainID) - if !ok { - return GetReceiptResponse{}, errors.New("no chain id found in context") - } - ok, receipt, err := rs.tbl.GetReceipt(ctx, chainID, req.TxnHash) - if err != nil { - return GetReceiptResponse{}, fmt.Errorf("calling GetReceipt: %v", err) - } - ret := GetReceiptResponse{Ok: ok} - if ok { - ret.Receipt = &TxnReceipt{ - ChainID: int64(receipt.ChainID), - TxnHash: receipt.TxnHash, - BlockNumber: receipt.BlockNumber, - TableID: receipt.TableID, - Error: receipt.Error, - ErrorEventIdx: receipt.ErrorEventIdx, - } - } - return ret, nil -} - -// SetController allows users to the controller for a token id. -func (rs *RPCService) SetController( - ctx context.Context, - req SetControllerRequest, -) (SetControllerResponse, error) { - ctxChainID := ctx.Value(middlewares.ContextKeyChainID) - chainID, ok := ctxChainID.(tableland.ChainID) - if !ok { - return SetControllerResponse{}, errors.New("no chain id found in context") - } - ctxCaller := ctx.Value(middlewares.ContextKeyAddress) - caller, ok := ctxCaller.(string) - if !ok || caller == "" { - return SetControllerResponse{}, errors.New("no caller address found in context") - } - tableID, err := tables.NewTableID(req.TokenID) - if err != nil { - return SetControllerResponse{}, fmt.Errorf("parsing token ID: %v", err) - } - txn, err := rs.tbl.SetController( - ctx, chainID, - common.HexToAddress(caller), - common.HexToAddress(req.Controller), - tableID, - ) - if err != nil { - return SetControllerResponse{}, fmt.Errorf("calling SetController: %v", err) - } - ret := SetControllerResponse{} - ret.Transaction.Hash = txn.Hash().Hex() - return ret, nil -} diff --git a/internal/router/controllers/legacy/rpcservice_test.go b/internal/router/controllers/legacy/rpcservice_test.go deleted file mode 100644 index 0981ec28..00000000 --- a/internal/router/controllers/legacy/rpcservice_test.go +++ /dev/null @@ -1,161 +0,0 @@ -package legacy - -import ( - "net/http" - "net/http/httptest" - "strings" - "testing" - - "github.com/ethereum/go-ethereum/rpc" - "github.com/gorilla/mux" - "github.com/stretchr/testify/mock" - "github.com/stretchr/testify/require" - "github.com/textileio/go-tableland/internal/tableland" - "github.com/textileio/go-tableland/mocks" -) - -func TestRunReadQueryManyRows(t *testing.T) { - tbl := mocks.NewTableland(t) - tbl.EXPECT().RunReadQuery(mock.Anything, "SELECT * FROM bruno_69_7").Return( - &tableland.TableData{ - Columns: []tableland.Column{{Name: "name"}, {Name: "age"}}, - Rows: [][]*tableland.ColumnValue{ - {tableland.OtherColValue("bob"), tableland.OtherColValue(40)}, - {tableland.OtherColValue("jane"), tableland.OtherColValue(30)}, - }, - }, - nil, - ) - - rpcService := NewRPCService(tbl) - - server := rpc.NewServer() - err := server.RegisterName("tableland", rpcService) - require.NoError(t, err) - - router := mux.NewRouter() - router.Handle("/rpc", server) - - // Table output - in := `{"jsonrpc":"2.0","method":"tableland_runReadQuery","id":1,"params":[{"statement":"SELECT * FROM bruno_69_7","output":"table"}]}` // nolint - req, err := http.NewRequest("POST", "/rpc", strings.NewReader(in)) - require.NoError(t, err) - req.Header.Set("Content-Type", "application/json") - - rr := httptest.NewRecorder() - router.ServeHTTP(rr, req) - require.Equal(t, http.StatusOK, rr.Code) - - expJSON := `{"jsonrpc":"2.0","id":1,"result":{"data":{"columns":[{"name":"name"},{"name":"age"}],"rows":[["bob",40],["jane",30]]}}}` // nolint - require.JSONEq(t, expJSON, rr.Body.String()) - - // Objects output - in = `{"jsonrpc":"2.0","method":"tableland_runReadQuery","id":1,"params":[{"statement":"SELECT * FROM bruno_69_7","output":"objects"}]}` // nolint - req, err = http.NewRequest("POST", "/rpc", strings.NewReader(in)) - require.NoError(t, err) - req.Header.Set("Content-Type", "application/json") - - rr = httptest.NewRecorder() - router.ServeHTTP(rr, req) - require.Equal(t, http.StatusOK, rr.Code) - - expJSON = `{"jsonrpc":"2.0","id":1,"result":{"data":[{"age":40,"name":"bob"},{"age":30,"name":"jane"}]}}` - require.JSONEq(t, expJSON, rr.Body.String()) - - // Extract error - in = `{"jsonrpc":"2.0","method":"tableland_runReadQuery","id":1,"params":[{"statement":"SELECT * FROM bruno_69_7","output":"objects","extract":true}]}` // nolint - req, err = http.NewRequest("POST", "/rpc", strings.NewReader(in)) - require.NoError(t, err) - req.Header.Set("Content-Type", "application/json") - - rr = httptest.NewRecorder() - router.ServeHTTP(rr, req) - require.Equal(t, http.StatusOK, rr.Code) - - expJSON = `{"jsonrpc":"2.0","id":1,"error":{"code":-32000,"message":"formatting result: extracting values: can only extract values for result sets with one column but this has 2"}}` // nolint - require.JSONEq(t, expJSON, rr.Body.String()) - - // Unwrap error - in = `{"jsonrpc":"2.0","method":"tableland_runReadQuery","id":1,"params":[{"statement":"SELECT * FROM bruno_69_7","output":"objects","unwrap":true}]}` // nolint - req, err = http.NewRequest("POST", "/rpc", strings.NewReader(in)) - require.NoError(t, err) - req.Header.Set("Content-Type", "application/json") - - rr = httptest.NewRecorder() - router.ServeHTTP(rr, req) - require.Equal(t, http.StatusOK, rr.Code) - - expJSON = `{"jsonrpc":"2.0","id":1,"error":{"code":-32000,"message":"unwrapped results with more than one row aren't supported in JSON RPC API"}}` // nolint - require.JSONEq(t, expJSON, rr.Body.String()) -} - -func TestRunReadQueryExtract(t *testing.T) { - tbl := mocks.NewTableland(t) - tbl.EXPECT().RunReadQuery(mock.Anything, "SELECT * FROM bruno_69_7").Return( - &tableland.TableData{ - Columns: []tableland.Column{{Name: "name"}}, - Rows: [][]*tableland.ColumnValue{ - {tableland.OtherColValue("bob")}, - {tableland.OtherColValue("jane")}, - }, - }, - nil, - ) - - rpcService := NewRPCService(tbl) - - server := rpc.NewServer() - err := server.RegisterName("tableland", rpcService) - require.NoError(t, err) - - router := mux.NewRouter() - router.Handle("/rpc", server) - - // Extract - in := `{"jsonrpc":"2.0","method":"tableland_runReadQuery","id":1,"params":[{"statement":"SELECT * FROM bruno_69_7","output":"objects","extract":true}]}` // nolint - req, err := http.NewRequest("POST", "/rpc", strings.NewReader(in)) - require.NoError(t, err) - req.Header.Set("Content-Type", "application/json") - - rr := httptest.NewRecorder() - router.ServeHTTP(rr, req) - require.Equal(t, http.StatusOK, rr.Code) - - expJSON := `{"jsonrpc":"2.0","id":1,"result":{"data":["bob","jane"]}}` // nolint - require.JSONEq(t, expJSON, rr.Body.String()) -} - -func TestRunReadQueryUnwrap(t *testing.T) { - tbl := mocks.NewTableland(t) - tbl.EXPECT().RunReadQuery(mock.Anything, "SELECT * FROM bruno_69_7").Return( - &tableland.TableData{ - Columns: []tableland.Column{{Name: "name"}, {Name: "age"}}, - Rows: [][]*tableland.ColumnValue{ - {tableland.OtherColValue("bob"), tableland.OtherColValue(40)}, - }, - }, - nil, - ) - - rpcService := NewRPCService(tbl) - - server := rpc.NewServer() - err := server.RegisterName("tableland", rpcService) - require.NoError(t, err) - - router := mux.NewRouter() - router.Handle("/rpc", server) - - // Unwrap - in := `{"jsonrpc":"2.0","method":"tableland_runReadQuery","id":1,"params":[{"statement":"SELECT * FROM bruno_69_7","output":"objects","unwrap":true}]}` // nolint - req, err := http.NewRequest("POST", "/rpc", strings.NewReader(in)) - require.NoError(t, err) - req.Header.Set("Content-Type", "application/json") - - rr := httptest.NewRecorder() - router.ServeHTTP(rr, req) - require.Equal(t, http.StatusOK, rr.Code) - - expJSON := `{"jsonrpc":"2.0","id":1,"result":{"data":{"age":40,"name":"bob"}}}` - require.JSONEq(t, expJSON, rr.Body.String()) -} diff --git a/internal/router/middlewares/authentication.go b/internal/router/middlewares/authentication.go deleted file mode 100644 index 5ffdd4c3..00000000 --- a/internal/router/middlewares/authentication.go +++ /dev/null @@ -1,109 +0,0 @@ -package middlewares - -import ( - "bytes" - "context" - "encoding/base64" - "encoding/json" - stderrors "errors" - "fmt" - "io" - "net/http" - "strings" - - "github.com/spruceid/siwe-go" - "github.com/textileio/go-tableland/internal/tableland" - "github.com/textileio/go-tableland/pkg/errors" -) - -var ( - errSIWEWrongDomain = stderrors.New("SIWE domain isn't Tableland") - siweDomain = "Tableland" - unauthenticatedRPCMethods = []string{ - "tableland_runReadQuery", - } -) - -// Authentication is middleware that provides JWT authentication. -func Authentication(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-type", "application/json") - - fullBody, err := io.ReadAll(r.Body) - if err != nil { - w.WriteHeader(http.StatusInternalServerError) - _ = json.NewEncoder(w).Encode(errors.ServiceError{Message: "reading request body"}) - return - } - var rpcMethod struct { - Method string `json:"method"` - } - if err := json.Unmarshal(fullBody, &rpcMethod); err != nil { - w.WriteHeader(http.StatusBadRequest) - _ = json.NewEncoder(w).Encode(errors.ServiceError{Message: "request body doesn't have a method field"}) - return - } - r.Body = io.NopCloser(bytes.NewReader(fullBody)) - if requiresAuthentication(rpcMethod.Method) { - authorization := r.Header.Get("Authorization") - if authorization == "" { - w.WriteHeader(http.StatusUnauthorized) - _ = json.NewEncoder(w).Encode(errors.ServiceError{Message: "no authorization header provided"}) - return - } - - parts := strings.Split(authorization, "Bearer ") - if len(parts) != 2 { - w.WriteHeader(http.StatusBadRequest) - _ = json.NewEncoder(w).Encode(errors.ServiceError{Message: "malformed authorization header provided"}) - return - } - - chainID, issuer, err := parseAuth(parts[1]) - if err != nil { - w.WriteHeader(http.StatusBadRequest) - _ = json.NewEncoder(w).Encode(errors.ServiceError{Message: fmt.Sprintf("parsing authorization: %v", err)}) - return - } - - r = r.WithContext(context.WithValue(r.Context(), ContextKeyAddress, strings.ToLower(issuer))) - r = r.WithContext(context.WithValue(r.Context(), ContextKeyChainID, chainID)) - } - - next.ServeHTTP(w, r) - }) -} - -func parseAuth(bearerToken string) (tableland.ChainID, string, error) { - var siweAuthMsg struct { - Message string `json:"message"` - Signature string `json:"signature"` - } - decodedSiwe, err := base64.StdEncoding.DecodeString(bearerToken) - if err != nil { - return 0, "", fmt.Errorf("decoding base64 siwe authorization: %s", err) - } - if err := json.Unmarshal(decodedSiwe, &siweAuthMsg); err != nil { - return 0, "", fmt.Errorf("unmarshalling siwe auth message: %s", err) - } - msg, err := siwe.ParseMessage(siweAuthMsg.Message) - if err != nil { - return 0, "", fmt.Errorf("parsing siwe: %s", err) - } - if msg.GetDomain() != siweDomain { - return 0, "", errSIWEWrongDomain - } - if _, err := msg.Verify(siweAuthMsg.Signature, &siweDomain, nil, nil); err != nil { - return 0, "", fmt.Errorf("checking siwe validity: %w", err) - } - return tableland.ChainID(msg.GetChainID()), msg.GetAddress().String(), nil -} - -func requiresAuthentication(rpcMethodName string) bool { - for _, methodName := range unauthenticatedRPCMethods { - if methodName == rpcMethodName { - return false - } - } - return true -} diff --git a/internal/router/middlewares/authentication_test.go b/internal/router/middlewares/authentication_test.go deleted file mode 100644 index 18725860..00000000 --- a/internal/router/middlewares/authentication_test.go +++ /dev/null @@ -1,86 +0,0 @@ -package middlewares - -import ( - "bytes" - "fmt" - "net/http" - "net/http/httptest" - "testing" - - "github.com/spruceid/siwe-go" - "github.com/stretchr/testify/require" - "github.com/textileio/go-tableland/internal/tableland" -) - -func TestSIWE(t *testing.T) { - t.Parallel() - - t.Run("valid", func(t *testing.T) { - t.Parallel() - - siweToken := "eyJtZXNzYWdlIjoiVGFibGVsYW5kIHdhbnRzIHlvdSB0byBzaWduIGluIHdpdGggeW91ciBFdGhlcmV1bSBhY2NvdW50OlxuMHhkNTM1YkFkNTA0Q0RkNzdlMkM1MWRFMjZGNDE2NjkzREY3YTAxYWM4XG5cblNJV0UgTm90ZXBhZCBFeGFtcGxlXG5cblVSSTogaHR0cDovL2xvY2FsaG9zdDo0MzYxXG5WZXJzaW9uOiAxXG5DaGFpbiBJRDogNFxuTm9uY2U6IEhHVkJWMFdvYlFHb1ZWUUlzXG5Jc3N1ZWQgQXQ6IDIwMjItMDQtMTlUMTg6NDA6MDQuMDQ2WlxuRXhwaXJhdGlvbiBUaW1lOiAyMDUyLTA0LTE4VDE1OjA4OjE0LjgwNVoiLCJzaWduYXR1cmUiOiIweDk3NTFjNDI2MjNiYTZhNjc1OTA5YjEzMzVjZGI2NDc0ODU4MmY5OTMyMTQxOTBmZmM2MGE0OGRhN2UzOTNhMjcwMDkzMDgzZmRkMzI4ZTNkZjA2ODc3ZTY3MjQ2MWJhMjcwYmI2YjFiYmQxMGJmNTBiMTliMTg5MmExNDhiNzkzMWMifQ==" //nolint - chainID, issuer, err := parseAuth(siweToken) - require.NoError(t, err) - require.Equal(t, "0xd535bAd504CDd77e2C51dE26F416693DF7a01ac8", issuer) - require.Equal(t, tableland.ChainID(4), chainID) - }) - t.Run("valid with signature with recovery bytes with leading 0", func(t *testing.T) { - t.Parallel() - - siweToken := "eyJtZXNzYWdlIjoiVGFibGVsYW5kIHdhbnRzIHlvdSB0byBzaWduIGluIHdpdGggeW91ciBFdGhlcmV1bSBhY2NvdW50OlxuMHgyQjgwRkEyNDMxN2IzYTgwMzlkYzY1ODVmMEVEYzkyNDdDNzgxZjJjXG5cblNJV0UgTm90ZXBhZCBFeGFtcGxlXG5cblVSSTogaHR0cDovL2xvY2FsaG9zdDo1MTczXG5WZXJzaW9uOiAxXG5DaGFpbiBJRDogODAwMDFcbk5vbmNlOiBnZU1kbmVLUnFyQ3BORWtCV1xuSXNzdWVkIEF0OiAyMDIyLTExLTE1VDE1OjE4OjI2LjIyNlpcbkV4cGlyYXRpb24gVGltZTogMjA1Mi0wNC0xOFQxNTowODoxNC44MDVaIiwic2lnbmF0dXJlIjoiMHg5NWFkYjJhZGU2OTE0OWJlNjE5OGViYjAwOTVmMzY1M2NjN2JhZjM3ODQ3MmZkMzQ3YzFjM2I3NWVjZjhkMGIwNjhkZWU1ZWE2ZGI5MWUwN2VjYjYyNDUzNjI0M2FlMmJiMmNkMmU4ZjJiMjEwNGY5OTBmOTVhZTAwZTNhMGM0MzAwIn0=" //nolint - _, _, err := parseAuth(siweToken) - require.NoError(t, err) - }) - t.Run("wrong domain", func(t *testing.T) { - t.Parallel() - - siweToken := "eyJtZXNzYWdlIjoibG9jYWxob3N0OjQzNjEgd2FudHMgeW91IHRvIHNpZ24gaW4gd2l0aCB5b3VyIEV0aGVyZXVtIGFjY291bnQ6XG4weGQ1MzViQWQ1MDRDRGQ3N2UyQzUxZEUyNkY0MTY2OTNERjdhMDFhYzhcblxuU0lXRSBOb3RlcGFkIEV4YW1wbGVcblxuVVJJOiBodHRwOi8vbG9jYWxob3N0OjQzNjFcblZlcnNpb246IDFcbkNoYWluIElEOiA0XG5Ob25jZTogdHhEY1pOOUJ1NkhHbXpDdmRcbklzc3VlZCBBdDogMjAyMi0wNC0xOFQyMjoyNDoxNS4xNDRaXG5FeHBpcmF0aW9uIFRpbWU6IDIwNTItMDQtMThUMTU6MDg6MTQuODA1WiIsInNpZ25hdHVyZSI6IjB4MThiOTlmOTY3YjUzNjgxZWZiNTU0Mjk4ZmNkYjJmYjE5N2JiYjEwODU0MmM4Mzc3ZDM0MGE5Zjk0M2RkZTY4NzcwNWUyOTQ3OGZjNTI1MzYyZmU5OGU1ZWI2NzAxOTU3OWM3MzQ4ZThkMTVmNzhjOTRiZDdiNWIzMjdlOTQ3MTAxYyJ9" //nolint - _, _, err := parseAuth(siweToken) - require.ErrorIs(t, err, errSIWEWrongDomain) - }) - t.Run("expired", func(t *testing.T) { - t.Parallel() - - siweToken := "eyJtZXNzYWdlIjoiVGFibGVsYW5kIHdhbnRzIHlvdSB0byBzaWduIGluIHdpdGggeW91ciBFdGhlcmV1bSBhY2NvdW50OlxuMHhkNTM1YkFkNTA0Q0RkNzdlMkM1MWRFMjZGNDE2NjkzREY3YTAxYWM4XG5cblNJV0UgTm90ZXBhZCBFeGFtcGxlXG5cblVSSTogaHR0cDovL2xvY2FsaG9zdDo0MzYxXG5WZXJzaW9uOiAxXG5DaGFpbiBJRDogNFxuTm9uY2U6IDBPT3dzOERXSlE5OEJ2ZGZWXG5Jc3N1ZWQgQXQ6IDIwMjItMDQtMTlUMTg6NDc6NTMuMTUxWlxuRXhwaXJhdGlvbiBUaW1lOiAyMDIyLTA0LTE4VDE1OjA4OjE0LjgwNVoiLCJzaWduYXR1cmUiOiIweGViMjM4MGNiMjA0NmQzNzZiZWI3NjQ0YjBkYTE4ZTA4NWM4NmVlNTZhZGY1MjUzYTcwZDZiZGY2N2Q0MGRjMDAwMzk0ZDk3ZWQzOTA2YmI5ZDNkMTM0MWFmODg3YWFhYzE5YWNmY2QwNmE3ZTI0ODBlMGI0MDJhMzRhOTdkZjEzMWMifQ==" //nolint - _, _, err := parseAuth(siweToken) - var expErr *siwe.ExpiredMessage - require.ErrorAs(t, err, &expErr) - }) -} - -func TestOptionality(t *testing.T) { - t.Parallel() - - tests := []struct { - rpcMethodName string - expStatusCode int - }{ - {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}, - } - - for _, tc := range tests { - tc := tc - t.Run(tc.rpcMethodName, func(t *testing.T) { - t.Parallel() - called := false - next := http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { - called = true - }) - - body := bytes.NewReader([]byte(fmt.Sprintf(`{"method": "%s"}`, tc.rpcMethodName))) - r := httptest.NewRequest("POST", "/rpc", body) - rw := httptest.NewRecorder() - - h := Authentication(next) - h.ServeHTTP(rw, r) - - require.Equal(t, tc.expStatusCode, rw.Code) - require.Equal(t, requiresAuthentication(tc.rpcMethodName), !called) - }) - } -} diff --git a/internal/router/middlewares/basicauth.go b/internal/router/middlewares/basicauth.go deleted file mode 100644 index ddd10c36..00000000 --- a/internal/router/middlewares/basicauth.go +++ /dev/null @@ -1,33 +0,0 @@ -package middlewares - -import ( - "crypto/sha256" - "crypto/subtle" - "net/http" -) - -// BasicAuth is middleware that checks the expected username and password match the http basic auth values. -func BasicAuth(expectedUsername, expectedPassword string) func(http.Handler) http.Handler { - return func(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - username, password, ok := r.BasicAuth() - - if ok { - usernameHash := sha256.Sum256([]byte(username)) - passwordHash := sha256.Sum256([]byte(password)) - expectedUsernameHash := sha256.Sum256([]byte(expectedUsername)) - expectedPasswordHash := sha256.Sum256([]byte(expectedPassword)) - - usernameMatch := (subtle.ConstantTimeCompare(usernameHash[:], expectedUsernameHash[:]) == 1) - passwordMatch := (subtle.ConstantTimeCompare(passwordHash[:], expectedPasswordHash[:]) == 1) - - if usernameMatch && passwordMatch { - next.ServeHTTP(w, r) - return - } - } - - http.Error(w, "Unauthorized", http.StatusUnauthorized) - }) - } -} diff --git a/internal/router/middlewares/contextkeys.go b/internal/router/middlewares/contextkeys.go index 4d68f9ba..8d7a92c7 100644 --- a/internal/router/middlewares/contextkeys.go +++ b/internal/router/middlewares/contextkeys.go @@ -4,10 +4,10 @@ package middlewares type ContextKey int const ( - // ContextKeyAddress is used to store the address of the client for the incoming request. - ContextKeyAddress ContextKey = iota - // ContextKeyChainID is used to store the chain id of the client for the incoming request. + // ContextKeyChainID is used to store the chain id of the client for the incoming request, + // this is found in the request path. ContextKeyChainID ContextKey = iota - // ContextIPAddress is used to store the ip address of the client for the incoming request. + // ContextIPAddress is used to store the ip address of the client for the incoming request, + // this is found in either the request IP or the x-forwarded header. ContextIPAddress ContextKey = iota ) diff --git a/internal/router/middlewares/ratelim.go b/internal/router/middlewares/ratelim.go index d8475c83..dd11f083 100644 --- a/internal/router/middlewares/ratelim.go +++ b/internal/router/middlewares/ratelim.go @@ -1,30 +1,20 @@ package middlewares import ( - "bytes" - "encoding/json" "fmt" - "io" "net" "net/http" "strings" "time" "github.com/gorilla/mux" - "github.com/rs/zerolog/log" "github.com/sethvargo/go-limiter/httplimit" "github.com/sethvargo/go-limiter/memorystore" - "github.com/textileio/go-tableland/pkg/errors" ) -// RateLimiterConfig specifies a default rate limiting configuration, and optional custom rate limiting -// rules for a JSON RPC sub-route with path JSONRPCRoute. i.e: particular JSON RPC methods can have different -// rate limiting. +// RateLimiterConfig specifies a default rate limiting configuration. type RateLimiterConfig struct { Default RateLimiterRouteConfig - - JSONRPCRoute string - JSONRPCMethodLimits map[string]RateLimiterRouteConfig } // RateLimiterRouteConfig specifies the maximum request per interval, and @@ -36,18 +26,10 @@ type RateLimiterRouteConfig struct { // RateLimitController creates a new middleware to rate limit requests. // It applies a priority based rate limiting key for the rate limiting: -// 1. A "chain-address" was detected (i.e: via a signed SIWE). -// 2. If 1. isn't present, it will use an existing X-Forwarded-For IP included by a load-balancer in the infrastructure. -// 3. If 2. isn't present, it will use the connection remote address. +// 1. If found, use an existing X-Forwarded-For IP included by a load-balancer in the infrastructure. +// 2. If 1. isn't present, it will use the connection remote address. func RateLimitController(cfg RateLimiterConfig) (mux.MiddlewareFunc, error) { keyFunc := func(r *http.Request) (string, error) { - // Use a chain address if present. - address := r.Context().Value(ContextKeyAddress) - ctrlAddress, ok := address.(string) - if ok && ctrlAddress != "" { - return ctrlAddress, nil - } - ip, err := extractClientIP(r) if err != nil { return "", fmt.Errorf("extract client ip: %s", err) @@ -59,50 +41,9 @@ func RateLimitController(cfg RateLimiterConfig) (mux.MiddlewareFunc, error) { if err != nil { return nil, fmt.Errorf("creating default rate limiter: %s", err) } - customRLs := make(map[string]*httplimit.Middleware, len(cfg.JSONRPCMethodLimits)) - for route, routeCfg := range cfg.JSONRPCMethodLimits { - customRLs[route], err = createRateLimiter(routeCfg, keyFunc) - if err != nil { - return nil, fmt.Errorf("creating custom rate limiter for route %s: %s", route, err) - } - } return func(next http.Handler) http.Handler { - defaultRLHandler := defaultRL.Handle(next) - customRLHandlers := make(map[string]http.Handler, len(cfg.JSONRPCMethodLimits)) - for jsonMethod := range customRLs { - customRLHandlers[jsonMethod] = customRLs[jsonMethod].Handle(next) - } - - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - // By default, set `m` with the default handler. - m := defaultRLHandler - - // Now inspect if we should use some custom handler, if that's the case set `m` to that - // value. If none is found, we'll use `m` as is (default). - if r.URL.Path == cfg.JSONRPCRoute { - fullBody, err := io.ReadAll(r.Body) - if err != nil { - w.WriteHeader(http.StatusInternalServerError) - _ = json.NewEncoder(w).Encode(errors.ServiceError{Message: "reading request body"}) - return - } - log.Warn().Str("body", string(fullBody)).Msg("call to legacy RPC") - var rpcMethod struct { - Method string `json:"method"` - } - if err := json.Unmarshal(fullBody, &rpcMethod); err != nil { - w.WriteHeader(http.StatusBadRequest) - _ = json.NewEncoder(w).Encode(errors.ServiceError{Message: "request body doesn't have a method field"}) - return - } - r.Body = io.NopCloser(bytes.NewReader(fullBody)) - if customLimiter, ok := customRLHandlers[rpcMethod.Method]; ok { - m = customLimiter - } - } - m.ServeHTTP(w, r) - }) + return http.HandlerFunc(defaultRL.Handle(next).ServeHTTP) }, nil } diff --git a/internal/router/middlewares/ratelim_test.go b/internal/router/middlewares/ratelim_test.go index 8b99875a..c3da0c52 100644 --- a/internal/router/middlewares/ratelim_test.go +++ b/internal/router/middlewares/ratelim_test.go @@ -1,9 +1,7 @@ package middlewares import ( - "bytes" "context" - "fmt" "net/http" "net/http/httptest" "strconv" @@ -14,126 +12,6 @@ import ( "github.com/stretchr/testify/require" ) -func TestLimit1Addr(t *testing.T) { - t.Parallel() - - type testCase struct { - name string - callRPS int - limitRPS int - } - - tests := []testCase{ - {name: "success", callRPS: 100, limitRPS: 500}, - {name: "block-me", callRPS: 1000, limitRPS: 500}, - } - - for _, tc := range tests { - t.Run(tc.name, func(tc testCase) func(t *testing.T) { - return func(t *testing.T) { - t.Parallel() - - cfg := RateLimiterConfig{ - Default: RateLimiterRouteConfig{ - MaxRPI: uint64(tc.limitRPS), - Interval: time.Second, - }, - JSONRPCRoute: "/rpc", - } - rlcm, err := RateLimitController(cfg) - require.NoError(t, err) - rlc := rlcm(dummyHandler{}) - - ctx := context.WithValue(context.Background(), ContextKeyAddress, "0xdeadbeef") - r, err := http.NewRequestWithContext(ctx, "", "", nil) - require.NoError(t, err) - - res := httptest.NewRecorder() - - // Verify that after some seconds making requests with the configured - // callRPS with the limitRPS, we are getting the expected output: - // - If callRPS < limitRPS, we never get a 429. - // - If callRPS > limitRPS, we eventually should see a 429. - assertFunc := require.Eventually - if tc.callRPS < tc.limitRPS { - assertFunc = require.Never - } - assertFunc(t, func() bool { - rlc.ServeHTTP(res, r) - return res.Code == 429 - }, time.Second*5, time.Second/time.Duration(tc.callRPS)) - } - }(tc)) - } -} - -func TestCustomRPCLimits(t *testing.T) { - t.Parallel() - - cfg := RateLimiterConfig{ - Default: RateLimiterRouteConfig{ - MaxRPI: uint64(10000), - Interval: time.Second, - }, - JSONRPCRoute: "/rpc", - JSONRPCMethodLimits: map[string]RateLimiterRouteConfig{ - "tableland_runSQLQuery": { - MaxRPI: 100, - Interval: time.Second, - }, - "tableland_relayWriteQuery": { - MaxRPI: 10, - Interval: time.Second, - }, - }, - } - - type testCase struct { - name string - rpcMethod string - callRPS int - success bool - } - - tests := []testCase{ - {name: "success", rpcMethod: "tableland_runSQLQuery", callRPS: 90, success: true}, - {name: "success", rpcMethod: "tableland_relayWriteQuery", callRPS: 8, success: true}, - - {name: "blocked", rpcMethod: "tableland_runSQLQuery", callRPS: 110, success: false}, - {name: "blocked", rpcMethod: "tableland_relayWriteQuery", callRPS: 11, success: false}, - } - - for _, tc := range tests { - t.Run(fmt.Sprintf("%s-%s", tc.rpcMethod, tc.name), func(tc testCase) func(t *testing.T) { - return func(t *testing.T) { - t.Parallel() - - rlcm, err := RateLimitController(cfg) - require.NoError(t, err) - rlc := rlcm(dummyHandler{}) - - ctx := context.WithValue(context.Background(), ContextKeyAddress, "0xdeadbeef") - - reqBody := fmt.Sprintf(`{"method": "%s"}`, tc.rpcMethod) - body := bytes.NewReader([]byte(reqBody)) - r, err := http.NewRequestWithContext(ctx, "POST", cfg.JSONRPCRoute, body) - require.NoError(t, err) - - res := httptest.NewRecorder() - - assertFunc := require.Eventually - if tc.success { - assertFunc = require.Never - } - assertFunc(t, func() bool { - rlc.ServeHTTP(res, r) - return res.Code == 429 - }, time.Second*5, time.Second/time.Duration(tc.callRPS)) - } - }(tc)) - } -} - func TestLimit1IP(t *testing.T) { t.Parallel() @@ -162,7 +40,6 @@ func TestLimit1IP(t *testing.T) { MaxRPI: uint64(tc.limitRPS), Interval: time.Second, }, - JSONRPCRoute: "/rpc", } rlcm, err := RateLimitController(cfg) require.NoError(t, err) @@ -197,36 +74,6 @@ func TestLimit1IP(t *testing.T) { } } -func TestRateLim10Addresses(t *testing.T) { - t.Parallel() - - // Only allow 150 req per second *per address*. - cfg := RateLimiterConfig{ - Default: RateLimiterRouteConfig{ - MaxRPI: 150, - Interval: time.Second, - }, - JSONRPCRoute: "/rpc", - } - rlcm, err := RateLimitController(cfg) - require.NoError(t, err) - rlc := rlcm(dummyHandler{}) - - // Do 1000 requests as fast as we can with *different addresses*, and see that - // we never get a 429 status response. The request per second being done is - // clearly more than 10 per second, but from different addresses which should be fine. - for i := 0; i < 1000; i++ { - ctx := context.WithValue(context.Background(), ContextKeyAddress, strconv.Itoa(i%10)) - r, err := http.NewRequestWithContext(ctx, "", "", nil) - require.NoError(t, err) - - res := httptest.NewRecorder() - - rlc.ServeHTTP(res, r) - require.Equal(t, 200, res.Code) - } -} - func TestRateLim10IPs(t *testing.T) { t.Parallel() @@ -236,7 +83,6 @@ func TestRateLim10IPs(t *testing.T) { MaxRPI: 100, Interval: time.Second, }, - JSONRPCRoute: "/rpc", } rlcm, err := RateLimitController(cfg) require.NoError(t, err) @@ -246,7 +92,7 @@ func TestRateLim10IPs(t *testing.T) { // we never get a 429 status response. The request per second being done is // clearly more than 10 per second, but from different addresses which should be fine. for i := 0; i < 1000; i++ { - ctx := context.WithValue(context.Background(), ContextKeyAddress, strconv.Itoa(i)) + ctx := context.WithValue(context.Background(), ContextIPAddress, strconv.Itoa(i)) r, err := http.NewRequestWithContext(ctx, "", "", nil) require.NoError(t, err) r.Header.Set("X-Forwarded-For", uuid.NewString()) diff --git a/internal/router/router.go b/internal/router/router.go index d139c77e..25e6e190 100644 --- a/internal/router/router.go +++ b/internal/router/router.go @@ -5,30 +5,21 @@ import ( "net/http" "time" - "github.com/ethereum/go-ethereum/rpc" "github.com/gorilla/mux" + "github.com/textileio/go-tableland/internal/gateway" "github.com/textileio/go-tableland/internal/router/controllers" "github.com/textileio/go-tableland/internal/router/controllers/apiv1" - "github.com/textileio/go-tableland/internal/router/controllers/legacy" "github.com/textileio/go-tableland/internal/router/middlewares" - "github.com/textileio/go-tableland/internal/system" "github.com/textileio/go-tableland/internal/tableland" ) // ConfiguredRouter returns a fully configured Router that can be used as an http handler. func ConfiguredRouter( - tableland tableland.Tableland, - systemService system.SystemService, + gateway gateway.Gateway, maxRPI uint64, rateLimInterval time.Duration, supportedChainIDs []tableland.ChainID, ) (*Router, error) { - rpcService := legacy.NewRPCService(tableland) - server := rpc.NewServer() - if err := server.RegisterName("tableland", rpcService); err != nil { - return nil, fmt.Errorf("failed to register a json-rpc service: %s", err) - } - // General router configuration. router := newRouter() router.use(middlewares.CORS, middlewares.TraceID) @@ -38,18 +29,13 @@ func ConfiguredRouter( MaxRPI: maxRPI, Interval: rateLimInterval, }, - JSONRPCRoute: "/rpc", // TODO(json-rpc): remove this feature in the rate-limiter when we drop support. } rateLim, err := middlewares.RateLimitController(cfg) if err != nil { return nil, fmt.Errorf("creating rate limit controller middleware: %s", err) } - ctrl := controllers.NewController(tableland, systemService) - - // TODO(json-rpc): remove this when dropping support. - // APIs Legacy (REST + JSON-RPC) - configureLegacyRoutes(router, server, supportedChainIDs, rateLim, ctrl) + ctrl := controllers.NewController(gateway) // APIs V1 if err := configureAPIV1Routes(router, supportedChainIDs, rateLim, ctrl); err != nil { @@ -59,32 +45,6 @@ func ConfiguredRouter( return router, nil } -func configureLegacyRoutes( - router *Router, - server *rpc.Server, - supportedChainIDs []tableland.ChainID, - rateLim mux.MiddlewareFunc, - ctrl *controllers.Controller, -) { - router.post("/rpc", func(rw http.ResponseWriter, r *http.Request) { - server.ServeHTTP(rw, r) - }, middlewares.WithLogging, middlewares.OtelHTTP("rpc"), middlewares.Authentication, rateLim) - - // Gateway configuration. - router.get("/chain/{chainId}/tables/{tableId}", ctrl.GetTable, middlewares.WithLogging, middlewares.OtelHTTP("GetTable"), middlewares.RESTChainID(supportedChainIDs), rateLim) // nolint - router.get("/chain/{chainId}/tables/{id}/{key}/{value}", ctrl.GetTableRow, middlewares.WithLogging, middlewares.OtelHTTP("GetTableRow"), middlewares.RESTChainID(supportedChainIDs), rateLim) // nolint - router.get("/chain/{chainId}/tables/controller/{address}", ctrl.GetTablesByController, middlewares.WithLogging, middlewares.OtelHTTP("GetTablesByController"), middlewares.RESTChainID(supportedChainIDs), rateLim) // nolint - router.get("/chain/{chainId}/tables/structure/{hash}", ctrl.GetTablesByStructureHash, middlewares.WithLogging, middlewares.OtelHTTP("GetTablesByStructureHash"), middlewares.RESTChainID(supportedChainIDs), rateLim) // nolint - router.get("/schema/{table_name}", ctrl.GetSchemaByTableName, middlewares.WithLogging, middlewares.OtelHTTP("GetSchemaFromTableName"), rateLim) // nolint - - router.get("/query", ctrl.GetTableQuery, middlewares.WithLogging, middlewares.OtelHTTP("GetTableQuery"), rateLim) // nolint - router.get("/version", ctrl.Version, middlewares.WithLogging, middlewares.OtelHTTP("Version"), rateLim) // nolint - - // Health endpoint configuration. - router.get("/healthz", controllers.HealthHandler) - router.get("/health", controllers.HealthHandler) -} - func configureAPIV1Routes( router *Router, supportedChainIDs []tableland.ChainID, @@ -170,13 +130,6 @@ func (r *Router) get(uri string, f http.HandlerFunc, mid ...mux.MiddlewareFunc) sub.Use(mid...) } -// post creates a subroute on the specified URI that only accepts POST. You can provide specific middlewares. -func (r *Router) post(uri string, f func(http.ResponseWriter, *http.Request), mid ...mux.MiddlewareFunc) { - sub := r.r.Path(uri).Subrouter() - sub.HandleFunc("", f).Methods(http.MethodPost) - sub.Use(mid...) -} - // use adds middlewares to all routes. Should be used when a middleware should be execute all all routes (e.g. CORS). func (r *Router) use(mid ...mux.MiddlewareFunc) { r.r.Use(mid...) diff --git a/internal/system/impl/mock.go b/internal/system/impl/mock.go deleted file mode 100644 index 17edb5c6..00000000 --- a/internal/system/impl/mock.go +++ /dev/null @@ -1,156 +0,0 @@ -package impl - -import ( - "context" - "errors" - "fmt" - "math/big" - - "github.com/ethereum/go-ethereum/common" - "github.com/textileio/go-tableland/internal/system" - "github.com/textileio/go-tableland/internal/tableland" - "github.com/textileio/go-tableland/pkg/sqlstore" - "github.com/textileio/go-tableland/pkg/tables" -) - -// SystemMockService is a dummy implementation that returns a fixed value. -type SystemMockService struct{} - -// NewSystemMockService creates a new SystemMockService. -func NewSystemMockService() system.SystemService { - return &SystemMockService{} -} - -// GetReceiptByTransactionHash implements system.SystemService. -func (*SystemMockService) GetReceiptByTransactionHash(context.Context, common.Hash) (sqlstore.Receipt, bool, error) { - tableID, _ := tables.NewTableID("10") - return sqlstore.Receipt{ - ChainID: 1337, - BlockNumber: 10, - IndexInBlock: 1, - TxnHash: "0xDEADBEEF", - TableID: &tableID, - Error: nil, - ErrorEventIdx: nil, - }, true, nil -} - -// GetTableMetadata returns a fixed value for testing and demo purposes. -func (*SystemMockService) GetTableMetadata(_ context.Context, id tables.TableID) (sqlstore.TableMetadata, error) { - return sqlstore.TableMetadata{ - Name: "name-1", - ExternalURL: fmt.Sprintf("https://tableland.network/tables/%s", id), - Image: "https://bafkreifhuhrjhzbj4onqgbrmhpysk2mop2jimvdvfut6taiyzt2yqzt43a.ipfs.dweb.link", //nolint - Attributes: []sqlstore.TableMetadataAttribute{ - { - DisplayType: "date", - TraitType: "created", - Value: 1546360800, - }, - }, - Schema: sqlstore.TableSchema{ - Columns: []sqlstore.ColumnSchema{{Name: "foo", Type: "text"}}, - }, - }, nil -} - -// GetTablesByController returns table's fetched from SQLStore by controller address. -func (s *SystemMockService) GetTablesByController(_ context.Context, _ string) ([]sqlstore.Table, error) { - return []sqlstore.Table{ - { - ID: tables.TableID(*big.NewInt(0)), - ChainID: tableland.ChainID(1337), - Controller: "0x2a891118Cf3a8FdeBb00109ea3ed4E33B82D960f", - Prefix: "test", - // echo -n a:INT| shasum -a 256 - Structure: "0605f6c6705c7c1257edb2d61d94a03ad15f1d253a5a75525c6da8cda34a99ee", - }, - { - ID: tables.TableID(*big.NewInt(1)), - ChainID: tableland.ChainID(1337), - Controller: "0x2a891118Cf3a8FdeBb00109ea3ed4E33B82D960f", - Prefix: "test2", - // echo -n a:INT| shasum -a 256 - Structure: "0605f6c6705c7c1257edb2d61d94a03ad15f1d253a5a75525c6da8cda34a99ee", - }, - }, nil -} - -// GetTablesByStructure returns all tables that share the same structure. -func (s *SystemMockService) GetTablesByStructure(_ context.Context, _ string) ([]sqlstore.Table, error) { - return []sqlstore.Table{ - { - ID: tables.TableID(*big.NewInt(0)), - ChainID: tableland.ChainID(1337), - Controller: "0x2a891118Cf3a8FdeBb00109ea3ed4E33B82D960f", - Prefix: "test", - // echo -n a:INT| shasum -a 256 - Structure: "0605f6c6705c7c1257edb2d61d94a03ad15f1d253a5a75525c6da8cda34a99ee", - }, - { - ID: tables.TableID(*big.NewInt(1)), - ChainID: tableland.ChainID(1337), - Controller: "0x2a891118Cf3a8FdeBb00109ea3ed4E33B82D960f", - Prefix: "test2", - // echo -n a:INT| shasum -a 256 - Structure: "0605f6c6705c7c1257edb2d61d94a03ad15f1d253a5a75525c6da8cda34a99ee", - }, - }, nil -} - -// GetSchemaByTableName returns the schema of a table by its name. -func (s *SystemMockService) GetSchemaByTableName(_ context.Context, _ string) (sqlstore.TableSchema, error) { - return sqlstore.TableSchema{ - Columns: []sqlstore.ColumnSchema{ - { - Name: "a", - Type: "int", - Constraints: []string{"PRIMARY KEY"}, - }, - { - Name: "b", - Type: "text", - Constraints: []string{"DEFAULT ''"}, - }, - }, - TableConstraints: []string{ - "CHECK check (a > 0)", - }, - }, nil -} - -// SystemMockErrService is a dummy implementation that returns a fixed value. -type SystemMockErrService struct{} - -// NewSystemMockErrService creates a new SystemMockErrService. -func NewSystemMockErrService() system.SystemService { - return &SystemMockErrService{} -} - -// GetReceiptByTransactionHash implements system.SystemService. -func (*SystemMockErrService) GetReceiptByTransactionHash(context.Context, common.Hash) (sqlstore.Receipt, bool, error) { - return sqlstore.Receipt{}, false, nil -} - -// GetTableMetadata returns a fixed value for testing and demo purposes. -func (*SystemMockErrService) GetTableMetadata( - _ context.Context, - _ tables.TableID, -) (sqlstore.TableMetadata, error) { - return sqlstore.TableMetadata{}, errors.New("table not found") -} - -// GetTablesByController returns table's fetched from SQLStore by controller address. -func (s *SystemMockErrService) GetTablesByController(_ context.Context, _ string) ([]sqlstore.Table, error) { - return []sqlstore.Table{}, errors.New("no table found") -} - -// GetTablesByStructure returns all tables that share the same structure. -func (s *SystemMockErrService) GetTablesByStructure(_ context.Context, _ string) ([]sqlstore.Table, error) { - return []sqlstore.Table{}, errors.New("no table found") -} - -// GetSchemaByTableName returns the schema of a table by its name. -func (s *SystemMockErrService) GetSchemaByTableName(_ context.Context, _ string) (sqlstore.TableSchema, error) { - return sqlstore.TableSchema{}, errors.New("no table found") -} diff --git a/internal/system/impl/sqlstore.go b/internal/system/impl/sqlstore.go deleted file mode 100644 index 723ccf46..00000000 --- a/internal/system/impl/sqlstore.go +++ /dev/null @@ -1,250 +0,0 @@ -package impl - -import ( - "context" - "database/sql" - "encoding/base64" - "errors" - "fmt" - "net/url" - "strings" - - "github.com/ethereum/go-ethereum/common" - logger "github.com/rs/zerolog/log" - "github.com/textileio/go-tableland/internal/router/middlewares" - "github.com/textileio/go-tableland/internal/system" - "github.com/textileio/go-tableland/internal/tableland" - "github.com/textileio/go-tableland/pkg/sqlstore" - "github.com/textileio/go-tableland/pkg/tables" -) - -var log = logger.With().Str("component", "systemsqlstore").Logger() - -const ( - // SystemTablesPrefix is the prefix used in table names that - // aren't owned by users, but the system. - SystemTablesPrefix = "system_" - - // RegistryTableName is a special system table (not owned by user) - // that has information about all tables owned by users. - RegistryTableName = "registry" - - // DefaultMetadataImage is the default image for table's metadata. - DefaultMetadataImage = "https://bafkreifhuhrjhzbj4onqgbrmhpysk2mop2jimvdvfut6taiyzt2yqzt43a.ipfs.dweb.link" - - // DefaultAnimationURL is an empty string. It means that the attribute will not appear in the JSON metadata. - DefaultAnimationURL = "" -) - -// SystemSQLStoreService implements the SystemService interface using SQLStore. -type SystemSQLStoreService struct { - extURLPrefix string - metadataRendererURI string - animationRendererURI string - stores map[tableland.ChainID]sqlstore.SystemStore -} - -// NewSystemSQLStoreService creates a new SystemSQLStoreService. -func NewSystemSQLStoreService( - stores map[tableland.ChainID]sqlstore.SystemStore, - extURLPrefix string, - metadataRendererURI string, - animationRendererURI string, -) (system.SystemService, error) { - if _, err := url.ParseRequestURI(extURLPrefix); err != nil { - return nil, fmt.Errorf("invalid external url prefix: %s", err) - } - - metadataRendererURI = strings.TrimRight(metadataRendererURI, "/") - if metadataRendererURI != "" { - if _, err := url.ParseRequestURI(metadataRendererURI); err != nil { - return nil, fmt.Errorf("metadata renderer uri could not be parsed: %s", err) - } - } - - animationRendererURI = strings.TrimRight(animationRendererURI, "/") - if animationRendererURI != "" { - if _, err := url.ParseRequestURI(animationRendererURI); err != nil { - return nil, fmt.Errorf("animation renderer uri could not be parsed: %s", err) - } - } - - return &SystemSQLStoreService{ - extURLPrefix: extURLPrefix, - metadataRendererURI: metadataRendererURI, - animationRendererURI: animationRendererURI, - stores: stores, - }, nil -} - -// GetTableMetadata returns table's metadata fetched from SQLStore. -func (s *SystemSQLStoreService) GetTableMetadata( - ctx context.Context, - id tables.TableID, -) (sqlstore.TableMetadata, error) { - ctxChainID := ctx.Value(middlewares.ContextKeyChainID) - chainID, ok := ctxChainID.(tableland.ChainID) - if !ok { - return sqlstore.TableMetadata{}, errors.New("no chain id found in context") - } - store, ok := s.stores[chainID] - if !ok { - return sqlstore.TableMetadata{ - ExternalURL: fmt.Sprintf("%s/chain/%d/tables/%s", s.extURLPrefix, chainID, id), - Image: s.emptyMetadataImage(), - Message: "Chain isn't supported", - }, nil - } - table, err := store.GetTable(ctx, id) - if err != nil { - if !errors.Is(err, sql.ErrNoRows) { - log.Error().Err(err).Msg("error fetching the table") - return sqlstore.TableMetadata{ - ExternalURL: fmt.Sprintf("%s/chain/%d/tables/%s", s.extURLPrefix, chainID, id), - Image: s.emptyMetadataImage(), - Message: "Failed to fetch the table", - }, nil - } - - return sqlstore.TableMetadata{ - ExternalURL: fmt.Sprintf("%s/chain/%d/tables/%s", s.extURLPrefix, chainID, id), - Image: s.emptyMetadataImage(), - Message: "Table not found", - }, system.ErrTableNotFound - } - tableName := fmt.Sprintf("%s_%d_%s", table.Prefix, table.ChainID, table.ID) - schema, err := store.GetSchemaByTableName(ctx, tableName) - if err != nil { - return sqlstore.TableMetadata{}, fmt.Errorf("get table schema information: %s", err) - } - - return sqlstore.TableMetadata{ - Name: tableName, - ExternalURL: fmt.Sprintf("%s/chain/%d/tables/%s", s.extURLPrefix, table.ChainID, table.ID), - Image: s.getMetadataImage(table.ChainID, table.ID), - AnimationURL: s.getAnimationURL(table.ChainID, table.ID), - Attributes: []sqlstore.TableMetadataAttribute{ - { - DisplayType: "date", - TraitType: "created", - Value: table.CreatedAt.Unix(), - }, - }, - Schema: schema, - }, nil -} - -// GetReceiptByTransactionHash returns a receipt by transaction hash. -func (s *SystemSQLStoreService) GetReceiptByTransactionHash( - ctx context.Context, - txnHash common.Hash, -) (sqlstore.Receipt, bool, error) { - ctxChainID := ctx.Value(middlewares.ContextKeyChainID) - chainID, ok := ctxChainID.(tableland.ChainID) - if !ok { - return sqlstore.Receipt{}, false, errors.New("no chain id found in context") - } - store, ok := s.stores[chainID] - if !ok { - return sqlstore.Receipt{}, false, fmt.Errorf("chain id %d isn't supported in the validator", chainID) - } - receipt, exists, err := store.GetReceipt(ctx, txnHash.Hex()) - if err != nil { - return sqlstore.Receipt{}, false, fmt.Errorf("transaction receipt lookup: %s", err) - } - if !exists { - return sqlstore.Receipt{}, false, nil - } - return sqlstore.Receipt{ - ChainID: chainID, - BlockNumber: receipt.BlockNumber, - IndexInBlock: receipt.IndexInBlock, - TxnHash: receipt.TxnHash, - TableID: receipt.TableID, - Error: receipt.Error, - ErrorEventIdx: receipt.ErrorEventIdx, - }, true, nil -} - -// GetTablesByController returns table's fetched from SQLStore by controller address. -func (s *SystemSQLStoreService) GetTablesByController( - ctx context.Context, - controller string, -) ([]sqlstore.Table, error) { - ctxChainID := ctx.Value(middlewares.ContextKeyChainID) - chainID, ok := ctxChainID.(tableland.ChainID) - if !ok { - return nil, errors.New("no chain id found in context") - } - store, ok := s.stores[chainID] - if !ok { - return nil, fmt.Errorf("chain id %d isn't supported in the validator", chainID) - } - tables, err := store.GetTablesByController(ctx, controller) - if err != nil { - return nil, fmt.Errorf("error fetching the tables: %s", err) - } - return tables, nil -} - -// GetTablesByStructure returns all tables that share the same structure. -func (s *SystemSQLStoreService) GetTablesByStructure(ctx context.Context, structure string) ([]sqlstore.Table, error) { - ctxChainID := ctx.Value(middlewares.ContextKeyChainID) - chainID, ok := ctxChainID.(tableland.ChainID) - if !ok { - return nil, errors.New("no chain id found in context") - } - store, ok := s.stores[chainID] - if !ok { - return nil, fmt.Errorf("chain id %d isn't supported in the validator", chainID) - } - tables, err := store.GetTablesByStructure(ctx, structure) - if err != nil { - return nil, fmt.Errorf("get tables by structure: %s", err) - } - return tables, nil -} - -// GetSchemaByTableName returns the schema of a table by its name. -func (s *SystemSQLStoreService) GetSchemaByTableName( - ctx context.Context, - tableName string, -) (sqlstore.TableSchema, error) { - table, err := tableland.NewTableFromName(tableName) - if err != nil { - return sqlstore.TableSchema{}, fmt.Errorf("new table from name: %s", err) - } - - store, ok := s.stores[table.ChainID()] - if !ok { - return sqlstore.TableSchema{}, fmt.Errorf("chain id %d isn't supported in the validator", table.ChainID()) - } - - schema, err := store.GetSchemaByTableName(ctx, tableName) - if err != nil { - return sqlstore.TableSchema{}, fmt.Errorf("get schema by table name: %s", err) - } - return schema, nil -} - -func (s *SystemSQLStoreService) getMetadataImage(chainID tableland.ChainID, tableID tables.TableID) string { - if s.metadataRendererURI == "" { - return DefaultMetadataImage - } - - return fmt.Sprintf("%s/%d/%s", s.metadataRendererURI, chainID, tableID) -} - -func (s *SystemSQLStoreService) getAnimationURL(chainID tableland.ChainID, tableID tables.TableID) string { - if s.animationRendererURI == "" { - return DefaultAnimationURL - } - - return fmt.Sprintf("%s/?chain=%d&id=%s", s.animationRendererURI, chainID, tableID) -} - -func (s *SystemSQLStoreService) emptyMetadataImage() string { - svg := `` //nolint - svgEncoded := base64.StdEncoding.EncodeToString([]byte(svg)) - return fmt.Sprintf("data:image/svg+xml;base64,%s", svgEncoded) -} diff --git a/internal/system/impl/sqlstore_instrumented.go b/internal/system/impl/sqlstore_instrumented.go deleted file mode 100644 index f2e4db2f..00000000 --- a/internal/system/impl/sqlstore_instrumented.go +++ /dev/null @@ -1,148 +0,0 @@ -package impl - -import ( - "context" - "fmt" - "time" - - "github.com/ethereum/go-ethereum/common" - "github.com/textileio/go-tableland/internal/router/middlewares" - "github.com/textileio/go-tableland/internal/system" - "github.com/textileio/go-tableland/internal/tableland" - "github.com/textileio/go-tableland/pkg/metrics" - "github.com/textileio/go-tableland/pkg/sqlstore" - "github.com/textileio/go-tableland/pkg/tables" - "go.opentelemetry.io/otel/attribute" - "go.opentelemetry.io/otel/metric/global" - "go.opentelemetry.io/otel/metric/instrument" -) - -// InstrumentedSystemSQLStoreService implements the SystemService interface using SQLStore. -type InstrumentedSystemSQLStoreService struct { - system system.SystemService - callCount instrument.Int64Counter - latencyHistogram instrument.Int64Histogram -} - -// NewInstrumentedSystemSQLStoreService creates a new InstrumentedSystemSQLStoreService. -func NewInstrumentedSystemSQLStoreService(system system.SystemService) (system.SystemService, error) { - meter := global.MeterProvider().Meter("tableland") - callCount, err := meter.Int64Counter("tableland.system.call.count") - if err != nil { - return &InstrumentedSystemSQLStoreService{}, fmt.Errorf("registering call counter: %s", err) - } - latencyHistogram, err := meter.Int64Histogram("tableland.system.call.latency") - if err != nil { - return &InstrumentedSystemSQLStoreService{}, fmt.Errorf("registering latency histogram: %s", err) - } - - return &InstrumentedSystemSQLStoreService{system, callCount, latencyHistogram}, nil -} - -// GetReceiptByTransactionHash implements system.SystemService. -func (s *InstrumentedSystemSQLStoreService) GetReceiptByTransactionHash( - ctx context.Context, - hash common.Hash, -) (sqlstore.Receipt, bool, error) { - start := time.Now() - receipt, exists, err := s.system.GetReceiptByTransactionHash(ctx, hash) - latency := time.Since(start).Milliseconds() - chainID, _ := ctx.Value(middlewares.ContextKeyChainID).(tableland.ChainID) - - attributes := append([]attribute.KeyValue{ - {Key: "method", Value: attribute.StringValue("GetReceiptByTransactionHash")}, - {Key: "success", Value: attribute.BoolValue(err == nil)}, - {Key: "chainID", Value: attribute.Int64Value(int64(chainID))}, - }, metrics.BaseAttrs...) - - s.callCount.Add(ctx, 1, attributes...) - s.latencyHistogram.Record(ctx, latency, attributes...) - - return receipt, exists, err -} - -// GetTableMetadata returns table's metadata fetched from SQLStore. -func (s *InstrumentedSystemSQLStoreService) GetTableMetadata( - ctx context.Context, - id tables.TableID, -) (sqlstore.TableMetadata, error) { - start := time.Now() - metadata, err := s.system.GetTableMetadata(ctx, id) - latency := time.Since(start).Milliseconds() - chainID, _ := ctx.Value(middlewares.ContextKeyChainID).(tableland.ChainID) - - // NOTE: we may face a risk of high-cardilatity in the future. This should be revised. - attributes := append([]attribute.KeyValue{ - {Key: "method", Value: attribute.StringValue("GetTableMetadata")}, - {Key: "success", Value: attribute.BoolValue(err == nil)}, - {Key: "chainID", Value: attribute.Int64Value(int64(chainID))}, - }, metrics.BaseAttrs...) - - s.callCount.Add(ctx, 1, attributes...) - s.latencyHistogram.Record(ctx, latency, attributes...) - - return metadata, err -} - -// GetTablesByController returns table's fetched from SQLStore by controller address. -func (s *InstrumentedSystemSQLStoreService) GetTablesByController(ctx context.Context, - controller string, -) ([]sqlstore.Table, error) { - start := time.Now() - tables, err := s.system.GetTablesByController(ctx, controller) - latency := time.Since(start).Milliseconds() - chainID, _ := ctx.Value(middlewares.ContextKeyChainID).(tableland.ChainID) - - attributes := append([]attribute.KeyValue{ - {Key: "method", Value: attribute.StringValue("GetTablesByController")}, - {Key: "success", Value: attribute.BoolValue(err == nil)}, - {Key: "chainID", Value: attribute.Int64Value(int64(chainID))}, - }, metrics.BaseAttrs...) - - s.callCount.Add(ctx, 1, attributes...) - s.latencyHistogram.Record(ctx, latency, attributes...) - - return tables, err -} - -// GetTablesByStructure returns all tables that share the same structure. -func (s *InstrumentedSystemSQLStoreService) GetTablesByStructure( - ctx context.Context, - structure string, -) ([]sqlstore.Table, error) { - start := time.Now() - tables, err := s.system.GetTablesByStructure(ctx, structure) - latency := time.Since(start).Milliseconds() - chainID, _ := ctx.Value(middlewares.ContextKeyChainID).(tableland.ChainID) - - attributes := append([]attribute.KeyValue{ - {Key: "method", Value: attribute.StringValue("GetTablesByStructure")}, - {Key: "success", Value: attribute.BoolValue(err == nil)}, - {Key: "chainID", Value: attribute.Int64Value(int64(chainID))}, - }, metrics.BaseAttrs...) - - s.callCount.Add(ctx, 1, attributes...) - s.latencyHistogram.Record(ctx, latency, attributes...) - - return tables, err -} - -// GetSchemaByTableName returns the schema of a table by its name. -func (s *InstrumentedSystemSQLStoreService) GetSchemaByTableName( - ctx context.Context, - tableName string, -) (sqlstore.TableSchema, error) { - start := time.Now() - tables, err := s.system.GetSchemaByTableName(ctx, tableName) - latency := time.Since(start).Milliseconds() - - attributes := append([]attribute.KeyValue{ - {Key: "method", Value: attribute.StringValue("GetSchemaByTableName")}, - {Key: "success", Value: attribute.BoolValue(err == nil)}, - }, metrics.BaseAttrs...) - - s.callCount.Add(ctx, 1, attributes...) - s.latencyHistogram.Record(ctx, latency, attributes...) - - return tables, err -} diff --git a/internal/system/system.go b/internal/system/system.go deleted file mode 100644 index a3ee19aa..00000000 --- a/internal/system/system.go +++ /dev/null @@ -1,23 +0,0 @@ -package system - -import ( - "context" - "errors" - - "github.com/ethereum/go-ethereum/common" - "github.com/textileio/go-tableland/pkg/sqlstore" - "github.com/textileio/go-tableland/pkg/tables" -) - -// ErrTableNotFound indicates that the table doesn't exist. -var ErrTableNotFound = errors.New("table not found") - -// SystemService defines what system operations can be done. -// TODO(json-rpc): this interface should be cleaned up after dropping support. -type SystemService interface { - GetTableMetadata(context.Context, tables.TableID) (sqlstore.TableMetadata, error) - GetTablesByController(context.Context, string) ([]sqlstore.Table, error) - GetTablesByStructure(context.Context, string) ([]sqlstore.Table, error) - GetSchemaByTableName(context.Context, string) (sqlstore.TableSchema, error) - GetReceiptByTransactionHash(context.Context, common.Hash) (sqlstore.Receipt, bool, error) -} diff --git a/internal/tableland/impl/acl.go b/internal/tableland/impl/acl.go index f54c04f7..0bd10f0a 100644 --- a/internal/tableland/impl/acl.go +++ b/internal/tableland/impl/acl.go @@ -12,15 +12,13 @@ import ( ) type acl struct { - store sqlstore.SystemStore - registry tables.TablelandTables + store sqlstore.SystemStore } // NewACL creates a new instance of the ACL. -func NewACL(store sqlstore.SystemStore, registry tables.TablelandTables) tableland.ACL { +func NewACL(store sqlstore.SystemStore) tableland.ACL { return &acl{ - store: store, - registry: registry, + store: store, } } diff --git a/internal/tableland/impl/mesa.go b/internal/tableland/impl/mesa.go deleted file mode 100644 index a1c9bcbf..00000000 --- a/internal/tableland/impl/mesa.go +++ /dev/null @@ -1,208 +0,0 @@ -package impl - -import ( - "context" - "fmt" - - "github.com/ethereum/go-ethereum/common" - "github.com/textileio/go-tableland/internal/chains" - "github.com/textileio/go-tableland/internal/tableland" - "github.com/textileio/go-tableland/pkg/parsing" - "github.com/textileio/go-tableland/pkg/sqlstore" - "github.com/textileio/go-tableland/pkg/tables" -) - -// TablelandMesa is the main implementation of Tableland spec. -type TablelandMesa struct { - parser parsing.SQLValidator - userStore sqlstore.UserStore - chainStacks map[tableland.ChainID]chains.ChainStack -} - -// NewTablelandMesa creates a new TablelandMesa. -func NewTablelandMesa( - parser parsing.SQLValidator, - userStore sqlstore.UserStore, - chainStacks map[tableland.ChainID]chains.ChainStack, -) tableland.Tableland { - return &TablelandMesa{ - parser: parser, - userStore: userStore, - chainStacks: chainStacks, - } -} - -// ValidateCreateTable allows to validate a CREATE TABLE statement and also return the structure hash of it. -// This RPC method is stateless. -func (t *TablelandMesa) ValidateCreateTable( - _ context.Context, - chainID tableland.ChainID, - statement string, -) (string, error) { - createStmt, err := t.parser.ValidateCreateTable(statement, chainID) - if err != nil { - return "", fmt.Errorf("parsing create table statement: %s", err) - } - return createStmt.GetStructureHash(), nil -} - -// ValidateWriteQuery allows the user to validate a write query. -func (t *TablelandMesa) ValidateWriteQuery( - ctx context.Context, - chainID tableland.ChainID, - statement string, -) (tables.TableID, error) { - stack, chainOk := t.chainStacks[chainID] - if !chainOk { - return tables.TableID{}, fmt.Errorf("chain id %d isn't supported in the validator", chainID) - } - - mutatingStmts, err := t.parser.ValidateMutatingQuery(statement, chainID) - if err != nil { - return tables.TableID{}, 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 tables.TableID{}, 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 tables.TableID{}, fmt.Errorf( - "table prefix doesn't match (exp %s, got %s)", table.Prefix, prefix) - } - - return tableID, nil -} - -// RelayWriteQuery allows the user to rely on the validator wrapping the query in a chain transaction. -func (t *TablelandMesa) RelayWriteQuery( - ctx context.Context, - chainID tableland.ChainID, - caller common.Address, - statement string, -) (tables.Transaction, error) { - stack, ok := t.chainStacks[chainID] - if !ok { - return nil, fmt.Errorf("chain id %d isn't supported in the validator", chainID) - } - - if !stack.AllowTransactionRelay { - return nil, - fmt.Errorf("chain id %d does not suppport relaying of transactions", chainID) - } - - mutatingStmts, err := t.parser.ValidateMutatingQuery(statement, chainID) - if err != nil { - return nil, fmt.Errorf("validating query: %s", err) - } - - tableID := mutatingStmts[0].GetTableID() - tx, err := stack.Registry.RunSQL(ctx, caller, tableID, statement) - if err != nil { - return nil, fmt.Errorf("sending tx: %s", err) - } - - return tx, nil -} - -// RunReadQuery allows the user to run SQL. -func (t *TablelandMesa) RunReadQuery(ctx context.Context, statement string) (*tableland.TableData, error) { - readStmt, err := t.parser.ValidateReadQuery(statement) - if err != nil { - return nil, fmt.Errorf("validating query: %s", err) - } - - queryResult, err := t.runSelect(ctx, readStmt) - if err != nil { - return nil, fmt.Errorf("running read statement: %s", err) - } - return queryResult, nil -} - -// GetReceipt returns the receipt of a processed event by txn hash. -func (t *TablelandMesa) GetReceipt( - ctx context.Context, - chainID tableland.ChainID, - txnHash string, -) (bool, *tableland.TxnReceipt, error) { - if err := (&common.Hash{}).UnmarshalText([]byte(txnHash)); err != nil { - return false, nil, fmt.Errorf("invalid txn hash: %s", err) - } - stack, ok := t.chainStacks[chainID] - if !ok { - return false, nil, fmt.Errorf("chain id %d isn't supported in the validator", chainID) - } - receipt, ok, err := stack.Store.GetReceipt(ctx, txnHash) - if err != nil { - return false, nil, fmt.Errorf("get txn receipt: %s", err) - } - if !ok { - return false, nil, nil - } - - errorEventIdx := -1 - if receipt.ErrorEventIdx != nil { - errorEventIdx = *receipt.ErrorEventIdx - } - errorMsg := "" - if receipt.Error != nil { - errorMsg = *receipt.Error - } - - ret := &tableland.TxnReceipt{ - ChainID: receipt.ChainID, - TxnHash: receipt.TxnHash, - BlockNumber: receipt.BlockNumber, - Error: errorMsg, - ErrorEventIdx: errorEventIdx, - } - - if receipt.TableID != nil { - tID := receipt.TableID.String() - ret.TableID = &tID - } - - return ok, ret, nil -} - -// SetController allows users to the controller for a token id. -func (t *TablelandMesa) SetController( - ctx context.Context, - chainID tableland.ChainID, - caller common.Address, - controller common.Address, - tableID tables.TableID, -) (tables.Transaction, error) { - stack, ok := t.chainStacks[chainID] - if !ok { - return nil, fmt.Errorf("chain id %d isn't supported in the validator", chainID) - } - - if !stack.AllowTransactionRelay { - return nil, fmt.Errorf("chain id %d does not suppport relaying of transactions", chainID) - } - - tx, err := stack.Registry.SetController(ctx, caller, tableID, controller) - if err != nil { - return nil, fmt.Errorf("sending tx: %s", err) - } - - return tx, nil -} - -func (t *TablelandMesa) runSelect( - ctx context.Context, - stmt parsing.ReadStmt, -) (*tableland.TableData, error) { - queryResult, err := t.userStore.Read(ctx, stmt) - if err != nil { - return nil, fmt.Errorf("executing read-query: %s", err) - } - - return queryResult, nil -} diff --git a/internal/tableland/impl/mesa_instrumented.go b/internal/tableland/impl/mesa_instrumented.go deleted file mode 100644 index 6d3cfb3d..00000000 --- a/internal/tableland/impl/mesa_instrumented.go +++ /dev/null @@ -1,139 +0,0 @@ -package impl - -import ( - "context" - "fmt" - "time" - - "github.com/ethereum/go-ethereum/common" - "github.com/textileio/go-tableland/internal/tableland" - "github.com/textileio/go-tableland/pkg/metrics" - "github.com/textileio/go-tableland/pkg/tables" - "go.opentelemetry.io/otel/attribute" - "go.opentelemetry.io/otel/metric/global" - "go.opentelemetry.io/otel/metric/instrument" -) - -// InstrumentedTablelandMesa is the main implementation of Tableland spec with instrumentaion. -type InstrumentedTablelandMesa struct { - tableland tableland.Tableland - callCount instrument.Int64Counter - latencyHistogram instrument.Int64Histogram -} - -type recordData struct { - method string - controller string - tableID string - success bool - latency int64 - chainID tableland.ChainID -} - -// NewInstrumentedTablelandMesa creates a new InstrumentedTablelandMesa. -func NewInstrumentedTablelandMesa(t tableland.Tableland) (tableland.Tableland, error) { - meter := global.MeterProvider().Meter("tableland") - callCount, err := meter.Int64Counter("tableland.mesa.call.count") - if err != nil { - return &InstrumentedTablelandMesa{}, fmt.Errorf("registering call counter: %s", err) - } - latencyHistogram, err := meter.Int64Histogram("tableland.mesa.call.latency") - if err != nil { - return &InstrumentedTablelandMesa{}, fmt.Errorf("registering latency histogram: %s", err) - } - - return &InstrumentedTablelandMesa{t, callCount, latencyHistogram}, nil -} - -// ValidateCreateTable validates a CREATE TABLE statement and returns its structure hash. -func (t *InstrumentedTablelandMesa) ValidateCreateTable( - ctx context.Context, - chainID tableland.ChainID, - stmt string, -) (string, error) { - start := time.Now() - resp, err := t.tableland.ValidateCreateTable(ctx, chainID, stmt) - latency := time.Since(start).Milliseconds() - t.record(ctx, recordData{"ValidateCreateTable", "", "", err == nil, latency, chainID}) - return resp, err -} - -// ValidateWriteQuery validates a statement that would mutate a table and returns the table ID. -func (t *InstrumentedTablelandMesa) ValidateWriteQuery( - ctx context.Context, - chainID tableland.ChainID, - stmt string, -) (tables.TableID, error) { - start := time.Now() - resp, err := t.tableland.ValidateWriteQuery(ctx, chainID, stmt) - latency := time.Since(start).Milliseconds() - 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, stmt string) (*tableland.TableData, error) { - start := time.Now() - resp, err := t.tableland.RunReadQuery(ctx, stmt) - latency := time.Since(start).Milliseconds() - - t.record(ctx, recordData{"RunReadQuery", "", "", err == nil, latency, 0}) - return resp, err -} - -// RelayWriteQuery allows the user to rely on the validator to wrap a write-query in a chain transaction. -func (t *InstrumentedTablelandMesa) RelayWriteQuery( - ctx context.Context, - chainID tableland.ChainID, - caller common.Address, - stmt string, -) (tables.Transaction, error) { - start := time.Now() - resp, err := t.tableland.RelayWriteQuery(ctx, chainID, caller, stmt) - latency := time.Since(start).Milliseconds() - - t.record(ctx, recordData{"RelayWriteQuery", caller.Hex(), "", err == nil, latency, chainID}) - return resp, err -} - -// GetReceipt returns the receipt for a txn hash. -func (t *InstrumentedTablelandMesa) GetReceipt( - ctx context.Context, - chainID tableland.ChainID, - txnHash string, -) (bool, *tableland.TxnReceipt, error) { - start := time.Now() - ok, resp, err := t.tableland.GetReceipt(ctx, chainID, txnHash) - latency := time.Since(start).Milliseconds() - - t.record(ctx, recordData{"GetReceipt", "", "", err == nil, latency, chainID}) - return ok, resp, err -} - -// SetController allows users to the controller for a token id. -func (t *InstrumentedTablelandMesa) SetController( - ctx context.Context, - chainID tableland.ChainID, - caller common.Address, - controller common.Address, - tableID tables.TableID, -) (tables.Transaction, error) { - start := time.Now() - resp, err := t.tableland.SetController(ctx, chainID, caller, controller, tableID) - latency := time.Since(start).Milliseconds() - - t.record(ctx, recordData{"SetController", controller.Hex(), "", err == nil, latency, chainID}) - return resp, err -} - -func (t *InstrumentedTablelandMesa) record(ctx context.Context, data recordData) { - // NOTE: we may face a risk of high-cardilatity in the future. This should be revised. - attributes := append([]attribute.KeyValue{ - {Key: "method", Value: attribute.StringValue(data.method)}, - {Key: "table_id", Value: attribute.StringValue(data.tableID)}, - {Key: "success", Value: attribute.BoolValue(data.success)}, - }, metrics.BaseAttrs...) - - t.callCount.Add(ctx, 1, attributes...) - t.latencyHistogram.Record(ctx, data.latency, attributes...) -} diff --git a/internal/tableland/impl/mesa_test.go b/internal/tableland/impl/tableland_test.go similarity index 63% rename from internal/tableland/impl/mesa_test.go rename to internal/tableland/impl/tableland_test.go index 68c8174a..4c07d9ad 100644 --- a/internal/tableland/impl/mesa_test.go +++ b/internal/tableland/impl/tableland_test.go @@ -21,20 +21,17 @@ import ( "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/crypto" "github.com/stretchr/testify/require" - "github.com/textileio/go-tableland/internal/chains" + "github.com/textileio/go-tableland/internal/gateway" "github.com/textileio/go-tableland/internal/tableland" - "github.com/textileio/go-tableland/pkg/eventprocessor" "github.com/textileio/go-tableland/pkg/eventprocessor/eventfeed" efimpl "github.com/textileio/go-tableland/pkg/eventprocessor/eventfeed/impl" epimpl "github.com/textileio/go-tableland/pkg/eventprocessor/impl" executor "github.com/textileio/go-tableland/pkg/eventprocessor/impl/executor/impl" - "github.com/textileio/go-tableland/pkg/nonce/impl" "github.com/textileio/go-tableland/pkg/parsing" parserimpl "github.com/textileio/go-tableland/pkg/parsing/impl" - rsresolver "github.com/textileio/go-tableland/pkg/readstatementresolver" "github.com/textileio/go-tableland/pkg/sqlstore" "github.com/textileio/go-tableland/pkg/sqlstore/impl/system" - "github.com/textileio/go-tableland/pkg/sqlstore/impl/user" + "github.com/textileio/go-tableland/pkg/tables" "github.com/textileio/go-tableland/pkg/tables/impl/ethereum" "github.com/textileio/go-tableland/pkg/tables/impl/testutil" @@ -46,12 +43,11 @@ func TestTodoAppWorkflow(t *testing.T) { t.Parallel() setup := newTablelandSetupBuilder(). - withAllowTransactionRelay(true). build(t) tablelandClient := setup.newTablelandClient(t) - ctx, chainID, backend, sc := setup.ctx, setup.chainID, setup.ethClient, setup.contract - tbld, txOpts := tablelandClient.tableland, tablelandClient.txOpts + ctx, backend, sc := setup.ctx, setup.ethClient, setup.contract + gateway, txOpts := tablelandClient.gateway, tablelandClient.txOpts caller := txOpts.From _, err := sc.CreateTable(txOpts, caller, @@ -63,7 +59,7 @@ func TestTodoAppWorkflow(t *testing.T) { );`) require.NoError(t, err) - processCSV(ctx, t, chainID, caller, tbld, "testdata/todoapp_queries.csv", backend) + processCSV(ctx, t, sc, txOpts, caller, gateway, "testdata/todoapp_queries.csv", backend) } func TestInsertOnConflict(t *testing.T) { @@ -73,12 +69,11 @@ func TestInsertOnConflict(t *testing.T) { t.SkipNow() setup := newTablelandSetupBuilder(). - withAllowTransactionRelay(true). build(t) tablelandClient := setup.newTablelandClient(t) - ctx, chainID, backend, sc := setup.ctx, setup.chainID, setup.ethClient, setup.contract - tbld, txOpts := tablelandClient.tableland, tablelandClient.txOpts + ctx, backend, sc, store := setup.ctx, setup.ethClient, setup.contract, setup.systemStore + gateway, txOpts := tablelandClient.gateway, tablelandClient.txOpts caller := txOpts.From @@ -91,10 +86,10 @@ func TestInsertOnConflict(t *testing.T) { var txnHashes []string for i := 0; i < 10; i++ { - txn, err := tbld.RelayWriteQuery( - ctx, - chainID, + txn, err := sc.RunSQL( + txOpts, caller, + big.NewInt(1), `INSERT INTO foo_1337_1 VALUES ('bar', 0) ON CONFLICT (name) DO UPDATE SET count=_1.count+1`, ) require.NoError(t, err) @@ -104,23 +99,22 @@ func TestInsertOnConflict(t *testing.T) { require.Eventually( t, - jsonEq(ctx, t, tbld, "SELECT count FROM foo_1337_1", `{"columns":[{"name":"count"}],"rows":[[9]]}`), + jsonEq(ctx, t, gateway, "SELECT count FROM foo_1337_1", `{"columns":[{"name":"count"}],"rows":[[9]]}`), time.Second*5, time.Millisecond*100, ) - requireReceipts(ctx, t, tbld, chainID, txnHashes, true) + requireReceipts(ctx, t, store, txnHashes, true) } func TestMultiStatement(t *testing.T) { t.Parallel() setup := newTablelandSetupBuilder(). - withAllowTransactionRelay(true). build(t) tablelandClient := setup.newTablelandClient(t) - ctx, chainID, backend, sc := setup.ctx, setup.chainID, setup.ethClient, setup.contract - tbld, txOpts := tablelandClient.tableland, tablelandClient.txOpts + ctx, backend, sc, store := setup.ctx, setup.ethClient, setup.contract, setup.systemStore + gateway, txOpts := tablelandClient.gateway, tablelandClient.txOpts caller := txOpts.From _, err := sc.CreateTable(txOpts, caller, @@ -129,40 +123,40 @@ func TestMultiStatement(t *testing.T) { );`) require.NoError(t, err) - r, err := tbld.RelayWriteQuery( - ctx, - chainID, + r, err := sc.RunSQL( + txOpts, caller, + big.NewInt(1), `INSERT INTO foo_1337_1 values ('bar'); UPDATE foo_1337_1 SET name='zoo'`, ) + require.NoError(t, err) backend.Commit() require.Eventually( t, - jsonEq(ctx, t, tbld, "SELECT name from foo_1337_1", `{"columns":[{"name":"name"}],"rows":[["zoo"]]}`), + jsonEq(ctx, t, gateway, "SELECT name from foo_1337_1", `{"columns":[{"name":"name"}],"rows":[["zoo"]]}`), time.Second*5, time.Millisecond*100, ) - requireReceipts(ctx, t, tbld, chainID, []string{r.Hash().Hex()}, true) + requireReceipts(ctx, t, store, []string{r.Hash().Hex()}, true) } func TestReadSystemTable(t *testing.T) { t.Parallel() setup := newTablelandSetupBuilder(). - withAllowTransactionRelay(true). build(t) tablelandClient := setup.newTablelandClient(t) ctx, sc := setup.ctx, setup.contract - tbld, txOpts := tablelandClient.tableland, tablelandClient.txOpts + gateway, txOpts := tablelandClient.gateway, tablelandClient.txOpts caller := txOpts.From _, err := sc.CreateTable(txOpts, caller, `CREATE TABLE foo_1337 (myjson TEXT);`) require.NoError(t, err) - res, err := runReadQuery(ctx, t, tbld, "select * from registry") + res, err := runReadQuery(ctx, t, gateway, "select * from registry") require.NoError(t, err) _, err = json.Marshal(res) require.NoError(t, err) @@ -172,18 +166,17 @@ func TestJSON(t *testing.T) { t.Parallel() setup := newTablelandSetupBuilder(). - withAllowTransactionRelay(true). build(t) tablelandClient := setup.newTablelandClient(t) - ctx, chainID, backend, sc := setup.ctx, setup.chainID, setup.ethClient, setup.contract - tbld, txOpts := tablelandClient.tableland, tablelandClient.txOpts + ctx, backend, sc := setup.ctx, setup.ethClient, setup.contract + gateway, txOpts := tablelandClient.gateway, tablelandClient.txOpts caller := txOpts.From _, err := sc.CreateTable(txOpts, caller, `CREATE TABLE foo_1337 (myjson TEXT);`) require.NoError(t, err) - processCSV(ctx, t, chainID, caller, tbld, "testdata/json_queries.csv", backend) + processCSV(ctx, t, sc, txOpts, caller, gateway, "testdata/json_queries.csv", backend) } func TestCheckInsertPrivileges(t *testing.T) { @@ -212,15 +205,14 @@ func TestCheckInsertPrivileges(t *testing.T) { t.Parallel() setup := newTablelandSetupBuilder(). - withAllowTransactionRelay(true). build(t) granterSetup := setup.newTablelandClient(t) granteeSetup := setup.newTablelandClient(t) - ctx, chainID, backend, sc := setup.ctx, setup.chainID, setup.ethClient, setup.contract - tbldGranter, txOptsGranter := granterSetup.tableland, granterSetup.txOpts - tbldGrantee, txOptsGrantee := granteeSetup.tableland, granteeSetup.txOpts + ctx, backend, sc, store := setup.ctx, setup.ethClient, setup.contract, setup.systemStore + txOptsGranter := granterSetup.txOpts + gatewayGrantee, txOptsGrantee := granteeSetup.gateway, granteeSetup.txOpts granter := txOptsGranter.From grantee := txOptsGrantee.From @@ -238,29 +230,29 @@ func TestCheckInsertPrivileges(t *testing.T) { // execute grant statement according to test case grantQuery := fmt.Sprintf("GRANT %s ON foo_1337_1 TO '%s'", strings.Join(privileges, ","), grantee) - txn, err := relayWriteQuery(ctx, t, chainID, tbldGranter, grantQuery, granter) + txn, err := helpTestWriteQuery(t, sc, txOptsGranter, granter, grantQuery) require.NoError(t, err) backend.Commit() successfulTxnHashes = append(successfulTxnHashes, txn.Hash().Hex()) } - txn, err := relayWriteQuery(ctx, t, chainID, tbldGrantee, test.query, grantee) + txn, err := helpTestWriteQuery(t, sc, txOptsGrantee, grantee, test.query) require.NoError(t, err) backend.Commit() testQuery := "SELECT * FROM foo_1337_1 WHERE bar ='Hello';" if test.isAllowed { require.Eventually(t, - runSQLCountEq(ctx, t, tbldGrantee, testQuery, 1), + runSQLCountEq(ctx, t, gatewayGrantee, testQuery, 1), 5*time.Second, 100*time.Millisecond, ) successfulTxnHashes = append(successfulTxnHashes, txn.Hash().Hex()) - requireReceipts(ctx, t, tbldGrantee, chainID, successfulTxnHashes, true) + requireReceipts(ctx, t, store, successfulTxnHashes, true) } else { - require.Never(t, runSQLCountEq(ctx, t, tbldGrantee, testQuery, 1), 5*time.Second, 100*time.Millisecond) - requireReceipts(ctx, t, tbldGrantee, chainID, successfulTxnHashes, true) - requireReceipts(ctx, t, tbldGrantee, chainID, []string{txn.Hash().Hex()}, false) + require.Never(t, runSQLCountEq(ctx, t, gatewayGrantee, testQuery, 1), 5*time.Second, 100*time.Millisecond) + requireReceipts(ctx, t, store, successfulTxnHashes, true) + requireReceipts(ctx, t, store, []string{txn.Hash().Hex()}, false) } } }(test)) @@ -293,15 +285,14 @@ func TestCheckUpdatePrivileges(t *testing.T) { t.Parallel() setup := newTablelandSetupBuilder(). - withAllowTransactionRelay(true). build(t) granterSetup := setup.newTablelandClient(t) granteeSetup := setup.newTablelandClient(t) - ctx, chainID, backend, sc := setup.ctx, setup.chainID, setup.ethClient, setup.contract - tbldGranter, txOptsGranter := granterSetup.tableland, granterSetup.txOpts - tbldGrantee, txOptsGrantee := granteeSetup.tableland, granteeSetup.txOpts + ctx, backend, sc, store := setup.ctx, setup.ethClient, setup.contract, setup.systemStore + txOptsGranter := granterSetup.txOpts + gatewayGrantee, txOptsGrantee := granteeSetup.gateway, granteeSetup.txOpts granter := txOptsGranter.From grantee := txOptsGrantee.From @@ -312,7 +303,7 @@ func TestCheckUpdatePrivileges(t *testing.T) { var successfulTxnHashes []string // we initilize the table with a row to be updated - txn, err := relayWriteQuery(ctx, t, chainID, tbldGranter, "INSERT INTO foo_1337_1 (bar) VALUES ('Hello')", granter) // nolint + txn, err := helpTestWriteQuery(t, sc, txOptsGranter, granter, "INSERT INTO foo_1337_1 (bar) VALUES ('Hello')") require.NoError(t, err) backend.Commit() successfulTxnHashes = append(successfulTxnHashes, txn.Hash().Hex()) @@ -325,29 +316,29 @@ func TestCheckUpdatePrivileges(t *testing.T) { // execute grant statement according to test case grantQuery := fmt.Sprintf("GRANT %s ON foo_1337_1 TO '%s'", strings.Join(privileges, ","), grantee) - txn, err := relayWriteQuery(ctx, t, chainID, tbldGranter, grantQuery, granter) + txn, err := helpTestWriteQuery(t, sc, txOptsGranter, granter, grantQuery) require.NoError(t, err) backend.Commit() successfulTxnHashes = append(successfulTxnHashes, txn.Hash().Hex()) } - txn, err = relayWriteQuery(ctx, t, chainID, tbldGrantee, test.query, grantee) + txn, err = helpTestWriteQuery(t, sc, txOptsGrantee, grantee, test.query) require.NoError(t, err) backend.Commit() testQuery := "SELECT * FROM foo_1337_1 WHERE bar='Hello 2';" if test.isAllowed { require.Eventually(t, - runSQLCountEq(ctx, t, tbldGrantee, testQuery, 1), + runSQLCountEq(ctx, t, gatewayGrantee, testQuery, 1), 5*time.Second, 100*time.Millisecond, ) successfulTxnHashes = append(successfulTxnHashes, txn.Hash().Hex()) - requireReceipts(ctx, t, tbldGrantee, chainID, successfulTxnHashes, true) + requireReceipts(ctx, t, store, successfulTxnHashes, true) } else { - require.Never(t, runSQLCountEq(ctx, t, tbldGrantee, testQuery, 1), 5*time.Second, 100*time.Millisecond) - requireReceipts(ctx, t, tbldGrantee, chainID, successfulTxnHashes, true) - requireReceipts(ctx, t, tbldGrantee, chainID, []string{txn.Hash().Hex()}, false) + require.Never(t, runSQLCountEq(ctx, t, gatewayGrantee, testQuery, 1), 5*time.Second, 100*time.Millisecond) + requireReceipts(ctx, t, store, successfulTxnHashes, true) + requireReceipts(ctx, t, store, []string{txn.Hash().Hex()}, false) } } }(test)) @@ -380,15 +371,14 @@ func TestCheckDeletePrivileges(t *testing.T) { t.Parallel() setup := newTablelandSetupBuilder(). - withAllowTransactionRelay(true). build(t) granterSetup := setup.newTablelandClient(t) granteeSetup := setup.newTablelandClient(t) - ctx, chainID, backend, sc := setup.ctx, setup.chainID, setup.ethClient, setup.contract - tbldGranter, txOptsGranter := granterSetup.tableland, granterSetup.txOpts - tbldGrantee, txOptsGrantee := granteeSetup.tableland, granteeSetup.txOpts + ctx, backend, sc, store := setup.ctx, setup.ethClient, setup.contract, setup.systemStore + txOptsGranter := granterSetup.txOpts + gatewayGrantee, txOptsGrantee := granteeSetup.gateway, granteeSetup.txOpts granter := txOptsGranter.From grantee := txOptsGrantee.From @@ -398,7 +388,7 @@ func TestCheckDeletePrivileges(t *testing.T) { var successfulTxnHashes []string // we initilize the table with a row to be delete - _, err = relayWriteQuery(ctx, t, chainID, tbldGranter, "INSERT INTO foo_1337_1 (bar) VALUES ('Hello')", granter) // nolint + _, err = helpTestWriteQuery(t, sc, txOptsGranter, granter, "INSERT INTO foo_1337_1 (bar) VALUES ('Hello')") require.NoError(t, err) backend.Commit() @@ -410,28 +400,28 @@ func TestCheckDeletePrivileges(t *testing.T) { // execute grant statement according to test case grantQuery := fmt.Sprintf("GRANT %s ON foo_1337_1 TO '%s'", strings.Join(privileges, ","), grantee) - txn, err := relayWriteQuery(ctx, t, chainID, tbldGranter, grantQuery, granter) + txn, err := helpTestWriteQuery(t, sc, txOptsGranter, granter, grantQuery) require.NoError(t, err) backend.Commit() successfulTxnHashes = append(successfulTxnHashes, txn.Hash().Hex()) } - txn, err := relayWriteQuery(ctx, t, chainID, tbldGrantee, test.query, grantee) + txn, err := helpTestWriteQuery(t, sc, txOptsGrantee, grantee, test.query) require.NoError(t, err) backend.Commit() testQuery := "SELECT * FROM foo_1337_1" if test.isAllowed { require.Eventually(t, - runSQLCountEq(ctx, t, tbldGrantee, testQuery, 0), + runSQLCountEq(ctx, t, gatewayGrantee, testQuery, 0), 5*time.Second, 100*time.Millisecond, ) successfulTxnHashes = append(successfulTxnHashes, txn.Hash().Hex()) - requireReceipts(ctx, t, tbldGrantee, chainID, successfulTxnHashes, true) + requireReceipts(ctx, t, store, successfulTxnHashes, true) } else { - require.Never(t, runSQLCountEq(ctx, t, tbldGrantee, testQuery, 0), 5*time.Second, 100*time.Millisecond) - requireReceipts(ctx, t, tbldGrantee, chainID, []string{txn.Hash().Hex()}, false) + require.Never(t, runSQLCountEq(ctx, t, gatewayGrantee, testQuery, 0), 5*time.Second, 100*time.Millisecond) + requireReceipts(ctx, t, store, []string{txn.Hash().Hex()}, false) } } }(test)) @@ -442,12 +432,11 @@ func TestOwnerRevokesItsPrivilegeInsideMultipleStatements(t *testing.T) { t.Parallel() setup := newTablelandSetupBuilder(). - withAllowTransactionRelay(true). build(t) tablelandClient := setup.newTablelandClient(t) - ctx, chainID, backend, sc := setup.ctx, setup.chainID, setup.ethClient, setup.contract - tbld, txOpts := tablelandClient.tableland, tablelandClient.txOpts + ctx, backend, sc, store := setup.ctx, setup.ethClient, setup.contract, setup.systemStore + gateway, txOpts := tablelandClient.gateway, tablelandClient.txOpts caller := txOpts.From _, err := sc.CreateTable(txOpts, caller, `CREATE TABLE foo_1337 (bar text);`) @@ -459,29 +448,28 @@ func TestOwnerRevokesItsPrivilegeInsideMultipleStatements(t *testing.T) { REVOKE update ON foo_1337_1 FROM '` + caller.Hex() + `'; UPDATE foo_1337_1 SET bar = 'Hello 3'; ` - txn, err := relayWriteQuery(ctx, t, chainID, tbld, multiStatements, caller) + txn, err := helpTestWriteQuery(t, sc, txOpts, caller, multiStatements) require.NoError(t, err) backend.Commit() testQuery := "SELECT * FROM foo_1337_1;" - cond := runSQLCountEq(ctx, t, tbld, testQuery, 1) + cond := runSQLCountEq(ctx, t, gateway, testQuery, 1) require.Never(t, cond, 5*time.Second, 100*time.Millisecond) - requireReceipts(ctx, t, tbld, chainID, []string{txn.Hash().Hex()}, false) + requireReceipts(ctx, t, store, []string{txn.Hash().Hex()}, false) } func TestTransferTable(t *testing.T) { t.Parallel() setup := newTablelandSetupBuilder(). - withAllowTransactionRelay(true). build(t) owner1Setup := setup.newTablelandClient(t) owner2Setup := setup.newTablelandClient(t) - ctx, chainID, backend, sc := setup.ctx, setup.chainID, setup.ethClient, setup.contract - tbldOwner1, txOptsOwner1 := owner1Setup.tableland, owner1Setup.txOpts - tbldOwner2, txOptsOwner2 := owner2Setup.tableland, owner2Setup.txOpts + ctx, backend, sc, store := setup.ctx, setup.ethClient, setup.contract, setup.systemStore + gatewayOwner1, txOptsOwner1 := owner1Setup.gateway, owner1Setup.txOpts + gatewayOwner2, txOptsOwner2 := owner2Setup.gateway, owner2Setup.txOpts _, err := sc.CreateTable(txOptsOwner1, txOptsOwner1.From, `CREATE TABLE foo_1337 (bar text);`) require.NoError(t, err) @@ -492,36 +480,36 @@ func TestTransferTable(t *testing.T) { // we'll execute one insert with owner1 and one insert with owner2 query1 := "INSERT INTO foo_1337_1 (bar) VALUES ('Hello')" - txn1, err := relayWriteQuery(ctx, t, chainID, tbldOwner1, query1, txOptsOwner1.From) + txn1, err := helpTestWriteQuery(t, sc, txOptsOwner1, txOptsOwner1.From, query1) require.NoError(t, err) backend.Commit() query2 := "INSERT INTO foo_1337_1 (bar) VALUES ('Hello2')" - txn2, err := relayWriteQuery(ctx, t, chainID, tbldOwner2, query2, txOptsOwner2.From) + txn2, err := helpTestWriteQuery(t, sc, txOptsOwner2, txOptsOwner2.From, query2) require.NoError(t, err) backend.Commit() // insert from owner1 will NEVER go through require.Never(t, - runSQLCountEq(ctx, t, tbldOwner1, "SELECT * FROM foo_1337_1 WHERE bar ='Hello';", 1), + runSQLCountEq(ctx, t, gatewayOwner1, "SELECT * FROM foo_1337_1 WHERE bar ='Hello';", 1), 5*time.Second, 100*time.Millisecond, ) - requireReceipts(ctx, t, tbldOwner1, chainID, []string{txn1.Hash().Hex()}, false) + requireReceipts(ctx, t, store, []string{txn1.Hash().Hex()}, false) // insert from owner2 will EVENTUALLY go through require.Eventually(t, - runSQLCountEq(ctx, t, tbldOwner2, "SELECT * FROM foo_1337_1 WHERE bar ='Hello2';", 1), + runSQLCountEq(ctx, t, gatewayOwner2, "SELECT * FROM foo_1337_1 WHERE bar ='Hello2';", 1), 5*time.Second, 100*time.Millisecond, ) - requireReceipts(ctx, t, tbldOwner2, chainID, []string{txn2.Hash().Hex()}, true) + requireReceipts(ctx, t, store, []string{txn2.Hash().Hex()}, true) // check registry table new ownership require.Eventually(t, runSQLCountEq(ctx, t, - tbldOwner2, + gatewayOwner2, fmt.Sprintf("SELECT * FROM registry WHERE controller = '%s' AND id = 1 AND chain_id = 1337", txOptsOwner2.From.Hex()), // nolint 1, ), @@ -530,148 +518,13 @@ func TestTransferTable(t *testing.T) { ) } -func TestQueryConstraints(t *testing.T) { - t.Parallel() - - t.Run("write-query-size-ok", func(t *testing.T) { - t.Parallel() - - parsingOpts := []parsing.Option{ - parsing.WithMaxWriteQuerySize(45), - } - - setup := newTablelandSetupBuilder(). - withAllowTransactionRelay(true). - withParsingOpts(parsingOpts...). - build(t) - tablelandClient := setup.newTablelandClient(t) - - ctx, chainID, backend, sc := setup.ctx, setup.chainID, setup.ethClient, setup.contract - tbld, txOpts := tablelandClient.tableland, tablelandClient.txOpts - caller := txOpts.From - - _, err := sc.CreateTable(txOpts, caller, `CREATE TABLE foo_1337 (bar text);`) - require.NoError(t, err) - backend.Commit() - - _, err = tbld.RelayWriteQuery( - ctx, - chainID, - caller, - "INSERT INTO foo_1337_1 (bar) VALUES ('hello')", // length of 45 bytes - ) - require.NoError(t, err) - }) - - t.Run("write-query-size-nok", func(t *testing.T) { - t.Parallel() - - parsingOpts := []parsing.Option{ - parsing.WithMaxWriteQuerySize(45), - } - setup := newTablelandSetupBuilder(). - withAllowTransactionRelay(true). - withParsingOpts(parsingOpts...). - build(t) - tablelandClient := setup.newTablelandClient(t) - - ctx, chainID := setup.ctx, setup.chainID - tbld, txOpts := tablelandClient.tableland, tablelandClient.txOpts - caller := txOpts.From - - _, err := tbld.RelayWriteQuery( - ctx, - chainID, - caller, - "INSERT INTO foo_1337_1 (bar) VALUES ('hello2')", // length of 46 bytes - ) - require.Error(t, err) - require.ErrorContains(t, err, "write query size is too long") - }) - - t.Run("read-query-size-ok", func(t *testing.T) { - t.Parallel() - - parsingOpts := []parsing.Option{ - parsing.WithMaxReadQuerySize(44), - } - - setup := newTablelandSetupBuilder(). - withAllowTransactionRelay(true). - withParsingOpts(parsingOpts...). - build(t) - tablelandClient := setup.newTablelandClient(t) - - ctx, backend, sc := setup.ctx, setup.ethClient, setup.contract - tbld, txOpts := tablelandClient.tableland, tablelandClient.txOpts - caller := txOpts.From - - _, err := sc.CreateTable(txOpts, caller, `CREATE TABLE foo_1337 (bar text);`) - require.NoError(t, err) - backend.Commit() - - require.Eventually(t, - func() bool { - _, err := tbld.RunReadQuery(ctx, "SELECT * FROM foo_1337_1 WHERE bar = 'hello'") // length of 44 bytes - return err == nil - }, - 5*time.Second, - 100*time.Millisecond, - ) - }) - - t.Run("read-query-size-nok", func(t *testing.T) { - t.Parallel() - - parsingOpts := []parsing.Option{ - parsing.WithMaxReadQuerySize(44), - } - - setup := newTablelandSetupBuilder(). - withAllowTransactionRelay(true). - withParsingOpts(parsingOpts...). - build(t) - tablelandClient := setup.newTablelandClient(t) - - ctx := setup.ctx - tbld := tablelandClient.tableland - - _, err := tbld.RunReadQuery(ctx, "SELECT * FROM foo_1337_1 WHERE bar = 'hello2'") // length of 45 bytes - require.Error(t, err) - require.ErrorContains(t, err, "read query size is too long") - }) -} - -func TestAllowTransactionRelayConfig(t *testing.T) { - t.Parallel() - - setup := newTablelandSetupBuilder(). - withAllowTransactionRelay(false). - build(t) - - tablelandClient := setup.newTablelandClient(t) - - ctx, chainID, tbld, txOpts := setup.ctx, setup.chainID, tablelandClient.tableland, tablelandClient.txOpts - - t.Run("relay write query", func(t *testing.T) { - _, err := relayWriteQuery(ctx, t, chainID, tbld, "INSERT INTO foo_1337_1 VALUES ('bar', 0)", txOpts.From) - require.Error(t, err) - require.ErrorContains(t, err, "chain id 1337 does not suppport relaying of transactions") - }) - - t.Run("set controller", func(t *testing.T) { - _, err := setController(ctx, t, chainID, tbld, txOpts.From, common.Address{}, "1") // values don't matter - require.Error(t, err) - require.ErrorContains(t, err, "chain id 1337 does not suppport relaying of transactions") - }) -} - func processCSV( ctx context.Context, t *testing.T, - chainID tableland.ChainID, + sc *ethereum.Contract, + txOpts *bind.TransactOpts, caller common.Address, - tbld tableland.Tableland, + gateway gateway.Gateway, csvPath string, backend *backends.SimulatedBackend, ) { @@ -680,9 +533,9 @@ func processCSV( records := readCsvFile(t, csvPath) for _, record := range records { if record[0] == "r" { - require.Eventually(t, jsonEq(ctx, t, tbld, record[1], record[2]), time.Second*5, time.Millisecond*100) + require.Eventually(t, jsonEq(ctx, t, gateway, record[1], record[2]), time.Second*5, time.Millisecond*100) } else { - _, err := tbld.RelayWriteQuery(ctx, chainID, caller, record[1]) + _, err := sc.RunSQL(txOpts, caller, big.NewInt(1), record[1]) require.NoError(t, err) backend.Commit() } @@ -692,12 +545,12 @@ func processCSV( func jsonEq( ctx context.Context, t *testing.T, - tbld tableland.Tableland, + gateway gateway.Gateway, stm string, expJSON string, ) func() bool { return func() bool { - r, err := tbld.RunReadQuery(ctx, stm) + r, err := gateway.RunReadQuery(ctx, stm) // if we get a table undefined error, try again if err != nil && strings.Contains(err.Error(), "no such table") { return false @@ -765,34 +618,16 @@ func runReadQuery( return tbld.RunReadQuery(ctx, sql) } -func relayWriteQuery( - ctx context.Context, - t *testing.T, - chainID tableland.ChainID, - tbld tableland.Tableland, - sql string, - caller common.Address, -) (tables.Transaction, error) { - t.Helper() - - return tbld.RelayWriteQuery(ctx, chainID, caller, sql) -} - -func setController( - ctx context.Context, +func helpTestWriteQuery( t *testing.T, - chainID tableland.ChainID, - tbld tableland.Tableland, + sc *ethereum.Contract, + txOpts *bind.TransactOpts, caller common.Address, - controller common.Address, - tokenID string, + sql string, ) (tables.Transaction, error) { t.Helper() - tableID, err := tables.NewTableID(tokenID) - require.NoError(t, err) - - return tbld.SetController(ctx, chainID, caller, controller, tableID) + return sc.RunSQL(txOpts, caller, big.NewInt(1), sql) } func readCsvFile(t *testing.T, filePath string) [][]string { @@ -824,7 +659,7 @@ func (acl *aclHalfMock) CheckPrivileges( id tables.TableID, op tableland.Operation, ) (bool, error) { - aclImpl := NewACL(acl.sqlStore, nil) + aclImpl := NewACL(acl.sqlStore) return aclImpl.CheckPrivileges(ctx, tx, controller, id, op) } @@ -835,15 +670,16 @@ func (acl *aclHalfMock) IsOwner(_ context.Context, _ common.Address, _ tables.Ta func requireReceipts( ctx context.Context, t *testing.T, - tbld tableland.Tableland, - chainID tableland.ChainID, + store *system.SystemStore, txnHashes []string, ok bool, ) { t.Helper() for _, txnHash := range txnHashes { - found, receipt, err := tbld.GetReceipt(ctx, chainID, txnHash) + // TODO: GetReceipt is only used by the tests, we can use system service instead + + receipt, found, err := store.GetReceipt(ctx, txnHash) require.NoError(t, err) require.True(t, found) require.NotNil(t, receipt) @@ -904,24 +740,13 @@ func requireTxn( } type tablelandSetupBuilder struct { - allowTransactionRelay bool - parsingOpts []parsing.Option + parsingOpts []parsing.Option } func newTablelandSetupBuilder() *tablelandSetupBuilder { return &tablelandSetupBuilder{} } -func (b *tablelandSetupBuilder) withAllowTransactionRelay(v bool) *tablelandSetupBuilder { - b.allowTransactionRelay = v - return b -} - -func (b *tablelandSetupBuilder) withParsingOpts(opts ...parsing.Option) *tablelandSetupBuilder { - b.parsingOpts = opts - return b -} - func (b *tablelandSetupBuilder) build(t *testing.T) *tablelandSetup { t.Helper() dbURI := tests.Sqlite3URI(t) @@ -960,10 +785,6 @@ func (b *tablelandSetupBuilder) build(t *testing.T) *tablelandSetup { require.NoError(t, err) t.Cleanup(func() { ep.Stop() }) - userStore, err := user.New( - dbURI, rsresolver.New(map[tableland.ChainID]eventprocessor.EventProcessor{1337: ep})) - require.NoError(t, err) - return &tablelandSetup{ ctx: ctx, @@ -982,11 +803,7 @@ func (b *tablelandSetupBuilder) build(t *testing.T) *tablelandSetup { // common dependencies among mesa clients parser: parser, - userStore: userStore, systemStore: store, - - // configs - allowTransactionRelay: b.allowTransactionRelay, } } @@ -1008,11 +825,7 @@ type tablelandSetup struct { // common dependencies among tableland clients parser parsing.SQLValidator - userStore *user.UserStore systemStore *system.SystemStore - - // configs - allowTransactionRelay bool } func (s *tablelandSetup) newTablelandClient(t *testing.T) *tablelandClient { @@ -1033,32 +846,22 @@ func (s *tablelandSetup) newTablelandClient(t *testing.T) *tablelandClient { big.NewInt(1000000000000000000), ) - registry, err := ethereum.NewClient( - s.ethClient, - 1337, - s.contractAddr, - wallet, - impl.NewSimpleTracker(wallet, s.ethClient), + gateway, err := gateway.NewGateway( + s.parser, + map[tableland.ChainID]sqlstore.SystemStore{1337: s.systemStore}, + "https://tableland.network/tables", + "https://render.tableland.xyz", + "https://render.tableland.xyz/anim", ) require.NoError(t, err) - tbld := NewTablelandMesa( - s.parser, - s.userStore, - map[tableland.ChainID]chains.ChainStack{ - 1337: { - Store: s.systemStore, - Registry: registry, - AllowTransactionRelay: s.allowTransactionRelay, - }, - }) return &tablelandClient{ - tableland: tbld, - txOpts: txOpts, + gateway: gateway, + txOpts: txOpts, } } type tablelandClient struct { - tableland tableland.Tableland - txOpts *bind.TransactOpts + gateway gateway.Gateway + txOpts *bind.TransactOpts } diff --git a/internal/tableland/tableland.go b/internal/tableland/tableland.go index 0682c406..7f018e31 100644 --- a/internal/tableland/tableland.go +++ b/internal/tableland/tableland.go @@ -92,22 +92,6 @@ type TxnReceipt struct { // Tableland defines the interface of Tableland. type Tableland interface { RunReadQuery(ctx context.Context, stmt string) (*TableData, error) - ValidateCreateTable(ctx context.Context, chainID ChainID, stmt string) (string, error) - ValidateWriteQuery(ctx context.Context, chainID ChainID, stmt string) (tables.TableID, error) - RelayWriteQuery( - ctx context.Context, - chainID ChainID, - caller common.Address, - stmt string, - ) (tables.Transaction, error) - GetReceipt(ctx context.Context, chainID ChainID, txnHash string) (bool, *TxnReceipt, error) - SetController( - ctx context.Context, - chainID ChainID, - caller common.Address, - controller common.Address, - tableID tables.TableID, - ) (tables.Transaction, error) } // ChainID is a supported EVM chain identifier. diff --git a/mocks/Gateway.go b/mocks/Gateway.go new file mode 100644 index 00000000..7fc449b5 --- /dev/null +++ b/mocks/Gateway.go @@ -0,0 +1,189 @@ +// Code generated by mockery v2.14.0. DO NOT EDIT. + +package mocks + +import ( + context "context" + + common "github.com/ethereum/go-ethereum/common" + + mock "github.com/stretchr/testify/mock" + + sqlstore "github.com/textileio/go-tableland/pkg/sqlstore" + + tableland "github.com/textileio/go-tableland/internal/tableland" + + tables "github.com/textileio/go-tableland/pkg/tables" +) + +// Gateway is an autogenerated mock type for the Gateway type +type Gateway struct { + mock.Mock +} + +type Gateway_Expecter struct { + mock *mock.Mock +} + +func (_m *Gateway) EXPECT() *Gateway_Expecter { + return &Gateway_Expecter{mock: &_m.Mock} +} + +// GetReceiptByTransactionHash provides a mock function with given fields: _a0, _a1 +func (_m *Gateway) GetReceiptByTransactionHash(_a0 context.Context, _a1 common.Hash) (sqlstore.Receipt, bool, error) { + ret := _m.Called(_a0, _a1) + + var r0 sqlstore.Receipt + if rf, ok := ret.Get(0).(func(context.Context, common.Hash) sqlstore.Receipt); ok { + r0 = rf(_a0, _a1) + } else { + r0 = ret.Get(0).(sqlstore.Receipt) + } + + var r1 bool + if rf, ok := ret.Get(1).(func(context.Context, common.Hash) bool); ok { + r1 = rf(_a0, _a1) + } else { + r1 = ret.Get(1).(bool) + } + + var r2 error + if rf, ok := ret.Get(2).(func(context.Context, common.Hash) error); ok { + r2 = rf(_a0, _a1) + } else { + r2 = ret.Error(2) + } + + return r0, r1, r2 +} + +// Gateway_GetReceiptByTransactionHash_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetReceiptByTransactionHash' +type Gateway_GetReceiptByTransactionHash_Call struct { + *mock.Call +} + +// GetReceiptByTransactionHash is a helper method to define mock.On call +// - _a0 context.Context +// - _a1 common.Hash +func (_e *Gateway_Expecter) GetReceiptByTransactionHash(_a0 interface{}, _a1 interface{}) *Gateway_GetReceiptByTransactionHash_Call { + return &Gateway_GetReceiptByTransactionHash_Call{Call: _e.mock.On("GetReceiptByTransactionHash", _a0, _a1)} +} + +func (_c *Gateway_GetReceiptByTransactionHash_Call) Run(run func(_a0 context.Context, _a1 common.Hash)) *Gateway_GetReceiptByTransactionHash_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(common.Hash)) + }) + return _c +} + +func (_c *Gateway_GetReceiptByTransactionHash_Call) Return(_a0 sqlstore.Receipt, _a1 bool, _a2 error) *Gateway_GetReceiptByTransactionHash_Call { + _c.Call.Return(_a0, _a1, _a2) + return _c +} + +// GetTableMetadata provides a mock function with given fields: _a0, _a1 +func (_m *Gateway) GetTableMetadata(_a0 context.Context, _a1 tables.TableID) (sqlstore.TableMetadata, error) { + ret := _m.Called(_a0, _a1) + + var r0 sqlstore.TableMetadata + if rf, ok := ret.Get(0).(func(context.Context, tables.TableID) sqlstore.TableMetadata); ok { + r0 = rf(_a0, _a1) + } else { + r0 = ret.Get(0).(sqlstore.TableMetadata) + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, tables.TableID) error); ok { + r1 = rf(_a0, _a1) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Gateway_GetTableMetadata_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetTableMetadata' +type Gateway_GetTableMetadata_Call struct { + *mock.Call +} + +// GetTableMetadata is a helper method to define mock.On call +// - _a0 context.Context +// - _a1 tables.TableID +func (_e *Gateway_Expecter) GetTableMetadata(_a0 interface{}, _a1 interface{}) *Gateway_GetTableMetadata_Call { + return &Gateway_GetTableMetadata_Call{Call: _e.mock.On("GetTableMetadata", _a0, _a1)} +} + +func (_c *Gateway_GetTableMetadata_Call) Run(run func(_a0 context.Context, _a1 tables.TableID)) *Gateway_GetTableMetadata_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(tables.TableID)) + }) + return _c +} + +func (_c *Gateway_GetTableMetadata_Call) Return(_a0 sqlstore.TableMetadata, _a1 error) *Gateway_GetTableMetadata_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +// RunReadQuery provides a mock function with given fields: ctx, stmt +func (_m *Gateway) RunReadQuery(ctx context.Context, stmt string) (*tableland.TableData, error) { + ret := _m.Called(ctx, stmt) + + var r0 *tableland.TableData + if rf, ok := ret.Get(0).(func(context.Context, string) *tableland.TableData); ok { + r0 = rf(ctx, stmt) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*tableland.TableData) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, string) error); ok { + r1 = rf(ctx, stmt) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Gateway_RunReadQuery_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'RunReadQuery' +type Gateway_RunReadQuery_Call struct { + *mock.Call +} + +// RunReadQuery is a helper method to define mock.On call +// - ctx context.Context +// - stmt string +func (_e *Gateway_Expecter) RunReadQuery(ctx interface{}, stmt interface{}) *Gateway_RunReadQuery_Call { + return &Gateway_RunReadQuery_Call{Call: _e.mock.On("RunReadQuery", ctx, stmt)} +} + +func (_c *Gateway_RunReadQuery_Call) Run(run func(ctx context.Context, stmt string)) *Gateway_RunReadQuery_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(string)) + }) + return _c +} + +func (_c *Gateway_RunReadQuery_Call) Return(_a0 *tableland.TableData, _a1 error) *Gateway_RunReadQuery_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +type mockConstructorTestingTNewGateway interface { + mock.TestingT + Cleanup(func()) +} + +// NewGateway creates a new instance of Gateway. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +func NewGateway(t mockConstructorTestingTNewGateway) *Gateway { + mock := &Gateway{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/mocks/SQLRunner.go b/mocks/SQLRunner.go deleted file mode 100644 index 551382dd..00000000 --- a/mocks/SQLRunner.go +++ /dev/null @@ -1,86 +0,0 @@ -// Code generated by mockery v2.14.0. DO NOT EDIT. - -package mocks - -import ( - context "context" - - mock "github.com/stretchr/testify/mock" - - tableland "github.com/textileio/go-tableland/internal/tableland" -) - -// SQLRunner is an autogenerated mock type for the SQLRunner type -type SQLRunner struct { - mock.Mock -} - -type SQLRunner_Expecter struct { - mock *mock.Mock -} - -func (_m *SQLRunner) EXPECT() *SQLRunner_Expecter { - return &SQLRunner_Expecter{mock: &_m.Mock} -} - -// RunReadQuery provides a mock function with given fields: ctx, stmt -func (_m *SQLRunner) RunReadQuery(ctx context.Context, stmt string) (*tableland.TableData, error) { - ret := _m.Called(ctx, stmt) - - var r0 *tableland.TableData - if rf, ok := ret.Get(0).(func(context.Context, string) *tableland.TableData); ok { - r0 = rf(ctx, stmt) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*tableland.TableData) - } - } - - var r1 error - if rf, ok := ret.Get(1).(func(context.Context, string) error); ok { - r1 = rf(ctx, stmt) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// SQLRunner_RunReadQuery_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'RunReadQuery' -type SQLRunner_RunReadQuery_Call struct { - *mock.Call -} - -// RunReadQuery is a helper method to define mock.On call -// - ctx context.Context -// - stmt string -func (_e *SQLRunner_Expecter) RunReadQuery(ctx interface{}, stmt interface{}) *SQLRunner_RunReadQuery_Call { - return &SQLRunner_RunReadQuery_Call{Call: _e.mock.On("RunReadQuery", ctx, stmt)} -} - -func (_c *SQLRunner_RunReadQuery_Call) Run(run func(ctx context.Context, stmt string)) *SQLRunner_RunReadQuery_Call { - _c.Call.Run(func(args mock.Arguments) { - run(args[0].(context.Context), args[1].(string)) - }) - return _c -} - -func (_c *SQLRunner_RunReadQuery_Call) Return(_a0 *tableland.TableData, _a1 error) *SQLRunner_RunReadQuery_Call { - _c.Call.Return(_a0, _a1) - return _c -} - -type mockConstructorTestingTNewSQLRunner interface { - mock.TestingT - Cleanup(func()) -} - -// NewSQLRunner creates a new instance of SQLRunner. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. -func NewSQLRunner(t mockConstructorTestingTNewSQLRunner) *SQLRunner { - mock := &SQLRunner{} - mock.Mock.Test(t) - - t.Cleanup(func() { mock.AssertExpectations(t) }) - - return mock -} diff --git a/mocks/Tableland.go b/mocks/Tableland.go deleted file mode 100644 index 88b2fc5e..00000000 --- a/mocks/Tableland.go +++ /dev/null @@ -1,336 +0,0 @@ -// Code generated by mockery v2.14.0. DO NOT EDIT. - -package mocks - -import ( - context "context" - - common "github.com/ethereum/go-ethereum/common" - - mock "github.com/stretchr/testify/mock" - - tableland "github.com/textileio/go-tableland/internal/tableland" - - tables "github.com/textileio/go-tableland/pkg/tables" -) - -// Tableland is an autogenerated mock type for the Tableland type -type Tableland struct { - mock.Mock -} - -type Tableland_Expecter struct { - mock *mock.Mock -} - -func (_m *Tableland) EXPECT() *Tableland_Expecter { - return &Tableland_Expecter{mock: &_m.Mock} -} - -// GetReceipt provides a mock function with given fields: ctx, chainID, txnHash -func (_m *Tableland) GetReceipt(ctx context.Context, chainID tableland.ChainID, txnHash string) (bool, *tableland.TxnReceipt, error) { - ret := _m.Called(ctx, chainID, txnHash) - - var r0 bool - if rf, ok := ret.Get(0).(func(context.Context, tableland.ChainID, string) bool); ok { - r0 = rf(ctx, chainID, txnHash) - } else { - r0 = ret.Get(0).(bool) - } - - var r1 *tableland.TxnReceipt - if rf, ok := ret.Get(1).(func(context.Context, tableland.ChainID, string) *tableland.TxnReceipt); ok { - r1 = rf(ctx, chainID, txnHash) - } else { - if ret.Get(1) != nil { - r1 = ret.Get(1).(*tableland.TxnReceipt) - } - } - - var r2 error - if rf, ok := ret.Get(2).(func(context.Context, tableland.ChainID, string) error); ok { - r2 = rf(ctx, chainID, txnHash) - } else { - r2 = ret.Error(2) - } - - return r0, r1, r2 -} - -// Tableland_GetReceipt_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetReceipt' -type Tableland_GetReceipt_Call struct { - *mock.Call -} - -// GetReceipt is a helper method to define mock.On call -// - ctx context.Context -// - chainID tableland.ChainID -// - txnHash string -func (_e *Tableland_Expecter) GetReceipt(ctx interface{}, chainID interface{}, txnHash interface{}) *Tableland_GetReceipt_Call { - return &Tableland_GetReceipt_Call{Call: _e.mock.On("GetReceipt", ctx, chainID, txnHash)} -} - -func (_c *Tableland_GetReceipt_Call) Run(run func(ctx context.Context, chainID tableland.ChainID, txnHash string)) *Tableland_GetReceipt_Call { - _c.Call.Run(func(args mock.Arguments) { - run(args[0].(context.Context), args[1].(tableland.ChainID), args[2].(string)) - }) - return _c -} - -func (_c *Tableland_GetReceipt_Call) Return(_a0 bool, _a1 *tableland.TxnReceipt, _a2 error) *Tableland_GetReceipt_Call { - _c.Call.Return(_a0, _a1, _a2) - return _c -} - -// RelayWriteQuery provides a mock function with given fields: ctx, chainID, caller, stmt -func (_m *Tableland) RelayWriteQuery(ctx context.Context, chainID tableland.ChainID, caller common.Address, stmt string) (tables.Transaction, error) { - ret := _m.Called(ctx, chainID, caller, stmt) - - var r0 tables.Transaction - if rf, ok := ret.Get(0).(func(context.Context, tableland.ChainID, common.Address, string) tables.Transaction); ok { - r0 = rf(ctx, chainID, caller, stmt) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(tables.Transaction) - } - } - - var r1 error - if rf, ok := ret.Get(1).(func(context.Context, tableland.ChainID, common.Address, string) error); ok { - r1 = rf(ctx, chainID, caller, stmt) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// Tableland_RelayWriteQuery_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'RelayWriteQuery' -type Tableland_RelayWriteQuery_Call struct { - *mock.Call -} - -// RelayWriteQuery is a helper method to define mock.On call -// - ctx context.Context -// - chainID tableland.ChainID -// - caller common.Address -// - stmt string -func (_e *Tableland_Expecter) RelayWriteQuery(ctx interface{}, chainID interface{}, caller interface{}, stmt interface{}) *Tableland_RelayWriteQuery_Call { - return &Tableland_RelayWriteQuery_Call{Call: _e.mock.On("RelayWriteQuery", ctx, chainID, caller, stmt)} -} - -func (_c *Tableland_RelayWriteQuery_Call) Run(run func(ctx context.Context, chainID tableland.ChainID, caller common.Address, stmt string)) *Tableland_RelayWriteQuery_Call { - _c.Call.Run(func(args mock.Arguments) { - run(args[0].(context.Context), args[1].(tableland.ChainID), args[2].(common.Address), args[3].(string)) - }) - return _c -} - -func (_c *Tableland_RelayWriteQuery_Call) Return(_a0 tables.Transaction, _a1 error) *Tableland_RelayWriteQuery_Call { - _c.Call.Return(_a0, _a1) - return _c -} - -// RunReadQuery provides a mock function with given fields: ctx, stmt -func (_m *Tableland) RunReadQuery(ctx context.Context, stmt string) (*tableland.TableData, error) { - ret := _m.Called(ctx, stmt) - - var r0 *tableland.TableData - if rf, ok := ret.Get(0).(func(context.Context, string) *tableland.TableData); ok { - r0 = rf(ctx, stmt) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*tableland.TableData) - } - } - - var r1 error - if rf, ok := ret.Get(1).(func(context.Context, string) error); ok { - r1 = rf(ctx, stmt) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// Tableland_RunReadQuery_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'RunReadQuery' -type Tableland_RunReadQuery_Call struct { - *mock.Call -} - -// RunReadQuery is a helper method to define mock.On call -// - ctx context.Context -// - stmt string -func (_e *Tableland_Expecter) RunReadQuery(ctx interface{}, stmt interface{}) *Tableland_RunReadQuery_Call { - return &Tableland_RunReadQuery_Call{Call: _e.mock.On("RunReadQuery", ctx, stmt)} -} - -func (_c *Tableland_RunReadQuery_Call) Run(run func(ctx context.Context, stmt string)) *Tableland_RunReadQuery_Call { - _c.Call.Run(func(args mock.Arguments) { - run(args[0].(context.Context), args[1].(string)) - }) - return _c -} - -func (_c *Tableland_RunReadQuery_Call) Return(_a0 *tableland.TableData, _a1 error) *Tableland_RunReadQuery_Call { - _c.Call.Return(_a0, _a1) - return _c -} - -// SetController provides a mock function with given fields: ctx, chainID, caller, controller, tableID -func (_m *Tableland) SetController(ctx context.Context, chainID tableland.ChainID, caller common.Address, controller common.Address, tableID tables.TableID) (tables.Transaction, error) { - ret := _m.Called(ctx, chainID, caller, controller, tableID) - - var r0 tables.Transaction - if rf, ok := ret.Get(0).(func(context.Context, tableland.ChainID, common.Address, common.Address, tables.TableID) tables.Transaction); ok { - r0 = rf(ctx, chainID, caller, controller, tableID) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(tables.Transaction) - } - } - - var r1 error - if rf, ok := ret.Get(1).(func(context.Context, tableland.ChainID, common.Address, common.Address, tables.TableID) error); ok { - r1 = rf(ctx, chainID, caller, controller, tableID) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// Tableland_SetController_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SetController' -type Tableland_SetController_Call struct { - *mock.Call -} - -// SetController is a helper method to define mock.On call -// - ctx context.Context -// - chainID tableland.ChainID -// - caller common.Address -// - controller common.Address -// - tableID tables.TableID -func (_e *Tableland_Expecter) SetController(ctx interface{}, chainID interface{}, caller interface{}, controller interface{}, tableID interface{}) *Tableland_SetController_Call { - return &Tableland_SetController_Call{Call: _e.mock.On("SetController", ctx, chainID, caller, controller, tableID)} -} - -func (_c *Tableland_SetController_Call) Run(run func(ctx context.Context, chainID tableland.ChainID, caller common.Address, controller common.Address, tableID tables.TableID)) *Tableland_SetController_Call { - _c.Call.Run(func(args mock.Arguments) { - run(args[0].(context.Context), args[1].(tableland.ChainID), args[2].(common.Address), args[3].(common.Address), args[4].(tables.TableID)) - }) - return _c -} - -func (_c *Tableland_SetController_Call) Return(_a0 tables.Transaction, _a1 error) *Tableland_SetController_Call { - _c.Call.Return(_a0, _a1) - return _c -} - -// ValidateCreateTable provides a mock function with given fields: ctx, chainID, stmt -func (_m *Tableland) ValidateCreateTable(ctx context.Context, chainID tableland.ChainID, stmt string) (string, error) { - ret := _m.Called(ctx, chainID, stmt) - - var r0 string - if rf, ok := ret.Get(0).(func(context.Context, tableland.ChainID, string) string); ok { - r0 = rf(ctx, chainID, stmt) - } else { - r0 = ret.Get(0).(string) - } - - var r1 error - if rf, ok := ret.Get(1).(func(context.Context, tableland.ChainID, string) error); ok { - r1 = rf(ctx, chainID, stmt) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// Tableland_ValidateCreateTable_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ValidateCreateTable' -type Tableland_ValidateCreateTable_Call struct { - *mock.Call -} - -// ValidateCreateTable is a helper method to define mock.On call -// - ctx context.Context -// - chainID tableland.ChainID -// - stmt string -func (_e *Tableland_Expecter) ValidateCreateTable(ctx interface{}, chainID interface{}, stmt interface{}) *Tableland_ValidateCreateTable_Call { - return &Tableland_ValidateCreateTable_Call{Call: _e.mock.On("ValidateCreateTable", ctx, chainID, stmt)} -} - -func (_c *Tableland_ValidateCreateTable_Call) Run(run func(ctx context.Context, chainID tableland.ChainID, stmt string)) *Tableland_ValidateCreateTable_Call { - _c.Call.Run(func(args mock.Arguments) { - run(args[0].(context.Context), args[1].(tableland.ChainID), args[2].(string)) - }) - return _c -} - -func (_c *Tableland_ValidateCreateTable_Call) Return(_a0 string, _a1 error) *Tableland_ValidateCreateTable_Call { - _c.Call.Return(_a0, _a1) - return _c -} - -// ValidateWriteQuery provides a mock function with given fields: ctx, chainID, stmt -func (_m *Tableland) ValidateWriteQuery(ctx context.Context, chainID tableland.ChainID, stmt string) (tables.TableID, error) { - ret := _m.Called(ctx, chainID, stmt) - - var r0 tables.TableID - if rf, ok := ret.Get(0).(func(context.Context, tableland.ChainID, string) tables.TableID); ok { - r0 = rf(ctx, chainID, stmt) - } else { - r0 = ret.Get(0).(tables.TableID) - } - - var r1 error - if rf, ok := ret.Get(1).(func(context.Context, tableland.ChainID, string) error); ok { - r1 = rf(ctx, chainID, stmt) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// Tableland_ValidateWriteQuery_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ValidateWriteQuery' -type Tableland_ValidateWriteQuery_Call struct { - *mock.Call -} - -// ValidateWriteQuery is a helper method to define mock.On call -// - ctx context.Context -// - chainID tableland.ChainID -// - stmt string -func (_e *Tableland_Expecter) ValidateWriteQuery(ctx interface{}, chainID interface{}, stmt interface{}) *Tableland_ValidateWriteQuery_Call { - return &Tableland_ValidateWriteQuery_Call{Call: _e.mock.On("ValidateWriteQuery", ctx, chainID, stmt)} -} - -func (_c *Tableland_ValidateWriteQuery_Call) Run(run func(ctx context.Context, chainID tableland.ChainID, stmt string)) *Tableland_ValidateWriteQuery_Call { - _c.Call.Run(func(args mock.Arguments) { - run(args[0].(context.Context), args[1].(tableland.ChainID), args[2].(string)) - }) - return _c -} - -func (_c *Tableland_ValidateWriteQuery_Call) Return(_a0 tables.TableID, _a1 error) *Tableland_ValidateWriteQuery_Call { - _c.Call.Return(_a0, _a1) - return _c -} - -type mockConstructorTestingTNewTableland interface { - mock.TestingT - Cleanup(func()) -} - -// NewTableland creates a new instance of Tableland. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. -func NewTableland(t mockConstructorTestingTNewTableland) *Tableland { - mock := &Tableland{} - mock.Mock.Test(t) - - t.Cleanup(func() { mock.AssertExpectations(t) }) - - return mock -} diff --git a/pkg/client/chains.go b/pkg/client/chains.go index 8b2bc28b..5b55ff42 100644 --- a/pkg/client/chains.go +++ b/pkg/client/chains.go @@ -110,11 +110,6 @@ var Chains = map[ChainID]Chain{ }, } -// CanRelayWrites returns whether Tableland validators will relay write requests. -func (c Chain) CanRelayWrites() bool { - return c.ID != ChainIDs.Ethereum && c.ID != ChainIDs.Optimism && c.ID != ChainIDs.Polygon -} - // InfuraURLs contains the URLs for supported chains for Infura. var InfuraURLs = map[ChainID]string{ ChainIDs.EthereumGoerli: "https://goerli.infura.io/v3/%s", diff --git a/pkg/client/legacy/client.go b/pkg/client/legacy/client.go deleted file mode 100644 index 13804268..00000000 --- a/pkg/client/legacy/client.go +++ /dev/null @@ -1,521 +0,0 @@ -package clientlegacy - -import ( - "context" - "encoding/json" - "errors" - "fmt" - "math/big" - "net/http" - "time" - - "github.com/ethereum/go-ethereum/accounts/abi/bind" - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/ethclient" - "github.com/ethereum/go-ethereum/rpc" - "github.com/textileio/go-tableland/internal/router/controllers/legacy" - "github.com/textileio/go-tableland/internal/tableland" - "github.com/textileio/go-tableland/pkg/client" - "github.com/textileio/go-tableland/pkg/nonce/impl" - "github.com/textileio/go-tableland/pkg/siwe" - "github.com/textileio/go-tableland/pkg/tables" - "github.com/textileio/go-tableland/pkg/tables/impl/ethereum" - "github.com/textileio/go-tableland/pkg/wallet" -) - -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. -type TxnReceipt struct { - ChainID client.ChainID `json:"chain_id"` - TxnHash string `json:"txn_hash"` - BlockNumber int64 `json:"block_number"` - Error string `json:"error"` - ErrorEventIdx int `json:"error_event_idx"` - TableID *string `json:"table_id,omitempty"` -} - -// TableID is the ID of a Table. -type TableID big.Int - -// String returns a string representation of the TableID. -func (tid TableID) String() string { - bi := (big.Int)(tid) - return bi.String() -} - -// ToBigInt returns a *big.Int representation of the TableID. -func (tid TableID) ToBigInt() *big.Int { - bi := (big.Int)(tid) - b := &big.Int{} - b.Set(&bi) - return b -} - -// TableInfo summarizes information about a table. -type TableInfo struct { - Controller string `json:"controller"` - Name string `json:"name"` - Structure string `json:"structure"` - CreatedAt time.Time `json:"created_at"` -} - -// NewTableID creates a TableID from a string representation of the uint256. -func NewTableID(strID string) (TableID, error) { - tableID := &big.Int{} - if _, ok := tableID.SetString(strID, 10); !ok { - return TableID{}, fmt.Errorf("parsing stringified id failed") - } - if tableID.Cmp(&big.Int{}) < 0 { - return TableID{}, fmt.Errorf("table id is negative") - } - return TableID(*tableID), nil -} - -// Client is the Tableland client. -type Client struct { - tblRPC *rpc.Client - tblHTTP *http.Client - tblContract *ethereum.Client - chain client.Chain - relayWrites bool - wallet *wallet.Wallet -} - -type config struct { - chain *client.Chain - relayWrites *bool - infuraAPIKey string - alchemyAPIKey string - local bool - contractBackend bind.ContractBackend -} - -// NewClientOption controls the behavior of NewClient. -type NewClientOption func(*config) - -// NewClientChain specifies chaininfo. -func NewClientChain(chain client.Chain) NewClientOption { - return func(ncc *config) { - ncc.chain = &chain - } -} - -// NewClientRelayWrites specifies whether or not to relay write queries through the Tableland validator. -func NewClientRelayWrites(relay bool) NewClientOption { - return func(ncc *config) { - ncc.relayWrites = &relay - } -} - -// NewClientInfuraAPIKey specifies an Infura API to use when creating an EVM backend. -func NewClientInfuraAPIKey(key string) NewClientOption { - return func(c *config) { - c.infuraAPIKey = key - } -} - -// NewClientAlchemyAPIKey specifies an Alchemy API to use when creating an EVM backend. -func NewClientAlchemyAPIKey(key string) NewClientOption { - return func(c *config) { - c.alchemyAPIKey = key - } -} - -// NewClientLocal specifies that a local EVM backend should be used. -func NewClientLocal() NewClientOption { - return func(c *config) { - c.local = true - } -} - -// NewClientContractBackend specifies a custom EVM backend to use. -func NewClientContractBackend(backend bind.ContractBackend) NewClientOption { - return func(c *config) { - c.contractBackend = backend - } -} - -// NewClient creates a new Client. -func NewClient(ctx context.Context, wallet *wallet.Wallet, opts ...NewClientOption) (*Client, error) { - config := config{chain: &defaultChain} - for _, opt := range opts { - opt(&config) - } - var relay bool - if config.relayWrites != nil { - relay = *config.relayWrites - } else { - relay = config.chain.CanRelayWrites() - } - if relay && !config.chain.CanRelayWrites() { - return nil, errors.New("options specified to relay writes for a chain that doesn't support it") - } - - contractBackend, err := getContractBackend(ctx, config) - if err != nil { - return nil, fmt.Errorf("getting contract backend: %v", err) - } - - tblContract, err := ethereum.NewClient( - contractBackend, - tableland.ChainID(config.chain.ID), - config.chain.ContractAddr, - wallet, - impl.NewSimpleTracker(wallet, contractBackend), - ) - if err != nil { - return nil, fmt.Errorf("creating contract client: %v", err) - } - - siwe, err := siwe.EncodedSIWEMsg(tableland.ChainID(config.chain.ID), wallet, time.Hour*24*365) - if err != nil { - return nil, fmt.Errorf("creating siwe value: %v", err) - } - - tblRPC, err := rpc.DialContext(ctx, config.chain.Endpoint+"/rpc") - if err != nil { - return nil, fmt.Errorf("creating rpc client: %v", err) - } - tblRPC.SetHeader("Authorization", "Bearer "+siwe) - - return &Client{ - tblRPC: tblRPC, - tblHTTP: &http.Client{}, - tblContract: tblContract, - chain: *config.chain, - relayWrites: relay, - wallet: wallet, - }, nil -} - -// List lists something. -func (c *Client) List(ctx context.Context) ([]TableInfo, error) { - url := fmt.Sprintf( - "%s/chain/%d/tables/controller/%s", - c.chain.Endpoint, - c.chain.ID, - c.wallet.Address().Hex(), - ) - req, err := http.NewRequest("GET", url, nil) - if err != nil { - return nil, fmt.Errorf("creating request: %v", err) - } - req = req.WithContext(ctx) - res, err := c.tblHTTP.Do(req) - if err != nil { - return nil, fmt.Errorf("calling http endpoint: %v", err) - } - defer func() { - _ = res.Body.Close() - }() - - var ret []TableInfo - - if err := json.NewDecoder(res.Body).Decode(&ret); err != nil { - return nil, fmt.Errorf("decoding response body: %v", err) - } - - return ret, nil -} - -type createConfig struct { - prefix string - receiptTimeout *time.Duration -} - -// CreateOption controls the behavior of Create. -type CreateOption func(*createConfig) - -// WithPrefix allows you to specify an optional table name prefix where -// the final table name will be __. -func WithPrefix(prefix string) CreateOption { - return func(cc *createConfig) { - cc.prefix = prefix - } -} - -// WithReceiptTimeout specifies how long to wait for the Tableland -// receipt that contains the table id. -func WithReceiptTimeout(timeout time.Duration) CreateOption { - return func(cc *createConfig) { - cc.receiptTimeout = &timeout - } -} - -// Create creates a new table on the Tableland. -func (c *Client) Create(ctx context.Context, schema string, opts ...CreateOption) (TableID, string, error) { - defaultTimeout := time.Minute * 10 - conf := createConfig{receiptTimeout: &defaultTimeout} - for _, opt := range opts { - opt(&conf) - } - - createStatement := fmt.Sprintf("CREATE TABLE %s_%d %s", conf.prefix, c.chain.ID, schema) - req := &legacy.ValidateCreateTableRequest{CreateStatement: createStatement} - var res legacy.ValidateCreateTableResponse - - if err := c.tblRPC.CallContext(ctx, &res, "tableland_validateCreateTable", req); err != nil { - return TableID{}, "", fmt.Errorf("calling rpc validateCreateTable: %v", err) - } - - t, err := c.tblContract.CreateTable(ctx, c.wallet.Address(), createStatement) - if err != nil { - return TableID{}, "", fmt.Errorf("calling contract create table: %v", err) - } - - r, found, err := c.waitForReceipt(ctx, t.Hash().Hex(), *conf.receiptTimeout) - if err != nil { - return TableID{}, "", fmt.Errorf("waiting for txn receipt: %v", err) - } - if !found { - return TableID{}, "", errors.New("no receipt found before timeout") - } - - tableID, ok := big.NewInt(0).SetString(*r.TableID, 10) - if !ok { - return TableID{}, "", errors.New("parsing table id from response") - } - - return TableID(*tableID), fmt.Sprintf("%s_%d_%s", conf.prefix, c.chain.ID, *r.TableID), nil -} - -// Output is used to control the output format of a Read using the ReadOutput option. -type Output string - -const ( - // Table returns the query results as a JSON object with columns and rows properties. - Table Output = "table" - // Objects returns the query results as a JSON array of JSON objects. This is the default. - Objects Output = "objects" -) - -// ReadOption controls the behavior of Read. -type ReadOption func(*legacy.RunReadQueryRequest) - -// ReadOutput sets the output format. Default is Objects. -func ReadOutput(output Output) ReadOption { - return func(rrqr *legacy.RunReadQueryRequest) { - rrqr.Output = (*string)(&output) - } -} - -// ReadExtract specifies whether or not to extract the JSON object -// from the single property of the surrounding JSON object. -// Default is false. -func ReadExtract() ReadOption { - return func(rrqr *legacy.RunReadQueryRequest) { - v := true - rrqr.Extract = &v - } -} - -// ReadUnwrap specifies whether or not to unwrap the returned JSON objects from their surrounding array. -// Default is false. -func ReadUnwrap() ReadOption { - return func(rrqr *legacy.RunReadQueryRequest) { - v := true - rrqr.Unwrap = &v - } -} - -// Read runs a read query with the provided opts and unmarshals the results into target. -func (c *Client) Read(ctx context.Context, query string, target interface{}, opts ...ReadOption) error { - req := &legacy.RunReadQueryRequest{Statement: query} - for _, opt := range opts { - opt(req) - } - res := &legacy.RunReadQueryResponse{ - Result: target, - } - if err := c.tblRPC.CallContext(ctx, &res, "tableland_runReadQuery", req); err != nil { - return fmt.Errorf("calling rpc runReadQuery: %v", err) - } - return nil -} - -type writeConfig struct { - relay bool -} - -// WriteOption controls the behavior of Write. -type WriteOption func(*writeConfig) - -// WriteRelay specifies whether or not to relay write queries through the Tableland validator. -// Default behavior is false for main net EVM chains, true for all others. -func WriteRelay(relay bool) WriteOption { - return func(wc *writeConfig) { - wc.relay = relay - } -} - -// Write initiates a write query, returning the txn hash. -func (c *Client) Write(ctx context.Context, query string, opts ...WriteOption) (string, error) { - conf := writeConfig{relay: c.relayWrites} - for _, opt := range opts { - opt(&conf) - } - if conf.relay { - req := &legacy.RelayWriteQueryRequest{Statement: query} - var res legacy.RelayWriteQueryResponse - if err := c.tblRPC.CallContext(ctx, &res, "tableland_relayWriteQuery", req); err != nil { - return "", fmt.Errorf("calling rpc relayWriteQuery: %v", err) - } - return res.Transaction.Hash, nil - } - tableID, err := c.Validate(ctx, query) - if err != nil { - return "", fmt.Errorf("calling Validate: %v", err) - } - res, err := c.tblContract.RunSQL(ctx, c.wallet.Address(), tables.TableID(tableID), query) - if err != nil { - return "", fmt.Errorf("calling RunSQL: %v", err) - } - return res.Hash().Hex(), nil -} - -// Hash validates the provided create table statement and returns its hash. -func (c *Client) Hash(ctx context.Context, statement string) (string, error) { - req := &legacy.ValidateCreateTableRequest{CreateStatement: statement} - var res legacy.ValidateCreateTableResponse - if err := c.tblRPC.CallContext(ctx, &res, "tableland_validateCreateTable", req); err != nil { - return "", fmt.Errorf("calling rpc validateCreateTable: %v", err) - } - return res.StructureHash, nil -} - -// Validate validates a write query, returning the table id. -func (c *Client) Validate(ctx context.Context, statement string) (TableID, error) { - req := &legacy.ValidateWriteQueryRequest{Statement: statement} - var res legacy.ValidateWriteQueryResponse - if err := c.tblRPC.CallContext(ctx, &res, "tableland_validateWriteQuery", req); err != nil { - return TableID{}, fmt.Errorf("calling rpc validateWriteQuery: %v", err) - } - tableID, ok := big.NewInt(0).SetString(res.TableID, 10) - if !ok { - return TableID{}, errors.New("parsing table id from response") - } - - return TableID(*tableID), nil -} - -type receiptConfig struct { - timeout *time.Duration -} - -// ReceiptOption controls the behavior of calls to Receipt. -type ReceiptOption func(*receiptConfig) - -// WaitFor causes calls to Receipt to wait for the specified duration. -func WaitFor(timeout time.Duration) ReceiptOption { - return func(rc *receiptConfig) { - rc.timeout = &timeout - } -} - -// Receipt gets a transaction receipt. -func (c *Client) Receipt( - ctx context.Context, - txnHash string, - options ...ReceiptOption, -) (*TxnReceipt, bool, error) { - config := receiptConfig{} - for _, option := range options { - option(&config) - } - if config.timeout != nil { - return c.waitForReceipt(ctx, txnHash, *config.timeout) - } - return c.getReceipt(ctx, txnHash) -} - -// SetController sets the controller address for the specified table. -func (c *Client) SetController( - ctx context.Context, - controller common.Address, - tableID TableID, -) (string, error) { - req := legacy.SetControllerRequest{Controller: controller.Hex(), TokenID: tableID.String()} - var res legacy.SetControllerResponse - - if err := c.tblRPC.CallContext(ctx, &res, "tableland_setController", req); err != nil { - return "", fmt.Errorf("calling rpc setController: %v", err) - } - - return res.Transaction.Hash, nil -} - -func (c *Client) getReceipt(ctx context.Context, txnHash string) (*TxnReceipt, bool, error) { - req := legacy.GetReceiptRequest{TxnHash: txnHash} - var res legacy.GetReceiptResponse - if err := c.tblRPC.CallContext(ctx, &res, "tableland_getReceipt", req); err != nil { - return nil, false, fmt.Errorf("calling rpc getReceipt: %v", err) - } - if !res.Ok { - return nil, res.Ok, nil - } - - receipt := TxnReceipt{ - ChainID: client.ChainID(res.Receipt.ChainID), - TxnHash: res.Receipt.TxnHash, - BlockNumber: res.Receipt.BlockNumber, - Error: res.Receipt.Error, - ErrorEventIdx: res.Receipt.ErrorEventIdx, - TableID: res.Receipt.TableID, - } - return &receipt, res.Ok, nil -} - -func (c *Client) waitForReceipt( - ctx context.Context, - txnHash string, - timeout time.Duration, -) (*TxnReceipt, bool, error) { - for stay, timeout := true, time.After(timeout); stay; { - select { - case <-timeout: - stay = false - default: - receipt, found, err := c.getReceipt(ctx, txnHash) - if err != nil { - return nil, false, err - } - if found { - return receipt, found, nil - } - time.Sleep(time.Second) - } - } - return nil, false, nil -} - -// Close implements Close. -func (c *Client) Close() { - c.tblRPC.Close() -} - -func getContractBackend(ctx context.Context, config config) (bind.ContractBackend, error) { - if config.contractBackend != nil && config.infuraAPIKey == "" && config.alchemyAPIKey == "" { - return config.contractBackend, nil - } else if config.infuraAPIKey != "" && config.contractBackend == nil && config.alchemyAPIKey == "" { - tmpl, found := client.InfuraURLs[config.chain.ID] - if !found { - return nil, fmt.Errorf("chain id %v not supported for Infura", config.chain.ID) - } - return ethclient.DialContext(ctx, fmt.Sprintf(tmpl, config.infuraAPIKey)) - } else if config.alchemyAPIKey != "" && config.contractBackend == nil && config.infuraAPIKey == "" { - tmpl, found := client.AlchemyURLs[config.chain.ID] - if !found { - return nil, fmt.Errorf("chain id %v not supported for Alchemy", config.chain.ID) - } - return ethclient.DialContext(ctx, fmt.Sprintf(tmpl, config.alchemyAPIKey)) - } else if config.local { - url, found := client.LocalURLs[config.chain.ID] - if !found { - return nil, fmt.Errorf("chain id %v not supported for Local", config.chain.ID) - } - return ethclient.DialContext(ctx, url) - } - return nil, errors.New("no provider specified, must provide an Infura API key, Alchemy API key, or an ETH backend") -} diff --git a/pkg/client/legacy/client_test.go b/pkg/client/legacy/client_test.go deleted file mode 100644 index d2ca915a..00000000 --- a/pkg/client/legacy/client_test.go +++ /dev/null @@ -1,209 +0,0 @@ -package clientlegacy - -import ( - "context" - "fmt" - "testing" - "time" - - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/crypto" - "github.com/stretchr/testify/require" - "github.com/textileio/go-tableland/pkg/client" - "github.com/textileio/go-tableland/tests/fullstack" -) - -func TestCreate(t *testing.T) { - t.Parallel() - - calls := setup(t) - requireCreate(t, calls) -} - -func TestList(t *testing.T) { - t.Parallel() - - calls := setup(t) - requireCreate(t, calls) - res := calls.list() - require.Len(t, res, 1) -} - -func TestRelayWrite(t *testing.T) { - t.Parallel() - - calls := setup(t) - _, table := requireCreate(t, calls) - requireWrite(t, calls, table, WriteRelay(true)) -} - -func TestDirectWrite(t *testing.T) { - t.Parallel() - - calls := setup(t) - _, table := requireCreate(t, calls) - requireWrite(t, calls, table, WriteRelay(false)) -} - -func TestRead(t *testing.T) { - t.Parallel() - - calls := setup(t) - _, table := requireCreate(t, calls) - hash := requireWrite(t, calls, table) - requireReceipt(t, calls, hash, WaitFor(time.Second*10)) - - type result struct { - Bar string `json:"bar"` - } - - res0 := []result{} - calls.read(fmt.Sprintf("select * from %s", table), &res0) - require.Len(t, res0, 1) - require.Equal(t, "baz", res0[0].Bar) - - res1 := map[string]interface{}{} - calls.read(fmt.Sprintf("select * from %s", table), &res1, ReadOutput(Table)) - require.Len(t, res1, 2) - - res2 := result{} - calls.read(fmt.Sprintf("select * from %s", table), &res2, ReadUnwrap()) - require.Equal(t, "baz", res2.Bar) - - res3 := []string{} - calls.read(fmt.Sprintf("select * from %s", table), &res3, ReadExtract()) - require.Len(t, res3, 1) - require.Equal(t, "baz", res3[0]) - - res4 := "" - calls.read(fmt.Sprintf("select * from %s", table), &res4, ReadUnwrap(), ReadExtract()) - require.Equal(t, "baz", res4) -} - -func TestHash(t *testing.T) { - t.Parallel() - - calls := setup(t) - hash := calls.hash("create table foo_1337 (bar text)") - require.NotEmpty(t, hash) -} - -func TestValidate(t *testing.T) { - t.Parallel() - - calls := setup(t) - id, table := requireCreate(t, calls) - res := calls.validate(fmt.Sprintf("insert into %s (bar) values ('hi')", table)) - require.Equal(t, id, res) -} - -func TestSetController(t *testing.T) { - t.Parallel() - - calls := setup(t) - tableID, _ := requireCreate(t, calls) - key, err := crypto.GenerateKey() - require.NoError(t, err) - controller := common.HexToAddress(crypto.PubkeyToAddress(key.PublicKey).Hex()) - hash := calls.setController(controller, tableID) - require.NotEmpty(t, hash) -} - -func requireCreate(t *testing.T, calls clientCalls) (TableID, string) { - id, table := calls.create("(bar text)", WithPrefix("foo"), WithReceiptTimeout(time.Second*10)) - require.Equal(t, "foo_1337_1", table) - return id, table -} - -func requireWrite(t *testing.T, calls clientCalls, table string, opts ...WriteOption) string { - hash := calls.write(fmt.Sprintf("insert into %s (bar) values('baz')", table), opts...) - require.NotEmpty(t, hash) - return hash -} - -func requireReceipt(t *testing.T, calls clientCalls, hash string, opts ...ReceiptOption) *TxnReceipt { - res, found := calls.receipt(hash, opts...) - require.True(t, found) - require.NotNil(t, res) - return res -} - -type clientCalls struct { - list func() []TableInfo - create func(schema string, opts ...CreateOption) (TableID, string) - read func(query string, target interface{}, opts ...ReadOption) - write func(query string, opts ...WriteOption) string - hash func(statement string) string - validate func(statement string) TableID - receipt func(txnHash string, options ...ReceiptOption) (*TxnReceipt, bool) - setController func(controller common.Address, tableID TableID) string -} - -func setup(t *testing.T) clientCalls { - stack := fullstack.CreateFullStack(t, fullstack.Deps{}) - - c := client.Chain{ - Endpoint: stack.Server.URL, - ID: client.ChainID(fullstack.ChainID), - ContractAddr: stack.Address, - } - - client, err := NewClient( - context.Background(), - stack.Wallet, - NewClientChain(c), - NewClientContractBackend(stack.Backend)) - require.NoError(t, err) - t.Cleanup(func() { - client.Close() - }) - - ctx := context.Background() - return clientCalls{ - list: func() []TableInfo { - res, err := client.List(ctx) - require.NoError(t, err) - return res - }, - create: func(schema string, opts ...CreateOption) (TableID, string) { - go func() { - time.Sleep(time.Second * 1) - stack.Backend.Commit() - }() - id, table, err := client.Create(ctx, schema, opts...) - require.NoError(t, err) - return id, table - }, - read: func(query string, target interface{}, opts ...ReadOption) { - err := client.Read(ctx, query, target, opts...) - require.NoError(t, err) - }, - write: func(query string, opts ...WriteOption) string { - hash, err := client.Write(ctx, query, opts...) - require.NoError(t, err) - stack.Backend.Commit() - return hash - }, - hash: func(statement string) string { - hash, err := client.Hash(ctx, statement) - require.NoError(t, err) - return hash - }, - validate: func(statement string) TableID { - tableID, err := client.Validate(ctx, statement) - require.NoError(t, err) - return tableID - }, - receipt: func(txnHash string, options ...ReceiptOption) (*TxnReceipt, bool) { - receipt, found, err := client.Receipt(ctx, txnHash, options...) - require.NoError(t, err) - return receipt, found - }, - setController: func(controller common.Address, tableID TableID) string { - hash, err := client.SetController(ctx, controller, tableID) - require.NoError(t, err) - stack.Backend.Commit() - return hash - }, - } -} diff --git a/pkg/client/v1/client.go b/pkg/client/v1/client.go index 674779bc..6d204c48 100644 --- a/pkg/client/v1/client.go +++ b/pkg/client/v1/client.go @@ -11,7 +11,6 @@ import ( "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/ethclient" - systemimpl "github.com/textileio/go-tableland/internal/system/impl" "github.com/textileio/go-tableland/internal/tableland" "github.com/textileio/go-tableland/pkg/client" "github.com/textileio/go-tableland/pkg/nonce/impl" @@ -109,8 +108,8 @@ func NewClient(ctx context.Context, wallet *wallet.Wallet, opts ...NewClientOpti parser, err := parserimpl.New([]string{ "sqlite_", - systemimpl.SystemTablesPrefix, - systemimpl.RegistryTableName, + parsing.SystemTablesPrefix, + parsing.RegistryTableName, }, parserOpts...) if err != nil { return nil, fmt.Errorf("new parser: %s", err) diff --git a/pkg/eventprocessor/impl/eventprocessor_test.go b/pkg/eventprocessor/impl/eventprocessor_test.go index 5385d765..d4d3b5c3 100644 --- a/pkg/eventprocessor/impl/eventprocessor_test.go +++ b/pkg/eventprocessor/impl/eventprocessor_test.go @@ -18,7 +18,6 @@ import ( parserimpl "github.com/textileio/go-tableland/pkg/parsing/impl" rsresolver "github.com/textileio/go-tableland/pkg/readstatementresolver" "github.com/textileio/go-tableland/pkg/sqlstore/impl/system" - "github.com/textileio/go-tableland/pkg/sqlstore/impl/user" "github.com/textileio/go-tableland/pkg/tables" "github.com/textileio/go-tableland/pkg/tables/impl/testutil" "github.com/textileio/go-tableland/tests" @@ -373,15 +372,17 @@ func setup(t *testing.T) ( } require.NoError(t, err) - userStore, err := user.New( - dbURI, rsresolver.New(map[tableland.ChainID]eventprocessor.EventProcessor{chainID: ep})) + store, err := system.New( + dbURI, 1337) require.NoError(t, err) + store.SetReadResolver(rsresolver.New(map[tableland.ChainID]eventprocessor.EventProcessor{chainID: ep})) + tableReader := func(readQuery string) []int64 { rq, err := parser.ValidateReadQuery(readQuery) require.NoError(t, err) require.NotNil(t, rq) - res, err := userStore.Read(ctx, rq) + res, err := store.Read(ctx, rq) require.NoError(t, err) ret := make([]int64, len(res.Rows)) diff --git a/pkg/parsing/query_validator.go b/pkg/parsing/query_validator.go index ce2cd322..fe64409e 100644 --- a/pkg/parsing/query_validator.go +++ b/pkg/parsing/query_validator.go @@ -10,6 +10,16 @@ import ( "github.com/textileio/go-tableland/pkg/tables" ) +var ( + // SystemTablesPrefix is the prefix used in table names that + // aren't owned by users, but the system. + SystemTablesPrefix = "system_" + + // RegistryTableName is a special system table (not owned by user) + // that has information about all tables owned by users. + RegistryTableName = "registry" +) + // MutatingStmt represents mutating statement, that is either // a SugaredWriteStmt or a SugaredGrantStmt. type MutatingStmt interface { diff --git a/pkg/siwe/siwe.go b/pkg/siwe/siwe.go deleted file mode 100644 index 6818908b..00000000 --- a/pkg/siwe/siwe.go +++ /dev/null @@ -1,53 +0,0 @@ -package siwe - -import ( - "encoding/base64" - "encoding/json" - "fmt" - "time" - - "github.com/ethereum/go-ethereum/common/hexutil" - "github.com/ethereum/go-ethereum/crypto" - "github.com/spruceid/siwe-go" - - "github.com/textileio/go-tableland/internal/tableland" - "github.com/textileio/go-tableland/pkg/wallet" -) - -// EncodedSIWEMsg returns the encoded SIWE msg string for the provided chainid and wallet. -func EncodedSIWEMsg(chainID tableland.ChainID, wallet *wallet.Wallet, validFor time.Duration) (string, error) { - opts := map[string]interface{}{ - "chainId": int(chainID), - "expirationTime": time.Now().UTC().Add(validFor).Format(time.RFC3339), - } - - msg, err := siwe.InitMessage( - "Tableland", - wallet.Address().Hex(), - "https://tableland.xyz", - siwe.GenerateNonce(), - opts, - ) - if err != nil { - return "", fmt.Errorf("initializing siwe message: %v", err) - } - - payload := fmt.Sprintf("\x19Ethereum Signed Message:\n%d%s", len(msg.String()), msg.String()) - hash := crypto.Keccak256Hash([]byte(payload)) - signature, err := crypto.Sign(hash.Bytes(), wallet.PrivateKey()) - if err != nil { - return "", fmt.Errorf("signing siwe message: %v", err) - } - signature[64] += 27 - - value := struct { - Message string `json:"message"` - Signature string `json:"signature"` - }{Message: msg.String(), Signature: hexutil.Encode(signature)} - json, err := json.Marshal(value) - if err != nil { - return "", fmt.Errorf("json marshaling signed siwe value: %v", err) - } - - return base64.StdEncoding.EncodeToString(json), nil -} diff --git a/pkg/sqlstore/impl/user/rowstotabledata.go b/pkg/sqlstore/impl/system/rowstotabledata.go similarity index 98% rename from pkg/sqlstore/impl/user/rowstotabledata.go rename to pkg/sqlstore/impl/system/rowstotabledata.go index 0847d6b4..eed05b03 100644 --- a/pkg/sqlstore/impl/user/rowstotabledata.go +++ b/pkg/sqlstore/impl/system/rowstotabledata.go @@ -1,4 +1,4 @@ -package user +package system import ( "database/sql" diff --git a/pkg/sqlstore/impl/system/store.go b/pkg/sqlstore/impl/system/store.go index 473c14fe..8488651f 100644 --- a/pkg/sqlstore/impl/system/store.go +++ b/pkg/sqlstore/impl/system/store.go @@ -11,6 +11,7 @@ import ( "github.com/XSAM/otelsql" "github.com/ethereum/go-ethereum/common" "github.com/google/uuid" + "github.com/rs/zerolog" logger "github.com/rs/zerolog/log" "github.com/tablelandnetwork/sqlparser" @@ -24,6 +25,7 @@ import ( "github.com/textileio/go-tableland/pkg/eventprocessor" "github.com/textileio/go-tableland/pkg/metrics" "github.com/textileio/go-tableland/pkg/nonce" + "github.com/textileio/go-tableland/pkg/parsing" "github.com/textileio/go-tableland/pkg/sqlstore" "github.com/textileio/go-tableland/pkg/sqlstore/impl/system/internal/db" "github.com/textileio/go-tableland/pkg/sqlstore/impl/system/migrations" @@ -39,6 +41,7 @@ type SystemStore struct { chainID tableland.ChainID dbWithTx dbWithTx db *sql.DB + resolver sqlparser.ReadStatementResolver } // New returns a new SystemStore backed by database/sql. @@ -79,6 +82,11 @@ func New(dbURI string, chainID tableland.ChainID) (*SystemStore, error) { return systemStore, nil } +// SetReadResolver sets the resolver for read queries. +func (s *SystemStore) SetReadResolver(resolver sqlparser.ReadStatementResolver) { + s.resolver = resolver +} + // GetTable fetchs a table from its UUID. func (s *SystemStore) GetTable(ctx context.Context, id tables.TableID) (sqlstore.Table, error) { table, err := s.dbWithTx.queries().GetTable(ctx, db.GetTableParams{ @@ -303,6 +311,33 @@ func (s *SystemStore) GetID(ctx context.Context) (string, error) { return id, err } +// Read executes a read statement on the db. +func (s *SystemStore) Read(ctx context.Context, rq parsing.ReadStmt) (*tableland.TableData, error) { + query, err := rq.GetQuery(s.resolver) + if err != nil { + return nil, fmt.Errorf("get query: %s", err) + } + ret, err := s.execReadQuery(ctx, s.db, query) + if err != nil { + return nil, fmt.Errorf("parsing result to json: %s", err) + } + + return ret, nil +} + +func (s *SystemStore) execReadQuery(ctx context.Context, tx *sql.DB, q string) (*tableland.TableData, error) { + rows, err := tx.QueryContext(ctx, q) + if err != nil { + return nil, fmt.Errorf("executing query: %s", err) + } + defer func() { + if err = rows.Close(); err != nil { + s.log.Warn().Err(err).Msg("closing rows") + } + }() + return rowsToTableData(rows) +} + // WithTx returns a copy of the current SystemStore with a tx attached. func (s *SystemStore) WithTx(tx *sql.Tx) sqlstore.SystemStore { return &SystemStore{ diff --git a/pkg/sqlstore/impl/system_store_instrumented.go b/pkg/sqlstore/impl/system/store_instrumented.go similarity index 94% rename from pkg/sqlstore/impl/system_store_instrumented.go rename to pkg/sqlstore/impl/system/store_instrumented.go index 1d5361d5..1c5868a6 100644 --- a/pkg/sqlstore/impl/system_store_instrumented.go +++ b/pkg/sqlstore/impl/system/store_instrumented.go @@ -1,4 +1,4 @@ -package impl +package system import ( "context" @@ -8,10 +8,12 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/rs/zerolog/log" + "github.com/tablelandnetwork/sqlparser" "github.com/textileio/go-tableland/internal/tableland" "github.com/textileio/go-tableland/pkg/eventprocessor" "github.com/textileio/go-tableland/pkg/metrics" "github.com/textileio/go-tableland/pkg/nonce" + "github.com/textileio/go-tableland/pkg/parsing" "github.com/textileio/go-tableland/pkg/sqlstore" "github.com/textileio/go-tableland/pkg/tables" "go.opentelemetry.io/otel/attribute" @@ -47,6 +49,11 @@ func NewInstrumentedSystemStore(chainID tableland.ChainID, store sqlstore.System }, nil } +// SetReadResolver sets the resolver for read queries. +func (s *InstrumentedSystemStore) SetReadResolver(resolver sqlparser.ReadStatementResolver) { + s.store.SetReadResolver(resolver) +} + // GetTable fetchs a table from its UUID. func (s *InstrumentedSystemStore) GetTable(ctx context.Context, id tables.TableID) (sqlstore.Table, error) { start := time.Now() @@ -411,3 +418,20 @@ func (s *InstrumentedSystemStore) GetID(ctx context.Context) (string, error) { return id, err } + +// Read executes a read statement on the db. +func (s *InstrumentedSystemStore) Read(ctx context.Context, stmt parsing.ReadStmt) (*tableland.TableData, error) { + start := time.Now() + data, err := s.store.Read(ctx, stmt) + latency := time.Since(start).Milliseconds() + + attributes := append([]attribute.KeyValue{ + {Key: "method", Value: attribute.StringValue("Read")}, + {Key: "success", Value: attribute.BoolValue(err == nil)}, + }, metrics.BaseAttrs...) + + s.callCount.Add(ctx, 1, attributes...) + s.latencyHistogram.Record(ctx, latency, attributes...) + + return data, err +} diff --git a/pkg/sqlstore/store_user_test.go b/pkg/sqlstore/impl/system/store_test.go similarity index 58% rename from pkg/sqlstore/store_user_test.go rename to pkg/sqlstore/impl/system/store_test.go index 0052e6b8..115a5025 100644 --- a/pkg/sqlstore/store_user_test.go +++ b/pkg/sqlstore/impl/system/store_test.go @@ -1,14 +1,91 @@ -package sqlstore +package system import ( + "context" "encoding/json" "testing" "time" + "github.com/ethereum/go-ethereum/common" "github.com/stretchr/testify/require" "github.com/textileio/go-tableland/internal/tableland" + "github.com/textileio/go-tableland/tests" ) +func TestEVMEventPersistence(t *testing.T) { + t.Parallel() + + ctx := context.Background() + dbURI := tests.Sqlite3URI(t) + + chainID := tableland.ChainID(1337) + + store, err := New(dbURI, chainID) + require.NoError(t, err) + + testData := []tableland.EVMEvent{ + { + Address: common.HexToAddress("0x10"), + Topics: []byte(`["0x111,"0x122"]`), + Data: []byte("data1"), + BlockNumber: 1, + TxHash: common.HexToHash("0x11"), + TxIndex: 11, + BlockHash: common.HexToHash("0x12"), + Index: 12, + ChainID: chainID, + EventJSON: []byte("eventjson1"), + EventType: "Type1", + }, + { + Address: common.HexToAddress("0x20"), + Topics: []byte(`["0x211,"0x222"]`), + Data: []byte("data2"), + BlockNumber: 2, + TxHash: common.HexToHash("0x21"), + TxIndex: 11, + BlockHash: common.HexToHash("0x22"), + Index: 12, + ChainID: chainID, + EventJSON: []byte("eventjson2"), + EventType: "Type2", + }, + } + + // Check that AreEVMEventsPersisted for the future txn hashes aren't found. + for _, event := range testData { + exists, err := store.AreEVMEventsPersisted(ctx, event.TxHash) + require.NoError(t, err) + require.False(t, exists) + } + + err = store.SaveEVMEvents(ctx, testData) + require.NoError(t, err) + + // Check that AreEVMEventsPersisted for the future txn hashes are found, and the data matches. + for _, event := range testData { + exists, err := store.AreEVMEventsPersisted(ctx, event.TxHash) + require.NoError(t, err) + require.True(t, exists) + + events, err := store.GetEVMEvents(ctx, event.TxHash) + require.NoError(t, err) + require.Len(t, events, 1) + + require.Equal(t, events[0].Address, event.Address) + require.Equal(t, events[0].Topics, event.Topics) + require.Equal(t, events[0].Data, event.Data) + require.Equal(t, events[0].BlockNumber, event.BlockNumber) + require.Equal(t, events[0].TxHash, event.TxHash) + require.Equal(t, events[0].TxIndex, event.TxIndex) + require.Equal(t, events[0].BlockHash, event.BlockHash) + require.Equal(t, events[0].Index, event.Index) + require.Equal(t, events[0].ChainID, chainID) + require.Equal(t, events[0].EventJSON, event.EventJSON) + require.Equal(t, events[0].EventType, event.EventType) + } +} + func TestUserValue(t *testing.T) { uv := &tableland.ColumnValue{} diff --git a/pkg/sqlstore/impl/user/store.go b/pkg/sqlstore/impl/user/store.go deleted file mode 100644 index 0b12c969..00000000 --- a/pkg/sqlstore/impl/user/store.go +++ /dev/null @@ -1,74 +0,0 @@ -package user - -import ( - "context" - "database/sql" - "fmt" - - "github.com/XSAM/otelsql" - _ "github.com/mattn/go-sqlite3" // sqlite3 driver - logger "github.com/rs/zerolog/log" - "github.com/tablelandnetwork/sqlparser" - "github.com/textileio/go-tableland/internal/tableland" - "github.com/textileio/go-tableland/pkg/metrics" - "github.com/textileio/go-tableland/pkg/parsing" - "go.opentelemetry.io/otel/attribute" -) - -var log = logger.With().Str("component", "userstore").Logger() - -// UserStore provides access to the db store. -type UserStore struct { - db *sql.DB - resolver sqlparser.ReadStatementResolver -} - -// New creates a new UserStore. -func New(dbURI string, resolver sqlparser.ReadStatementResolver) (*UserStore, error) { - attrs := append([]attribute.KeyValue{attribute.String("name", "userstore")}, metrics.BaseAttrs...) - db, err := otelsql.Open("sqlite3", dbURI, otelsql.WithAttributes(attrs...)) - if err != nil { - return nil, fmt.Errorf("connecting to db: %s", err) - } - if err := otelsql.RegisterDBStatsMetrics(db, otelsql.WithAttributes(attrs...)); err != nil { - return nil, fmt.Errorf("registering dbstats: %s", err) - } - return &UserStore{ - db: db, - resolver: resolver, - }, nil -} - -// Read executes a read statement on the db. -func (db *UserStore) Read(ctx context.Context, rq parsing.ReadStmt) (*tableland.TableData, error) { - query, err := rq.GetQuery(db.resolver) - if err != nil { - return nil, fmt.Errorf("get query: %s", err) - } - ret, err := execReadQuery(ctx, db.db, query) - if err != nil { - return nil, fmt.Errorf("parsing result to json: %s", err) - } - return ret, nil -} - -// Close closes the store. -func (db *UserStore) Close() error { - if err := db.db.Close(); err != nil { - return fmt.Errorf("closing db: %s", err) - } - return nil -} - -func execReadQuery(ctx context.Context, tx *sql.DB, q string) (*tableland.TableData, error) { - rows, err := tx.QueryContext(ctx, q) - if err != nil { - return nil, fmt.Errorf("executing query: %s", err) - } - defer func() { - if err = rows.Close(); err != nil { - log.Warn().Err(err).Msg("closing rows") - } - }() - return rowsToTableData(rows) -} diff --git a/pkg/sqlstore/impl/user/store_test.go b/pkg/sqlstore/impl/user/store_test.go deleted file mode 100644 index efe72b75..00000000 --- a/pkg/sqlstore/impl/user/store_test.go +++ /dev/null @@ -1,66 +0,0 @@ -package user - -import ( - "context" - "database/sql" - "encoding/json" - "testing" - - _ "github.com/mattn/go-sqlite3" - "github.com/stretchr/testify/require" - "github.com/textileio/go-tableland/tests" -) - -func TestReadGeneralTypeCorrectness(t *testing.T) { - t.Parallel() - - db, err := sql.Open("sqlite3", tests.Sqlite3URI(t)) - require.NoError(t, err) - - ctx := context.Background() - - // INTEGER - { - data, err := execReadQuery(ctx, db, "SELECT cast(1 as INTEGER) one") - require.NoError(t, err) - b, err := json.Marshal(data) - require.NoError(t, err) - require.JSONEq(t, `{"columns":[{"name":"one"}],"rows":[[1]]}`, string(b)) - } - - // Two INTEGERs without cast. - { - data, err := execReadQuery(ctx, db, "SELECT 1 a, 2 b") - require.NoError(t, err) - b, err := json.Marshal(data) - require.NoError(t, err) - require.JSONEq(t, `{"columns":[{"name":"a"}, {"name":"b"}],"rows":[[1, 2]]}`, string(b)) - } - - // REAL - { - data, err := execReadQuery(ctx, db, "SELECT cast(1.2 as REAL) real") - require.NoError(t, err) - b, err := json.Marshal(data) - require.NoError(t, err) - require.JSONEq(t, `{"columns":[{"name":"real"}],"rows":[[1.2]]}`, string(b)) - } - - // TEXT - { - data, err := execReadQuery(ctx, db, "SELECT 'hello' text") - require.NoError(t, err) - b, err := json.Marshal(data) - require.NoError(t, err) - require.JSONEq(t, `{"columns":[{"name":"text"}],"rows":[["hello"]]}`, string(b)) - } - - // BLOB - { - data, err := execReadQuery(ctx, db, "SELECT cast(X'4141414141414141414141' as BLOB) blob") - require.NoError(t, err) - b, err := json.Marshal(data) - require.NoError(t, err) - require.JSONEq(t, `{"columns":[{"name":"blob"}],"rows":[["QUFBQUFBQUFBQUE="]]}`, string(b)) - } -} diff --git a/pkg/sqlstore/impl/user_store_instrumented.go b/pkg/sqlstore/impl/user_store_instrumented.go deleted file mode 100644 index 9fb5b373..00000000 --- a/pkg/sqlstore/impl/user_store_instrumented.go +++ /dev/null @@ -1,63 +0,0 @@ -package impl - -import ( - "context" - "fmt" - "time" - - "github.com/textileio/go-tableland/internal/tableland" - "github.com/textileio/go-tableland/pkg/metrics" - "github.com/textileio/go-tableland/pkg/parsing" - "github.com/textileio/go-tableland/pkg/sqlstore" - "go.opentelemetry.io/otel/attribute" - "go.opentelemetry.io/otel/metric/global" - "go.opentelemetry.io/otel/metric/instrument" -) - -// InstrumentedUserStore implements a instrumented SQLStore. -type InstrumentedUserStore struct { - store sqlstore.UserStore - callCount instrument.Int64Counter - latencyHistogram instrument.Int64Histogram -} - -// NewInstrumentedUserStore creates a new db pool and instantiate user store. -func NewInstrumentedUserStore(store sqlstore.UserStore) (sqlstore.UserStore, error) { - meter := global.MeterProvider().Meter("tableland") - callCount, err := meter.Int64Counter("tableland.sqlstore.call.count") - if err != nil { - return &InstrumentedUserStore{}, fmt.Errorf("registering call counter: %s", err) - } - latencyHistogram, err := meter.Int64Histogram("tableland.sqlstore.call.latency") - if err != nil { - return &InstrumentedUserStore{}, fmt.Errorf("registering latency histogram: %s", err) - } - - return &InstrumentedUserStore{ - store: store, - callCount: callCount, - latencyHistogram: latencyHistogram, - }, nil -} - -// Read executes a read statement on the db. -func (s *InstrumentedUserStore) Read(ctx context.Context, stmt parsing.ReadStmt) (*tableland.TableData, error) { - start := time.Now() - data, err := s.store.Read(ctx, stmt) - latency := time.Since(start).Milliseconds() - - attributes := append([]attribute.KeyValue{ - {Key: "method", Value: attribute.StringValue("Read")}, - {Key: "success", Value: attribute.BoolValue(err == nil)}, - }, metrics.BaseAttrs...) - - s.callCount.Add(ctx, 1, attributes...) - s.latencyHistogram.Record(ctx, latency, attributes...) - - return data, err -} - -// Close closes the store. -func (s *InstrumentedUserStore) Close() error { - return s.store.Close() -} diff --git a/pkg/sqlstore/store_system.go b/pkg/sqlstore/store.go similarity index 88% rename from pkg/sqlstore/store_system.go rename to pkg/sqlstore/store.go index 2df06a57..f9bbe9e9 100644 --- a/pkg/sqlstore/store_system.go +++ b/pkg/sqlstore/store.go @@ -5,14 +5,18 @@ import ( "database/sql" "github.com/ethereum/go-ethereum/common" + "github.com/tablelandnetwork/sqlparser" "github.com/textileio/go-tableland/internal/tableland" "github.com/textileio/go-tableland/pkg/eventprocessor" "github.com/textileio/go-tableland/pkg/nonce" + "github.com/textileio/go-tableland/pkg/parsing" "github.com/textileio/go-tableland/pkg/tables" ) // SystemStore defines the methods for interacting with system-wide data. type SystemStore interface { + Read(context.Context, parsing.ReadStmt) (*tableland.TableData, error) + GetTable(context.Context, tables.TableID) (Table, error) GetTablesByController(context.Context, string) ([]Table, error) @@ -39,5 +43,8 @@ type SystemStore interface { Begin(context.Context) (*sql.Tx, error) WithTx(tx *sql.Tx) SystemStore + + SetReadResolver(resolver sqlparser.ReadStatementResolver) + Close() error } diff --git a/pkg/sqlstore/store_user.go b/pkg/sqlstore/store_user.go deleted file mode 100644 index 49209dfe..00000000 --- a/pkg/sqlstore/store_user.go +++ /dev/null @@ -1,14 +0,0 @@ -package sqlstore - -import ( - "context" - - "github.com/textileio/go-tableland/internal/tableland" - "github.com/textileio/go-tableland/pkg/parsing" -) - -// UserStore defines the methods for interacting with user data. -type UserStore interface { - Read(context.Context, parsing.ReadStmt) (*tableland.TableData, error) - Close() error -} diff --git a/tests/fullstack/fullstack.go b/tests/fullstack/fullstack.go index f74ea1fd..dd826b28 100644 --- a/tests/fullstack/fullstack.go +++ b/tests/fullstack/fullstack.go @@ -14,23 +14,18 @@ import ( "github.com/ethereum/go-ethereum/crypto" "github.com/stretchr/testify/require" "github.com/textileio/go-tableland/internal/chains" + "github.com/textileio/go-tableland/internal/gateway" "github.com/textileio/go-tableland/internal/router" - "github.com/textileio/go-tableland/internal/system" - systemimpl "github.com/textileio/go-tableland/internal/system/impl" "github.com/textileio/go-tableland/internal/tableland" "github.com/textileio/go-tableland/internal/tableland/impl" - "github.com/textileio/go-tableland/pkg/eventprocessor" "github.com/textileio/go-tableland/pkg/eventprocessor/eventfeed" efimpl "github.com/textileio/go-tableland/pkg/eventprocessor/eventfeed/impl" epimpl "github.com/textileio/go-tableland/pkg/eventprocessor/impl" executor "github.com/textileio/go-tableland/pkg/eventprocessor/impl/executor/impl" - nonceimpl "github.com/textileio/go-tableland/pkg/nonce/impl" "github.com/textileio/go-tableland/pkg/parsing" parserimpl "github.com/textileio/go-tableland/pkg/parsing/impl" - rsresolver "github.com/textileio/go-tableland/pkg/readstatementresolver" "github.com/textileio/go-tableland/pkg/sqlstore" sqlstoreimplsystem "github.com/textileio/go-tableland/pkg/sqlstore/impl/system" - "github.com/textileio/go-tableland/pkg/sqlstore/impl/user" "github.com/textileio/go-tableland/pkg/tables" "github.com/textileio/go-tableland/pkg/tables/impl/ethereum" "github.com/textileio/go-tableland/pkg/tables/impl/testutil" @@ -54,13 +49,11 @@ type FullStack struct { // Deps holds possile dependencies that can optionally be provided to spin up the full stack. type Deps struct { - DBURI string - Parser parsing.SQLValidator - SystemStore sqlstore.SystemStore - UserStore sqlstore.UserStore - ACL tableland.ACL - Tableland tableland.Tableland - SystemService system.SystemService + DBURI string + Parser parsing.SQLValidator + SystemStore sqlstore.SystemStore + ACL tableland.ACL + GatewayService gateway.Gateway } // CreateFullStack creates a running validator with the provided dependencies, or defaults otherwise. @@ -91,15 +84,6 @@ func CreateFullStack(t *testing.T, deps Deps) FullStack { wallet, err := wallet.NewWallet(hex.EncodeToString(crypto.FromECDSA(sk))) require.NoError(t, err) - registry, err := ethereum.NewClient( - backend, - ChainID, - addr, - wallet, - nonceimpl.NewSimpleTracker(wallet, backend), - ) - require.NoError(t, err) - db, err := sql.Open("sqlite3", dbURI) require.NoError(t, err) db.SetMaxOpenConns(1) @@ -134,60 +118,43 @@ func CreateFullStack(t *testing.T, deps Deps) FullStack { chainStacks := map[tableland.ChainID]chains.ChainStack{ 1337: { - Store: systemStore, - Registry: registry, - AllowTransactionRelay: true, - EventProcessor: ep, + Store: systemStore, + EventProcessor: ep, }, } - tbl := deps.Tableland - if tbl == nil { - userStore := deps.UserStore - if userStore == nil { - userStore, err = user.New( - dbURI, - rsresolver.New(map[tableland.ChainID]eventprocessor.EventProcessor{1337: ep}), - ) - require.NoError(t, err) - } - tbl = impl.NewTablelandMesa(parser, userStore, chainStacks) - tbl, err = impl.NewInstrumentedTablelandMesa(tbl) - require.NoError(t, err) - } - stores := make(map[tableland.ChainID]sqlstore.SystemStore, len(chainStacks)) for chainID, stack := range chainStacks { stores[chainID] = stack.Store } - systemService := deps.SystemService - if systemService == nil { - systemService, err = systemimpl.NewSystemSQLStoreService( + gatewayService := deps.GatewayService + if gatewayService == nil { + gatewayService, err = gateway.NewGateway( + parser, stores, "https://testnets.tableland.network", "https://render.tableland.xyz", "https://render.tableland.xyz/anim", ) require.NoError(t, err) - systemService, err = systemimpl.NewInstrumentedSystemSQLStoreService(systemService) + gatewayService, err = gateway.NewInstrumentedGateway(gatewayService) require.NoError(t, err) } - router, err := router.ConfiguredRouter(tbl, systemService, 10, time.Second, []tableland.ChainID{ChainID}) + router, err := router.ConfiguredRouter(gatewayService, 10, time.Second, []tableland.ChainID{ChainID}) require.NoError(t, err) server := httptest.NewServer(router.Handler()) t.Cleanup(server.Close) return FullStack{ - Backend: backend, - Address: addr, - Contract: contract, - TransactOpts: transactOpts, - Wallet: wallet, - TblContractClient: registry, - Server: server, + Backend: backend, + Address: addr, + Contract: contract, + TransactOpts: transactOpts, + Wallet: wallet, + Server: server, } } @@ -202,7 +169,7 @@ func (acl *aclHalfMock) CheckPrivileges( id tables.TableID, op tableland.Operation, ) (bool, error) { - aclImpl := impl.NewACL(acl.sqlStore, nil) + aclImpl := impl.NewACL(acl.sqlStore) return aclImpl.CheckPrivileges(ctx, tx, controller, id, op) }