Skip to content

Commit

Permalink
Merge pull request #141 from Tenderly/feature/spawn-devnet-rpc-url
Browse files Browse the repository at this point in the history
add support for spawn-rpc api
  • Loading branch information
daleksov authored Apr 7, 2023
2 parents eb0c806 + 561aaf5 commit b6b8660
Show file tree
Hide file tree
Showing 9 changed files with 276 additions and 18 deletions.
1 change: 1 addition & 0 deletions buidler/buidler.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ func NewDeploymentProvider() *DeploymentProvider {
call.NewExportCalls(),
call.NewNetworkCalls(),
call.NewActionCalls(),
call.NewDevNetCalls(),
)

networks, err := rest.Networks.GetPublicNetworks()
Expand Down
15 changes: 15 additions & 0 deletions commands/devnet/devnet.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package devnet

import (
"github.com/spf13/cobra"
"github.com/tenderly/tenderly-cli/commands"
)

func init() {
commands.RootCmd.AddCommand(CmdDevNet)
}

var CmdDevNet = &cobra.Command{
Use: "devnet",
Short: "Tenderly DevNets.",
}
122 changes: 122 additions & 0 deletions commands/devnet/spawn_rpc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
package devnet

import (
"os"

"github.com/pkg/errors"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"github.com/tenderly/tenderly-cli/commands"
"github.com/tenderly/tenderly-cli/config"
"github.com/tenderly/tenderly-cli/userError"
)

var accountID string
var projectSlug string
var templateSlug string
var accessKey string
var token string

func init() {
cmdSpawnRpc.PersistentFlags().StringVar(
&accountID,
"account",
"",
"The Tenderly account username or organization slug. If not provided, the system will try to read 'account_id' from the 'tenderly.yaml' configuration file.",
)
cmdSpawnRpc.PersistentFlags().StringVar(
&projectSlug,
"project",
"",
"The DevNet project slug. If not provided, the system will try to read 'project_slug' from the 'tenderly.yaml' configuration file.",
)
cmdSpawnRpc.PersistentFlags().StringVar(
&templateSlug,
"template",
"",
"The DevNet template slug which is going to be applied when spawning the DevNet RPC.",
)
cmdSpawnRpc.PersistentFlags().StringVar(
&accessKey,
"access_key",
"",
"The Tenderly access key. If not provided, the system will try to read 'access_key' from the 'tenderly.yaml' configuration file.",
)
cmdSpawnRpc.PersistentFlags().StringVar(
&token,
"token",
"",
"The Tenderly JWT. If not provided, the system will try to read 'token' from the 'tenderly.yaml' configuration file.",
)
CmdDevNet.AddCommand(cmdSpawnRpc)
}

var cmdSpawnRpc = &cobra.Command{
Use: "spawn-rpc",
Short: "Spawn DevNet RPC",
Long: `Spawn DevNet RPC that represents the JSON-RPC endpoint for your DevNet`,
Run: spawnRPCHandler,
}

func spawnRPCHandler(cmd *cobra.Command, args []string) {
if accountID == "" {
commands.CheckLogin()
accountID = config.GetGlobalString(config.AccountID)
}

if accountID == "" {
err := userError.NewUserError(
errors.New("account not found"),
"An account is required. Please log in using tenderly login or include the '--account' flag.",
)
userError.LogError(err)
os.Exit(1)
}

if accessKey == "" {
accessKey = config.GetGlobalString(config.AccessKey)
}

if token == "" {
token = config.GetGlobalString(config.Token)
}

if accessKey == "" && token == "" {
err := userError.NewUserError(
errors.New("access key or token not found"),
"An access key or token is required. Please log in using tenderly login or include the '--access_key' or '--token' flag.",
)
userError.LogError(err)
os.Exit(1)
}

if projectSlug == "" {
projectSlug = config.MaybeGetString(config.ProjectSlug)
}

if projectSlug == "" {
err := userError.NewUserError(
errors.New("project not found"),
"No project was found. To set a project, use tenderly use project <project-slug> or include the '--project' flag.",
)
userError.LogError(err)
os.Exit(1)
}

if templateSlug == "" {
err := userError.NewUserError(
errors.New("Missing required argument 'template'"),
"The 'template' argument is required. Please include the '--template' flag and provide the DevNet template slug.",
)
userError.LogError(err)
os.Exit(1)
}

rest := commands.NewRest()
response, err := rest.DevNet.SpawnRPC(accountID, projectSlug, templateSlug, accessKey, token)
if err != nil {
logrus.Error("Failed to spawn RPC", err)
return
}
logrus.Info(response)
}
1 change: 1 addition & 0 deletions commands/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ func NewRest() *rest.Rest {
call.NewExportCalls(),
call.NewNetworkCalls(),
call.NewActionCalls(),
call.NewDevNetCalls(),
)
}

Expand Down
1 change: 1 addition & 0 deletions hardhat/hardhat.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ func NewDeploymentProvider() *DeploymentProvider {
call.NewExportCalls(),
call.NewNetworkCalls(),
call.NewActionCalls(),
call.NewDevNetCalls(),
)

networks, err := rest.Networks.GetPublicNetworks()
Expand Down
1 change: 1 addition & 0 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
// THIS IS HOW WE SUBSCRIBE NESTED COMMANDS
_ "github.com/tenderly/tenderly-cli/commands/actions"
_ "github.com/tenderly/tenderly-cli/commands/contract"
_ "github.com/tenderly/tenderly-cli/commands/devnet"
_ "github.com/tenderly/tenderly-cli/commands/export"
)

Expand Down
57 changes: 57 additions & 0 deletions rest/call/devnet.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package call

import (
"encoding/json"
"fmt"
"github.com/tenderly/tenderly-cli/config"
"github.com/tenderly/tenderly-cli/rest"
"github.com/tenderly/tenderly-cli/rest/client"
"github.com/tenderly/tenderly-cli/rest/payloads"
)

var _ rest.DevNetRoutes = (*DevNetCalls)(nil)

type DevNetCalls struct{}

func NewDevNetCalls() *DevNetCalls {
return &DevNetCalls{}
}

type SpawnRPCRequest struct {
Template string `json:"templateSlugOrId"`
}

func (rest *DevNetCalls) SpawnRPC(
accountID string,
projectID string,
templateSlug string,
accessKey string,
token string,
) (string, error) {
req := &SpawnRPCRequest{
Template: templateSlug,
}
reqJson, err := json.Marshal(req)
if err != nil {
return "", err
}
config.SetProjectConfig(config.AccessKey, accessKey)
config.SetProjectConfig(config.Token, token)
path := fmt.Sprintf("api/v1/account/%s/project/%s/devnet/container/spawn-rpc", accountID, projectID)
resp := client.Request(
"POST",
path,
reqJson,
)
var response *SpawnRPCResponse
err = json.NewDecoder(resp).Decode(&response)
if err != nil {
return "", err
}
return response.URL, err
}

type SpawnRPCResponse struct {
URL string `json:"url"`
Error *payloads.ApiError `json:"error"`
}
89 changes: 71 additions & 18 deletions rest/client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,40 +5,39 @@ import (
"crypto/tls"
"encoding/json"
"fmt"
"github.com/tenderly/tenderly-cli/rest/payloads"
"io"
"net/http"
"os"
"strings"

"github.com/sirupsen/logrus"
"github.com/tenderly/tenderly-cli/config"
"github.com/tenderly/tenderly-cli/rest/payloads"
"github.com/tenderly/tenderly-cli/userError"
)

const sessionLimitErrorSlug = "session_limit_exceeded"
const (
sessionLimitErrorSlug = "session_limit_exceeded"
defaultApiBaseURL = "https://api.tenderly.co"
)

func Request(method, path string, body []byte) io.Reader {
apiBase := "https://api.tenderly.co"
if alternativeApiBase := config.MaybeGetString("api_base"); len(alternativeApiBase) != 0 {
apiBase = alternativeApiBase
}

requestUrl := fmt.Sprintf("%s/%s", apiBase, strings.TrimPrefix(path, "/"))
apiBaseURL := resolveApiBaseURL()
requestURL := resolveRequestURL(apiBaseURL, path)
req, err := http.NewRequest(
method,
requestUrl,
requestURL,
bytes.NewReader(body),
)
if err != nil {
userError.LogErrorf("failed creating request: %s", userError.NewUserError(
userError.LogErrorf("failed to create request: %s", userError.NewUserError(
err,
"Failed creating request. Please try again.",
"Failed to create request. Please try again.",
))
os.Exit(1)
}

http.DefaultTransport.(*http.Transport).TLSClientConfig = &tls.Config{InsecureSkipVerify: false}
ensureTLS()

if key := config.GetAccessKey(); key != "" {
// set access key
Expand All @@ -48,7 +47,7 @@ func Request(method, path string, body []byte) io.Reader {
req.Header.Add("Authorization", "Bearer "+token)

urlPath := fmt.Sprintf("api/v1/account/%s/token", config.GetAccountId())
if requestUrl != fmt.Sprintf("%s/%s", apiBase, urlPath) {
if requestURL != fmt.Sprintf("%s/%s", apiBaseURL, urlPath) {
var request payloads.GenerateAccessTokenRequest
request.Name = "CLI access token"

Expand Down Expand Up @@ -110,10 +109,12 @@ func Request(method, path string, body []byte) io.Reader {
}
}

logrus.WithFields(logrus.Fields{
"request_url": requestUrl,
"request_body": string(body),
}).Debug("Making request")
logrus.WithFields(
logrus.Fields{
"request_url": requestURL,
"request_body": string(body),
},
).Debug("Making request")

res, err := http.DefaultClient.Do(req)
if err != nil {
Expand All @@ -123,9 +124,13 @@ func Request(method, path string, body []byte) io.Reader {
))
os.Exit(1)
}
defer func() {
_ = res.Body.Close()
}()

handleResponseStatus(res, err)

data, err := io.ReadAll(res.Body)
defer res.Body.Close()

if err != nil {
userError.LogErrorf("failed reading response body: %s", userError.NewUserError(
Expand All @@ -139,3 +144,51 @@ func Request(method, path string, body []byte) io.Reader {

return bytes.NewReader(data)
}

// handleResponseStatus handles the response status code.
func handleResponseStatus(res *http.Response, err error) {
if res.StatusCode < 200 || res.StatusCode >= 300 {
if res.StatusCode >= 500 {
userError.LogErrorf("request failed: %s", userError.NewUserError(
err,
fmt.Sprintf(
"The request failed with a status code of %d and status message of '%s'. Please try again.",
res.StatusCode,
res.Status,
),
))
} else {
userError.LogErrorf("request failed: %s", userError.NewUserError(
err,
fmt.Sprintf(
"The request failed with a status code of %d and status message of '%s'",
res.StatusCode,
res.Status,
),
))
}
os.Exit(1)
}
}

// ensureTLS configures the default http transport to use TLS.
func ensureTLS() {
http.DefaultTransport.(*http.Transport).TLSClientConfig = &tls.Config{
InsecureSkipVerify: false,
}
}

// resolveRequestURL resolves the request URL based on the API base URL and the path.
func resolveRequestURL(apiBaseURL string, path string) string {
requestURL := fmt.Sprintf("%s/%s", apiBaseURL, strings.TrimPrefix(path, "/"))
return requestURL
}

// resolveApiBaseURL resolves the API base URL based on the config.
func resolveApiBaseURL() string {
apiBase := defaultApiBaseURL
if apiBaseOverride := config.MaybeGetString("api_base"); len(apiBaseOverride) != 0 {
apiBase = apiBaseOverride
}
return apiBase
}
Loading

0 comments on commit b6b8660

Please sign in to comment.