Skip to content

Commit

Permalink
WaaS client library for Public Preview
Browse files Browse the repository at this point in the history
  • Loading branch information
yuga-cb committed Mar 21, 2023
0 parents commit 6878252
Show file tree
Hide file tree
Showing 128 changed files with 33,433 additions and 0 deletions.
25 changes: 25 additions & 0 deletions .github/workflows/go.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# This workflow will build a golang project
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-go

name: Go

on:
push:
branches: [ "main" ]
pull_request:
branches: [ "main" ]

jobs:

build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3

- name: Set up Go
uses: actions/setup-go@v3
with:
go-version: 1.18

- name: Build
run: go build
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
.proto/*

.idea/*

waas-client-library-go
13 changes: 13 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
Copyright (c) 2018-2023 Coinbase, Inc.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
62 changes: 62 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# WaaS Go Client Library

This repository contains the Protocol Buffer definitions for the Coinbase **Wallet-as-a-Service** (WaaS)
APIs, as well as the Go client libraries generated from them.

## Overview

WaaS is Coinbase's suite of cloud APIs for creating, managing, and using Multi-Party Computation
(MPC)-based crypto wallets. Designed for use by nimble start-ups and large enterprise clients alike,
WaaS APIs are self-service and composable, allowing developers to easily create applications that
interact with the blockchain and inherit the maximal security properties of MPC-based key management.

## Documentation

For full documentation, refer to [docs.cloud.coinbase.com/waas](https://docs.cloud.coinbase.com/waas/).

## Prerequisites

- [Golang 1.17+](https://go.dev/learn/)
- [node 17+](https://nodejs.org/en/download/)
- [yarn 1.22+](https://yarnpkg.com/getting-started/install)

For iOS development:
- [Xcode 14.0+](https://developer.apple.com/xcode/)
- iOS15.2+ simulator (iPhone 14 recommended)
- [CocoaPods](https://guides.cocoapods.org/using/getting-started.html)

For Android development:
- [Android Studio](https://developer.android.com/studio)
- 64-bit Android emulator
- [Android NDK 30+](https://developer.android.com/ndk)

## Repository Structure
- [`auth/`](./auth/) contains the authentication-related code for accessing WaaS APIs.
- [`clients/`](./clients/) contains client instantiation helpers for WaaS APIs.
- [`gen/`](./gen/) contains Go code generated from the Protocol Buffers.
- [`protos/`](./protos/) contains the Protocol Buffers that define the WaaS APIs.

## Module Installation
```
go get github.com/coinbase/waas-client-library-go
```

## Get Started
To test that your API Key gives you access as expected to the WaaS APIs:

1. Replace `apiKeyName` and `apiKeyPrivateKey` in [`example.go`](./example.go) with your API Key information.
2. Run `go build`.
3. Run `./waas-client-library-go`.
4. You should see output like the following:
```
2023/03/17 12:37:35 creating pool...
2023/03/17 12:37:35 created pool: name:"pools/e08c1784-f2be-4b77-8307-a9ea2d8d0017" display_name:"My First Pool"
2023/03/17 12:37:35 listing networks...
2023/03/17 12:37:35 got network: name:"networks/ethereum-goerli" display_name:"Goerli Ethereum Testnet" native_asset:"networks/ethereum-goerli/assets/0c3569d3-b253-5128-a229-543e1e819430" protocol_family:"protocolFamilies/evm" type:TESTNET
2023/03/17 12:37:35 listing first 5 assets on Ethereum Goerli...
2023/03/17 12:37:35 got asset: name:"networks/ethereum-goerli/assets/0c3569d3-b253-5128-a229-543e1e819430" advertised_symbol:"ETH" decimals:18 definition:{asset_type:"native"}
2023/03/17 12:37:35 got asset: name:"networks/ethereum-goerli/assets/adbf9e76-de39-51a0-9e53-5f8ef31b7925" advertised_symbol:"POLY" decimals:18 definition:{asset_type:"erc20" asset_group_id:"0x887CFe31C888EE0780795b7feFF46CE7f9AB556C"}
2023/03/17 12:37:35 got asset: name:"networks/ethereum-goerli/assets/a055a425-fe93-51ae-9099-cf5495db6e79" advertised_symbol:"SIM" decimals:18 definition:{asset_type:"erc20" asset_group_id:"0x0E89BF4135acE3d4d67BF828707746D3855f3a25"}
2023/03/17 12:37:35 got asset: name:"networks/ethereum-goerli/assets/145f3157-a45d-5a77-92be-6b2af8b7af12" advertised_symbol:"TERC20" decimals:18 definition:{asset_type:"erc20" asset_group_id:"0xea100Bec80418680e55D28b655da6CbEF427275f"}
2023/03/17 12:37:35 got asset: name:"networks/ethereum-goerli/assets/20b2830e-53c1-5540-9d1b-0061df3555f6" advertised_symbol:"BETH" decimals:18 definition:{asset_type:"erc20" asset_group_id:"0xED6CCd7e5131073aE67221B1cA195db0fFacc940"}
```
83 changes: 83 additions & 0 deletions auth/api_key.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package auth

import (
"errors"
"os"
)

const (
// nameEnvVar is the environment variable for the Coinbase Cloud API Key name.
nameEnvVar = "COINBASE_CLOUD_API_KEY_NAME"

// privateKeyEnvVar is the environment variable for the Coinbase Cloud API Key private key.
privateKeyEnvVar = "COINBASE_CLOUD_API_KEY_PRIVATE_KEY"
)

// APIKey represents a Coinbase Cloud API Key.
type APIKey struct {
Name string
PrivateKey string
}

// apiKeyConfig contains configuration information for constructing the API Key.
type apiKeyConfig struct {
loadApiKeyFromEnv bool
apiKeyName string
apiKeyPrivateKey string
}

// apiKeyOption is a function that applies changes to a apiKeyConfig.
type apiKeyOption func(t *apiKeyConfig)

// WithAPIKeyName returns an option to set the API Key.
func WithAPIKeyName(apiKeyName, apiKeyPrivateKey string) apiKeyOption {
return func(t *apiKeyConfig) {
t.apiKeyName = apiKeyName
t.apiKeyPrivateKey = apiKeyPrivateKey
}
}

// WithLoadAPIKeyFromEnv returns an option to set whether or not to load the API
// Key from environment variables. If the API Key name and private key are both set,
// they take precedence over the environment variables.
func WithLoadAPIKeyFromEnv(loadAPIKeyFromEnv bool) apiKeyOption {
return func(t *apiKeyConfig) {
t.loadApiKeyFromEnv = loadAPIKeyFromEnv
}
}

// NewAPIKey creates a new Coinbase Cloud API Key based on the provided options.
func NewAPIKey(opts ...apiKeyOption) (*APIKey, error) {
config := &apiKeyConfig{}

for _, opt := range opts {
opt(config)
}

if config.apiKeyName != "" && config.apiKeyPrivateKey != "" {
return &APIKey{
Name: config.apiKeyName,
PrivateKey: config.apiKeyPrivateKey,
}, nil
}

return loadAPIKeyFromEnv()
}

// loadAPIKeyFromEnv loads a new Coinbase Cloud API Key from environment variables.
func loadAPIKeyFromEnv() (*APIKey, error) {
name := os.Getenv(nameEnvVar)
if name == "" {
return nil, errors.New("environment variable COINBASE_CLOUD_API_KEY_NAME must be set")
}

privateKey := os.Getenv(privateKeyEnvVar)
if privateKey == "" {
return nil, errors.New("environment variable COINBASE_CLOUD_API_KEY_PRIVATE_KEY must be set")
}

return &APIKey{
Name: name,
PrivateKey: privateKey,
}, nil
}
86 changes: 86 additions & 0 deletions auth/authenticator.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
package auth

import (
"crypto/rand"
"crypto/x509"
"encoding/pem"
"fmt"
"math"
"math/big"
"time"

"gopkg.in/square/go-jose.v2"
"gopkg.in/square/go-jose.v2/jwt"
)

// Authenticator builds a JWT based on the APIKey.
type Authenticator struct {
apiKey *APIKey
}

// APIKeyClaims holds public claim values for a JWT, as well as a URI.
type APIKeyClaims struct {
*jwt.Claims
URI string `json:"uri"`
}

// NewAuthenticator returns a new Authenticator.
func NewAuthenticator(apiKey *APIKey) *Authenticator {
return &Authenticator{
apiKey: apiKey,
}
}

// BuildJWT constructs and returns the JWT as a string along with an error, if any.
func (a *Authenticator) BuildJWT(service, uri string) (string, error) {
block, _ := pem.Decode([]byte(a.apiKey.PrivateKey))
if block == nil {
return "", fmt.Errorf("jwt: Could not decode private key")
}

if block.Type != "ECDSA Private Key" {
return "", fmt.Errorf("jwt: Bad private key type")
}

key, err := x509.ParseECPrivateKey(block.Bytes)
if err != nil {
return "", fmt.Errorf("jwt: %w", err)
}

sig, err := jose.NewSigner(
jose.SigningKey{Algorithm: jose.ES256, Key: key},
(&jose.SignerOptions{NonceSource: nonceSource{}}).WithType("JWT").WithHeader("kid", a.apiKey.Name),
)
if err != nil {
return "", fmt.Errorf("jwt: %w", err)
}

cl := &APIKeyClaims{
Claims: &jwt.Claims{
Subject: a.apiKey.Name,
Issuer: "coinbase-cloud",
NotBefore: jwt.NewNumericDate(time.Now()),
Expiry: jwt.NewNumericDate(time.Now().Add(1 * time.Minute)),
Audience: jwt.Audience{service},
},
URI: uri,
}
jwtString, err := jwt.Signed(sig).Claims(cl).CompactSerialize()
if err != nil {
return "", fmt.Errorf("jwt: %w", err)
}
return jwtString, nil
}

// nonceSource implements the jose.NonceSource interface. It is used for building
// the JWT.
type nonceSource struct{}

// Nonce calculates and returns nonce as a string and an error, if any.
func (n nonceSource) Nonce() (string, error) {
r, err := rand.Int(rand.Reader, big.NewInt(math.MaxInt64))
if err != nil {
return "", err
}
return r.String(), nil
}
41 changes: 41 additions & 0 deletions clients/errors.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package clients

import (
"net/http"

"github.com/googleapis/gax-go/v2/apierror"
"google.golang.org/api/googleapi"
)

// HTTPCode attempts to return the HTTP status code for the given error.
// If it cannot be determined, it returns a 500 (Internal Server Error).
func HTTPCode(err error) int {
if err == nil {
return http.StatusOK
}

if googleapiErr, ok := err.(*googleapi.Error); ok {
return googleapiErr.Code
}

return http.StatusInternalServerError
}

// UnwrapError attempts to unwrap the passed error and return a googleapi error.
// It is expected that the passed error is of type apierror.APIError.
// If it is not, it returns the passed error unchanged.
func UnwrapError(err error) error {
if err == nil {
return nil
}

if apiErr, ok := err.(*apierror.APIError); ok { //nolint:errorlint
// Unwrap the the `apierror` to extract the HTTP status code and original error message.
unwrappedErr := apiErr.Unwrap()
if googleapiErr, ok := unwrappedErr.(*googleapi.Error); ok { //nolint:errorlint
return googleapiErr
}
}

return err
}
Loading

0 comments on commit 6878252

Please sign in to comment.