diff --git a/address.go b/address.go deleted file mode 100644 index dd53770..0000000 --- a/address.go +++ /dev/null @@ -1,5 +0,0 @@ -package coinbase - -type Address struct { - ID string `json:"id"` -} diff --git a/examples/build_staking_operation.go b/examples/build_staking_operation.go new file mode 100644 index 0000000..56f2fbb --- /dev/null +++ b/examples/build_staking_operation.go @@ -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()) + +} diff --git a/api_key.go b/pkg/auth/api_key.go similarity index 95% rename from api_key.go rename to pkg/auth/api_key.go index fd5d056..28261a8 100644 --- a/api_key.go +++ b/pkg/auth/api_key.go @@ -1,4 +1,4 @@ -package coinbase +package auth import ( "encoding/json" diff --git a/auth.go b/pkg/auth/auth.go similarity index 99% rename from auth.go rename to pkg/auth/auth.go index 06c9343..7175ca6 100644 --- a/auth.go +++ b/pkg/auth/auth.go @@ -1,4 +1,4 @@ -package coinbase +package auth import ( "crypto/rand" diff --git a/transport.go b/pkg/auth/transport.go similarity index 70% rename from transport.go rename to pkg/auth/transport.go index 0e0006b..4af2f04 100644 --- a/transport.go +++ b/pkg/auth/transport.go @@ -1,18 +1,18 @@ -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, } } @@ -20,7 +20,7 @@ func NewAuthedTransport(apiKey APIKey, transport http.RoundTripper) http.RoundTr // 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", diff --git a/pkg/coinbase/address.go b/pkg/coinbase/address.go new file mode 100644 index 0000000..ed37a18 --- /dev/null +++ b/pkg/coinbase/address.go @@ -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, + } +} diff --git a/client.go b/pkg/coinbase/client.go similarity index 84% rename from client.go rename to pkg/coinbase/client.go index 2a6cfb6..e3ac0cb 100644 --- a/client.go +++ b/pkg/coinbase/client.go @@ -4,6 +4,7 @@ import ( "net/http" "github.com/coinbase/coinbase-sdk-go/gen/client" + "github.com/coinbase/coinbase-sdk-go/pkg/auth" ) type Client struct { @@ -11,7 +12,7 @@ type Client struct { client *client.APIClient baseHTTPClient *http.Client - apiKey APIKey + apiKey auth.APIKey } type ClientOption func(*Client) error @@ -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 } @@ -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 } diff --git a/pkg/coinbase/staking_balance.go b/pkg/coinbase/staking_balance.go new file mode 100644 index 0000000..e69de29 diff --git a/pkg/coinbase/staking_operation.go b/pkg/coinbase/staking_operation.go new file mode 100644 index 0000000..473b871 --- /dev/null +++ b/pkg/coinbase/staking_operation.go @@ -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, + } +} diff --git a/pkg/coinbase/transaction.go b/pkg/coinbase/transaction.go new file mode 100644 index 0000000..a11e82b --- /dev/null +++ b/pkg/coinbase/transaction.go @@ -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, + } +}