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