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

Feature add staking orchestration bones #8

Merged
merged 3 commits into from
Aug 16, 2024
Merged
Show file tree
Hide file tree
Changes from 2 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
5 changes: 0 additions & 5 deletions address.go

This file was deleted.

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

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

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

func main() {
client, err := coinbase.NewClient(
coinbase.WithAPIKeyFromJSON("/Users/deangalvin/Downloads/cdp_api_key (3).json"),
deangalvin-cb marked this conversation as resolved.
Show resolved Hide resolved
)
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")
//req := address
// .BuildStakeOperaitonRequest(context.Background(), "eth", big.NewFloat(0.0001))
op, err := client.BuildStakeOperation(
context.Background(),
&coinbase.BuildStakeOperationRequest{
Address: address,
AssetId: "eth",
Amount: big.NewFloat(0.0001),
Options: map[string]string{
"mode": "default",
}

Check failure on line 38 in examples/build_staking_operation.go

View workflow job for this annotation

GitHub Actions / lint

missing ',' before newline in composite literal (typecheck)
}

Check failure on line 39 in examples/build_staking_operation.go

View workflow job for this annotation

GitHub Actions / lint

missing ',' before newline in argument list (typecheck)
)
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(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should this be private then if we want to direct our users to go through BuildStakeOperation , BuildUnstakeOperation, and BuildClaimStakeOperation ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Our other methods are public for this :)

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,
}
}
Loading