Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

app/obolapi: create cluster launchpad URL (#2518) #2526

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 42 additions & 13 deletions app/obolapi/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import (
"bytes"
"context"
"fmt"
"io"
"net/http"
"net/url"
Expand All @@ -15,46 +16,74 @@
"github.com/obolnetwork/charon/cluster"
)

const (
// launchpadReturnPathFmt is the URL path format string at which one can find details for a given cluster lock hash.
launchpadReturnPathFmt = "/lock/0x%X/launchpad"
)

// New returns a new Client.
func New(url string) Client {
return Client{
baseURL: url,
func New(urlStr string) (Client, error) {
_, err := url.ParseRequestURI(urlStr) // check that urlStr is valid
if err != nil {
return Client{}, errors.Wrap(err, "could not parse Obol API URL")
}

return Client{
baseURL: urlStr,
}, nil
}

// Client is the REST client for obol-api requests.
type Client struct {
baseURL string // Base obol-api URL
}

// url returns a *url.URL from the baseURL stored in c.
// Will panic if somehow c.baseURL got corrupted, and it's not a valid URL anymore.
func (c Client) url() *url.URL {
baseURL, err := url.ParseRequestURI(c.baseURL)
if err != nil {
panic(errors.Wrap(err, "could not parse Obol API URL, this should never happen"))

Check warning on line 46 in app/obolapi/api.go

View check run for this annotation

Codecov / codecov/patch

app/obolapi/api.go#L46

Added line #L46 was not covered by tests
}

return baseURL
}

// PublishLock posts the lockfile to obol-api.
func (c Client) PublishLock(ctx context.Context, lock cluster.Lock) error {
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()

addr, err := url.JoinPath(c.baseURL, "lock")
if err != nil {
return errors.Wrap(err, "invalid address")
}

url, err := url.Parse(addr)
if err != nil {
return errors.Wrap(err, "invalid endpoint")
}
addr := c.url()
addr.Path = "lock"

b, err := lock.MarshalJSON()
if err != nil {
return errors.Wrap(err, "marshal lock")
}

err = httpPost(ctx, url, b)
err = httpPost(ctx, addr, b)
if err != nil {
return err
}

return nil
}

// LaunchpadURLForLock returns the Launchpad cluster dashboard page for a given lock, on the given
// Obol API client.
func (c Client) LaunchpadURLForLock(lock cluster.Lock) string {
lURL := c.url()

lURL.Path = launchpadURLPath(lock)

return lURL.String()
}

func launchpadURLPath(lock cluster.Lock) string {
return fmt.Sprintf(launchpadReturnPathFmt, lock.LockHash)
}

func httpPost(ctx context.Context, url *url.URL, b []byte) error {
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url.String(), bytes.NewReader(b))
if err != nil {
Expand Down
49 changes: 47 additions & 2 deletions app/obolapi/api_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@
package obolapi_test

import (
"bytes"
"context"
"encoding/json"
"io"
"net/http"
"net/http/httptest"
"net/url"
"testing"

"github.com/stretchr/testify/require"
Expand Down Expand Up @@ -45,8 +47,51 @@ func TestLockPublish(t *testing.T) {

lock, _, _ := cluster.NewForT(t, 3, 3, 4, 0, opts...)

cl := obolapi.New(srv.URL)
err := cl.PublishLock(ctx, lock)
cl, err := obolapi.New(srv.URL)
require.NoError(t, err)
err = cl.PublishLock(ctx, lock)
require.NoError(t, err)
})
}

func TestURLParsing(t *testing.T) {
t.Run("invalid url", func(t *testing.T) {
cl, err := obolapi.New("badURL")
require.Error(t, err)
require.Empty(t, cl)
})

t.Run("http url", func(t *testing.T) {
cl, err := obolapi.New("http://unsafe.today")
require.NoError(t, err)
require.NotEmpty(t, cl)
})

t.Run("https url", func(t *testing.T) {
cl, err := obolapi.New("https://safe.today")
require.NoError(t, err)
require.NotEmpty(t, cl)
})
}

func TestLaunchpadDashURL(t *testing.T) {
t.Run("produced url is what we expect", func(t *testing.T) {
cl, err := obolapi.New("https://safe.today")
require.NoError(t, err)
require.NotEmpty(t, cl)

result := cl.LaunchpadURLForLock(cluster.Lock{LockHash: bytes.Repeat([]byte{0x42}, 32)})

require.NotEmpty(t, result)

parsedRes, err := url.ParseRequestURI(result)
require.NoError(t, err)

require.Equal(t, "safe.today", parsedRes.Host)
require.Equal(
t,
"/lock/0x4242424242424242424242424242424242424242424242424242424242424242/launchpad",
parsedRes.Path,
)
})
}
30 changes: 23 additions & 7 deletions cmd/createcluster.go
Original file line number Diff line number Diff line change
Expand Up @@ -257,9 +257,14 @@
lock.NodeSignatures = append(lock.NodeSignatures, nodeSig)
}

// dashboardURL is the Launchpad dashboard url for a given lock file.
// If empty, either conf.Publish wasn't specified or there was a processing error in publishing
// the generated lock file.
var dashboardURL string

// Write cluster-lock file
if conf.Publish {
if err = writeLockToAPI(ctx, conf.PublishAddr, lock); err != nil {
if dashboardURL, err = writeLockToAPI(ctx, conf.PublishAddr, lock); err != nil {
log.Warn(ctx, "Couldn't publish lock file to Obol API", err)
}
}
Expand All @@ -272,7 +277,15 @@
writeWarning(w)
}

return writeOutput(w, conf.SplitKeys, conf.ClusterDir, numNodes, keysToDisk)
if err := writeOutput(w, conf.SplitKeys, conf.ClusterDir, numNodes, keysToDisk); err != nil {
return err
}

Check warning on line 282 in cmd/createcluster.go

View check run for this annotation

Codecov / codecov/patch

cmd/createcluster.go#L281-L282

Added lines #L281 - L282 were not covered by tests

if dashboardURL != "" {
log.Info(ctx, fmt.Sprintf("You can find your newly-created cluster dashboard here: %s", dashboardURL))
}

return nil
}

// validateCreateConfig returns an error if any of the provided config parameters are invalid.
Expand Down Expand Up @@ -943,17 +956,20 @@
return hex.EncodeToString(b), nil
}

// writeLockToAPI posts the lock file to obol-api.
func writeLockToAPI(ctx context.Context, publishAddr string, lock cluster.Lock) error {
cl := obolapi.New(publishAddr)
// writeLockToAPI posts the lock file to obol-api and returns the Launchpad dashboard URL.
func writeLockToAPI(ctx context.Context, publishAddr string, lock cluster.Lock) (string, error) {
cl, err := obolapi.New(publishAddr)
if err != nil {
return "", err
}

Check warning on line 964 in cmd/createcluster.go

View check run for this annotation

Codecov / codecov/patch

cmd/createcluster.go#L963-L964

Added lines #L963 - L964 were not covered by tests

if err := cl.PublishLock(ctx, lock); err != nil {
return err
return "", err

Check warning on line 967 in cmd/createcluster.go

View check run for this annotation

Codecov / codecov/patch

cmd/createcluster.go#L967

Added line #L967 was not covered by tests
}

log.Info(ctx, "Published lock file", z.Str("addr", publishAddr))

return nil
return cl.LaunchpadURLForLock(lock), nil
}

// validateAddresses checks if we have sufficient addresses. It also fills addresses slices if only one is provided.
Expand Down
24 changes: 18 additions & 6 deletions dkg/dkg.go
Original file line number Diff line number Diff line change
Expand Up @@ -304,8 +304,13 @@
log.Debug(ctx, "Saved keyshares to disk")
}

// dashboardURL is the Launchpad dashboard url for a given lock file.
// If empty, either conf.Publish wasn't specified or there was a processing error in publishing
// the generated lock file.
var dashboardURL string

if conf.Publish {
if err = writeLockToAPI(ctx, conf.PublishAddr, lock); err != nil {
if dashboardURL, err = writeLockToAPI(ctx, conf.PublishAddr, lock); err != nil {
log.Warn(ctx, "Couldn't publish lock file to Obol API", err)
}
}
Expand All @@ -329,6 +334,10 @@

log.Info(ctx, "Successfully completed DKG ceremony 🎉")

if dashboardURL != "" {
log.Info(ctx, fmt.Sprintf("You can find your newly-created cluster dashboard here: %s", dashboardURL))
}

return nil
}

Expand Down Expand Up @@ -966,17 +975,20 @@
return dvs, nil
}

// writeLockToAPI posts the lock file to obol-api.
func writeLockToAPI(ctx context.Context, publishAddr string, lock cluster.Lock) error {
cl := obolapi.New(publishAddr)
// writeLockToAPI posts the lock file to obol-api and returns the Launchpad dashboard URL.
func writeLockToAPI(ctx context.Context, publishAddr string, lock cluster.Lock) (string, error) {
cl, err := obolapi.New(publishAddr)
if err != nil {
return "", err
}

Check warning on line 983 in dkg/dkg.go

View check run for this annotation

Codecov / codecov/patch

dkg/dkg.go#L982-L983

Added lines #L982 - L983 were not covered by tests

if err := cl.PublishLock(ctx, lock); err != nil {
return err
return "", err

Check warning on line 986 in dkg/dkg.go

View check run for this annotation

Codecov / codecov/patch

dkg/dkg.go#L986

Added line #L986 was not covered by tests
}

log.Debug(ctx, "Published lock file to api")

return nil
return cl.LaunchpadURLForLock(lock), nil
}

// validateKeymanagerFlags returns an error if one keymanager flag is present but the other is not.
Expand Down
Loading