Skip to content

Commit

Permalink
Merge pull request #8 from coinbase/feature/add-staking-orchestration
Browse files Browse the repository at this point in the history
Feature add staking orchestration bones
  • Loading branch information
marcin-cb authored Aug 16, 2024
2 parents f39050c + 776307b commit 11cbb51
Show file tree
Hide file tree
Showing 10 changed files with 287 additions and 16 deletions.
5 changes: 0 additions & 5 deletions address.go

This file was deleted.

51 changes: 51 additions & 0 deletions examples/build_staking_operation.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package main

import (
"context"
"log"
"math/big"

"github.com/coinbase/coinbase-sdk-go/pkg/coinbase"
)

func main() {
client, err := coinbase.NewClient(
coinbase.WithAPIKeyFromJSON("api_key.json"),
)
if err != nil {
log.Fatalf("error creating coinbase client: %v", err)
}
address := client.NewAddress("ethereum-holesky", "0x57a063e1df096aaA6b2068C3C7FE6Ac4BC3c4F58")
op, err := address.BuildStakeOperaiton(context.Background(), "eth", big.NewFloat(0.0001))
if err != nil {
log.Fatalf("error building staking operation: %v", err)
}

log.Printf("staking operation ID: %s\n", op.ID())
log.Printf("staking operation Transactions: %+v\n", op.Transactions())
//
address := coinbase.NewAddress("ethereum-holesky", "0x57a063e1df096aaA6b2068C3C7FE6Ac4BC3c4F58")
op, err := client.BuildStakeOperation(
context.Background(),
&coinbase.BuildStakeOperationRequest{
Address: address,
AssetId: "eth",
Amount: big.NewFloat(0.0001),
Options: map[string]string{
"mode": "default",
}
}
)
rewardsIter, err := client.ListStakingRewards(
ctx, []coinbase.Address{address}, "eth", time.Now().Add(-time.Hour*24*7), time.Now(),
coinbase.WithStakingRewardsLimit(100),
coinbase.WithStakingRewardsPageOffset(2),
)
if err != nil {
log.Fatalf("error building staking operation: %v", err)
}

log.Printf("staking operation ID: %s\n", op.ID())
log.Printf("staking operation Transactions: %+v\n", op.Transactions())

}
2 changes: 1 addition & 1 deletion api_key.go → pkg/auth/api_key.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package coinbase
package auth

import (
"encoding/json"
Expand Down
2 changes: 1 addition & 1 deletion auth.go → pkg/auth/auth.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package coinbase
package auth

import (
"crypto/rand"
Expand Down
12 changes: 6 additions & 6 deletions transport.go → pkg/auth/transport.go
Original file line number Diff line number Diff line change
@@ -1,26 +1,26 @@
package coinbase
package auth

import (
"fmt"
"net/http"
)

type authedTransport struct {
type transport struct {
transport http.RoundTripper
apiKey APIKey
}

func NewAuthedTransport(apiKey APIKey, transport http.RoundTripper) http.RoundTripper {
return &authedTransport{
transport: transport,
func NewTransport(apiKey APIKey, t http.RoundTripper) http.RoundTripper {
return &transport{
transport: t,
apiKey: apiKey,
}
}

// RoundTrip implements the http.RoundTripper interface and wraps
// the base round tripper with logic to inject the API key auth-based HTTP headers
// into the request. Reference: https://pkg.go.dev/net/http#RoundTripper
func (t *authedTransport) RoundTrip(req *http.Request) (*http.Response, error) {
func (t *transport) RoundTrip(req *http.Request) (*http.Response, error) {
jwt, err := BuildJWT(
t.apiKey,
"cdp_service",
Expand Down
13 changes: 13 additions & 0 deletions pkg/coinbase/address.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package coinbase

type Address struct {
NetworkID string `json:"network_id"`
ID string `json:"id"`
}

func NewAddress(networkID string, ID string) *Address {
return &Address{
NetworkID: networkID,
ID: ID,
}
}
7 changes: 4 additions & 3 deletions client.go → pkg/coinbase/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,15 @@ import (
"net/http"

"github.com/coinbase/coinbase-sdk-go/gen/client"
"github.com/coinbase/coinbase-sdk-go/pkg/auth"
)

type Client struct {
cfg *client.Configuration
client *client.APIClient

baseHTTPClient *http.Client
apiKey APIKey
apiKey auth.APIKey
}

type ClientOption func(*Client) error
Expand All @@ -25,7 +26,7 @@ func WithBaseURL(baseURL string) ClientOption {

func WithAPIKeyFromJSON(fileName string) ClientOption {
return func(c *Client) error {
key, err := LoadAPIKeyFromFile(fileName)
key, err := auth.LoadAPIKeyFromFile(fileName)
if err != nil {
return err
}
Expand Down Expand Up @@ -58,7 +59,7 @@ func NewClient(o ...ClientOption) (*Client, error) {
if c.cfg.HTTPClient.Transport == nil {
c.cfg.HTTPClient.Transport = http.DefaultTransport
}
c.cfg.HTTPClient.Transport = NewAuthedTransport(c.apiKey, c.cfg.HTTPClient.Transport)
c.cfg.HTTPClient.Transport = auth.NewTransport(c.apiKey, c.cfg.HTTPClient.Transport)
c.client = client.NewAPIClient(c.cfg)
return c, nil
}
Empty file added pkg/coinbase/staking_balance.go
Empty file.
161 changes: 161 additions & 0 deletions pkg/coinbase/staking_operation.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
package coinbase

import (
"context"
"crypto/ecdsa"
"math/big"

"github.com/coinbase/coinbase-sdk-go/gen/client"
)

// StakeingOperationOption allows for the passing of custom options to
// the staking operation, like `mode` or `withdrawal_address`.
type StakingOperationOption func(*client.BuildStakingOperationRequest)

// WithStakingOperationMode allows for the setting of the mode of
// the staking operation (ie. `default`, `partial`, or `native`)
func WithStakingOperationMode(mode string) StakingOperationOption {
return WithStakingOperationOption("mode", mode)
}

// WithStakingOperationOption allows for the passing of custom options
// to the staking operation, like `mode` or `withdrawal_address`.
func WithStakingOperationOption(optionKey string, optionValue string) StakingOperationOption {
return func(op *client.BuildStakingOperationRequest) {
op.Options[optionKey] = optionValue
}
}

// BuildStakingOperation will build an ephemeral staking operation based on
// the passed address, assetID, action, and amount.
func (c *Client) BuildStakingOperation(
ctx context.Context,
address *Address,
assetID string,
action string,
amount *big.Float,
o ...StakingOperationOption,
) (*StakingOperation, error) {
req := client.BuildStakingOperationRequest{
NetworkId: address.NetworkID,
AssetId: assetID,
AddressId: address.ID,
Action: action,
Options: map[string]string{
"mode": "default",
"amount": amount.String(),
},
}
for _, f := range o {
f(&req)
}
op, _, err := c.client.StakeAPI.BuildStakingOperation(ctx).BuildStakingOperationRequest(req).Execute()
if err != nil {
return nil, err
}

return newStakingOperationFromModel(op), nil
}

// BuildStakeOperation will build an ephemeral staking operation using the
// stake action
func (c *Client) BuildStakeOperation(
ctx context.Context,
address *Address,
assetID string,
amount *big.Float,
o ...StakingOperationOption,
) (*StakingOperation, error) {
return c.BuildStakingOperation(ctx, address, assetID, "stake", amount, o...)
}

// BuildStakeOperation will build an ephemeral staking operation using the
// unstake action
func (c *Client) BuildUnstakeOperation(
ctx context.Context,
address *Address,
assetID string,
amount *big.Float,
o ...StakingOperationOption,
) (*StakingOperation, error) {
return c.BuildStakingOperation(ctx, address, assetID, "unstake", amount, o...)
}

// BuildStakeOperation will build an ephemeral staking operation using the
// claim_stake action
func (c *Client) BuildClaimStakeOperation(
ctx context.Context,
address *Address,
assetID string,
amount *big.Float,
o ...StakingOperationOption,
) (*StakingOperation, error) {
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 newStakingOpertionFromModel(op), nil
}

// StakingOperation represents a staking operation for
// a given action, asset, and amount.
type StakingOperation struct {
model *client.StakingOperation
transactions []*Transaction
}

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

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

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

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

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

// Sign will sign each transaction using the supplied key
func (o *StakingOperation) Sign(k *ecdsa.PrivateKey) error {
for _, tx := range o.Transactions() {
if !tx.IsSigned() {
tx.Sign(k)
}
}
}

func newStakingOperationFromModel(m *client.StakingOperation) *StakingOperation {
if m == nil {
return nil
}

transactions := make([]*Transaction, len(m.Transactions))
for i, tx := range m.Transactions {
transactions[i] = newTransactionFromModel(&tx)
}
return &StakingOperation{
model: m,
transactions: transactions,
}
}
50 changes: 50 additions & 0 deletions pkg/coinbase/transaction.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package coinbase

import "github.com/coinbase/coinbase-sdk-go/gen/client"

// Transaction represents an onchain transaction
type Transaction struct {
model *client.Transaction
}

// UnsignedPayload returns the unsigned payload of the transaction
func (t *Transaction) UnsignedPayload() string {
return t.model.UnsignedPayload
}

// SignedPayload returns the signed payload of the transaction
func (t *Transaction) SignedPayload() string {
if t.model.SignedPayload == nil {
return ""
}

return *t.model.SignedPayload
}

// TransactionHash returns the hash of the transaction
func (t *Transaction) TransactionHash() string {
if t.model.TransactionHash == nil {
return ""
}

return *t.model.TransactionHash
}

// Status returns the status of the Transaction
func (t *Transaction) Status() string {
return t.model.Status
}

// FromAddressID returns the from address for the transaction
func (t *Transaction) FromAddressID() string {
return t.model.FromAddressId
}

func newTransactionFromModel(m *client.Transaction) *Transaction {
if m == nil {
return nil
}
return &Transaction{
model: m,
}
}

0 comments on commit 11cbb51

Please sign in to comment.