178 lines
		
	
	
		
			3.8 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			178 lines
		
	
	
		
			3.8 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| // Package btcaddress makes sure the Bitcoin address for the Ripple donations
 | |
| // account on Keybase is up-to-date.
 | |
| // This shows a default key if the system is not set up, or otherwise refreshes
 | |
| // the address every 30 minutes (using redis to hold the cache).
 | |
| // The wallet with the name "Ripple" will be used for getting the address.
 | |
| package btcaddress
 | |
| 
 | |
| import (
 | |
| 	"crypto/hmac"
 | |
| 	"crypto/sha256"
 | |
| 	"encoding/json"
 | |
| 	"fmt"
 | |
| 	"net/http"
 | |
| 	"time"
 | |
| 
 | |
| 	"strings"
 | |
| 
 | |
| 	"gopkg.in/redis.v5"
 | |
| )
 | |
| 
 | |
| // DefaultAddress is the address used if the package is not set up to refresh
 | |
| // from coinbase.
 | |
| const DefaultAddress = "1CKGzZqrVwKoXwHEWpTobWqqtkYuqoFNro"
 | |
| 
 | |
| // AccountName is the name the account on coinbase must have to be considered as
 | |
| // the actual account.
 | |
| const AccountName = "Ripple"
 | |
| 
 | |
| // Configuration variables.
 | |
| var (
 | |
| 	Redis     *redis.Client
 | |
| 	APIKey    string
 | |
| 	APISecret string
 | |
| )
 | |
| 
 | |
| // Get retrieves the Bitcoin address, using the following methods:
 | |
| //
 | |
| // First the address is requested from Redis, and if the key is available from
 | |
| // redis then that is used.
 | |
| //
 | |
| // After that, the key is requested from coinbase, and the account ID is found
 | |
| // if not already saved in Redis.
 | |
| func Get() string {
 | |
| 	v := Redis.Get("hanayo:btcaddress").Val()
 | |
| 	if v != "" {
 | |
| 		return v
 | |
| 	}
 | |
| 	if APIKey == "" || APISecret == "" {
 | |
| 		return DefaultAddress
 | |
| 	}
 | |
| 
 | |
| 	a, err := getFromCoinbase()
 | |
| 	if err != nil {
 | |
| 		fmt.Println(err)
 | |
| 		return DefaultAddress
 | |
| 	}
 | |
| 
 | |
| 	Redis.Set("hanayo:btcaddress", a, time.Minute*30)
 | |
| 
 | |
| 	return a
 | |
| }
 | |
| 
 | |
| type account struct {
 | |
| 	ID   string `json:"id"`
 | |
| 	Name string `json:"name"`
 | |
| }
 | |
| 
 | |
| type accountsResp struct {
 | |
| 	Data []account `json:"data"`
 | |
| }
 | |
| 
 | |
| type address struct {
 | |
| 	ID      string `json:"id"`
 | |
| 	Name    string `json:"name"`
 | |
| 	Address string `json:"address"`
 | |
| }
 | |
| 
 | |
| type createAddressResp struct {
 | |
| 	Data address `json:"data"`
 | |
| }
 | |
| 
 | |
| type errors struct {
 | |
| 	Errors []struct {
 | |
| 		ID      string `json:"id"`
 | |
| 		Message string `json:"message"`
 | |
| 	} `json:"errors"`
 | |
| }
 | |
| 
 | |
| const coinbaseAPIDate = "2017-01-23"
 | |
| const coinbaseAPIBase = "https://api.coinbase.com/v2/"
 | |
| 
 | |
| var accountID string
 | |
| 
 | |
| func getFromCoinbase() (string, error) {
 | |
| 	if accountID == "" {
 | |
| 		var accs accountsResp
 | |
| 		err := req("accounts", nil, &accs)
 | |
| 		if err != nil {
 | |
| 			return "", err
 | |
| 		}
 | |
| 		for _, el := range accs.Data {
 | |
| 			if el.Name == AccountName {
 | |
| 				accountID = el.ID
 | |
| 				break
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	var addrResp createAddressResp
 | |
| 	err := req("accounts/"+accountID+"/addresses", struct{}{}, &addrResp)
 | |
| 	if err != nil {
 | |
| 		return "", err
 | |
| 	}
 | |
| 
 | |
| 	return addrResp.Data.Address, nil
 | |
| }
 | |
| 
 | |
| func req(endpoint string, data interface{}, out interface{}) error {
 | |
| 	var dataJSON string
 | |
| 	method := "GET"
 | |
| 	if data != nil {
 | |
| 		method = "POST"
 | |
| 		// sort of a dirty hack, but oh well
 | |
| 		djr, err := json.Marshal(data)
 | |
| 		if err != nil {
 | |
| 			return err
 | |
| 		}
 | |
| 		dataJSON = string(djr)
 | |
| 	}
 | |
| 
 | |
| 	req, err := http.NewRequest(method, coinbaseAPIBase+endpoint, strings.NewReader(dataJSON))
 | |
| 	if err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 
 | |
| 	if data != nil {
 | |
| 		req.Header.Add("Content-Type", "application/json")
 | |
| 	}
 | |
| 
 | |
| 	// Not usind .Header.Add because we want to make sure the names are kept
 | |
| 	// uppercase.
 | |
| 	ts := time.Now().Unix()
 | |
| 	req.Header["CB-VERSION"] = []string{coinbaseAPIDate}
 | |
| 	req.Header["CB-ACCESS-KEY"] = []string{APIKey}
 | |
| 	req.Header["CB-ACCESS-TIMESTAMP"] = []string{fmt.Sprintf("%d", ts)}
 | |
| 
 | |
| 	sig := fmt.Sprintf(
 | |
| 		"%d%s/v2/%s%s",
 | |
| 		ts,
 | |
| 		method,
 | |
| 		endpoint,
 | |
| 		string(dataJSON),
 | |
| 	)
 | |
| 
 | |
| 	hm := hmac.New(
 | |
| 		sha256.New,
 | |
| 		[]byte(APISecret),
 | |
| 	)
 | |
| 	hm.Write([]byte(sig))
 | |
| 
 | |
| 	req.Header["CB-ACCESS-SIGN"] = []string{
 | |
| 		fmt.Sprintf("%x", hm.Sum(nil)),
 | |
| 	}
 | |
| 
 | |
| 	resp, err := http.DefaultClient.Do(req)
 | |
| 	if err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 	if resp.StatusCode != 200 && resp.StatusCode != 201 {
 | |
| 		var errs errors
 | |
| 		json.NewDecoder(resp.Body).Decode(&errs)
 | |
| 		return fmt.Errorf("btcaddress: response status code is %d - %v", resp.StatusCode, errs)
 | |
| 	}
 | |
| 
 | |
| 	err = json.NewDecoder(resp.Body).Decode(&out)
 | |
| 	return err
 | |
| }
 |