Skip to content

Commit

Permalink
Merge pull request #16 from coinbase/marcin/validator-features
Browse files Browse the repository at this point in the history
Add Validator features and test cases
  • Loading branch information
marcin-cb authored Aug 22, 2024
2 parents 5e71c92 + bcce09a commit 3b959ee
Show file tree
Hide file tree
Showing 8 changed files with 417 additions and 25 deletions.
9 changes: 7 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,19 @@ lint-fix:

.PHONY: test
test:
go test ./pkg/...
go test ./pkg/coinbase/... ./pkg/auth/...

.PHONY: test-coverage
test-coverage:
go test -coverprofile=coverage.out ./pkg/...
go test -coverprofile=coverage.out ./pkg/coinbase/... ./pkg/auth/...
go tool cover -html=coverage.out
open cover.html

.PHONY: mocks
mocks:
mockery --disable-version-string --name StakeAPI --keeptree --dir gen/client --output pkg/mocks
mockery --disable-version-string --name AssetsAPI --keeptree --dir gen/client --output pkg/mocks

.PHONY: docs
docs:
go doc -all
144 changes: 122 additions & 22 deletions pkg/coinbase/staking_operation.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@ package coinbase
import (
"context"
"crypto/ecdsa"
"encoding/base64"
"fmt"
"math/big"
"time"

"github.com/coinbase/coinbase-sdk-go/gen/client"
)
Expand All @@ -19,6 +21,12 @@ func WithStakingOperationMode(mode string) StakingOperationOption {
return WithStakingOperationOption("mode", mode)
}

// WithStakingOperationImmediate allows for the setting of the immediate flag
// specifically for Dedicated ETH Staking whether to immediate unstake or not. (i.e. `true` or `false`)
func WithStakingOperationImmediate(immediate string) StakingOperationOption {
return WithStakingOperationOption("immediate", immediate)
}

// WithStakingOperationOption allows for the passing of custom options
// to the staking operation, like `mode` or `withdrawal_address`.
func WithStakingOperationOption(optionKey string, optionValue string) StakingOperationOption {
Expand All @@ -27,6 +35,30 @@ func WithStakingOperationOption(optionKey string, optionValue string) StakingOpe
}
}

type waitOptions struct {
intervalSeconds int
timeoutSeconds int
}

// WaitOption allows for the passing of custom options to the wait function.
type WaitOption func(*waitOptions)

// WithWaitIntervalSeconds sets the interval in seconds to wait between
// polling the staking operation.
func WithWaitIntervalSeconds(intervalSeconds int) WaitOption {
return func(o *waitOptions) {
o.intervalSeconds = intervalSeconds
}
}

// WithWaitTimeoutSeconds sets the timeout in seconds to wait for the
// staking operation to complete.
func WithWaitTimeoutSeconds(timeoutSeconds int) WaitOption {
return func(o *waitOptions) {
o.timeoutSeconds = timeoutSeconds
}
}

// BuildStakingOperation will build an ephemeral staking operation based on
// the passed address, assetID, action, and amount.
func (c *Client) BuildStakingOperation(
Expand Down Expand Up @@ -99,16 +131,6 @@ func (c *Client) BuildClaimStakeOperation(
return c.BuildStakingOperation(ctx, address, assetID, "claim_stake", amount, o...)
}

// FetchExternalStakingOperation loads a staking operation from the API associated
// with an address.
func (c *Client) FetchExternalStakingOperation(ctx context.Context, address *Address, id string) (*StakingOperation, error) {
op, _, err := c.client.StakeAPI.GetExternalStakingOperation(ctx, address.NetworkID(), address.ID(), id).Execute()
if err != nil {
return nil, err
}
return newStakingOperationFromModel(op)
}

// StakingOperation represents a staking operation for
// a given action, asset, and amount.
type StakingOperation struct {
Expand All @@ -117,36 +139,36 @@ type StakingOperation struct {
}

// ID returns the StakingOperation ID
func (o *StakingOperation) ID() string {
return o.model.Id
func (s *StakingOperation) ID() string {
return s.model.Id
}

// NetworkID returns the StakingOperation network id
func (o *StakingOperation) NetworkID() string {
return o.model.NetworkId
func (s *StakingOperation) NetworkID() string {
return s.model.NetworkId
}

// AddressID returns the StakingOperation address id
func (o *StakingOperation) AddressID() string {
return o.model.AddressId
func (s *StakingOperation) AddressID() string {
return s.model.AddressId
}

// Status returns the StakingOperation status
func (o *StakingOperation) Status() string {
return o.model.Status
func (s *StakingOperation) Status() string {
return s.model.Status
}

// Transactions returns the transactions associated with
// the StakingOperation
func (o *StakingOperation) Transactions() []*Transaction {
return o.transactions
func (s *StakingOperation) Transactions() []*Transaction {
return s.transactions
}

// Sign will sign each transaction using the supplied key
// This will halt and return an error if any of the transactions
// fail to sign.
func (o *StakingOperation) Sign(k *ecdsa.PrivateKey) error {
for _, tx := range o.Transactions() {
func (s *StakingOperation) Sign(k *ecdsa.PrivateKey) error {
for _, tx := range s.Transactions() {
if !tx.IsSigned() {
err := tx.Sign(k)
if err != nil {
Expand All @@ -157,6 +179,84 @@ func (o *StakingOperation) Sign(k *ecdsa.PrivateKey) error {
return nil
}

func (s *StakingOperation) GetSignedVoluntaryExitMessages() ([]string, error) {
var signedVoluntaryExitMessages []string

stakingOperationMetadata := s.model.GetMetadata().ArrayOfSignedVoluntaryExitMessageMetadata

if s.model.Metadata == nil {
return []string{}, nil
}

for _, metadata := range *stakingOperationMetadata {
decodedMessage, err := base64.StdEncoding.DecodeString(metadata.SignedVoluntaryExit)
if err != nil {
return nil, fmt.Errorf("failed to decode signed voluntary exit message: %s", err)
}
signedVoluntaryExitMessages = append(signedVoluntaryExitMessages, string(decodedMessage))
}

return signedVoluntaryExitMessages, nil
}

func (c *Client) Wait(ctx context.Context, stakingOperation *StakingOperation, o ...WaitOption) (*StakingOperation, error) {

options := &waitOptions{
intervalSeconds: 5,
timeoutSeconds: 3600,
}

for _, f := range o {
f(options)
}

startTime := time.Now()

for time.Since(startTime).Seconds() < float64(options.timeoutSeconds) {
so, err := c.fetchExternalStakingOperation(ctx, stakingOperation)
if err != nil {
return stakingOperation, err
}
stakingOperation = so

if stakingOperation.isTerminalState() {
return stakingOperation, nil
}

if time.Since(startTime).Seconds() > float64(options.timeoutSeconds) {
return stakingOperation, fmt.Errorf("staking operation timed out")
}

time.Sleep(time.Duration(options.intervalSeconds) * time.Second)
}

return stakingOperation, fmt.Errorf("staking operation timed out")
}

// FetchExternalStakingOperation loads a staking operation from the API associated
// with an address.
func (c *Client) fetchExternalStakingOperation(ctx context.Context, stakingOperation *StakingOperation) (*StakingOperation, error) {
so, httpRes, err := c.client.StakeAPI.GetExternalStakingOperation(
ctx,
stakingOperation.NetworkID(),
stakingOperation.AddressID(),
stakingOperation.ID(),
).Execute()
if err != nil {
return nil, err
}

if httpRes.StatusCode != 200 {
return nil, fmt.Errorf("failed to fetch staking operation: %s", httpRes.Status)
}

return newStakingOperationFromModel(so)
}

func (s *StakingOperation) isTerminalState() bool {
return s.Status() == "complete" || s.Status() == "failed"
}

func newStakingOperationFromModel(m *client.StakingOperation) (*StakingOperation, error) {
if m == nil {
return nil, fmt.Errorf("staking operation model is nil")
Expand Down
Loading

0 comments on commit 3b959ee

Please sign in to comment.