Skip to content

Commit

Permalink
REFACTOR: Add coinmarketcap data, add normalization, and rename from …
Browse files Browse the repository at this point in the history
…tickerproxy to ticker since we do more than proxy now.
  • Loading branch information
tyler-smith committed Jun 23, 2018
1 parent 5abf5c6 commit 90aff6e
Show file tree
Hide file tree
Showing 15 changed files with 811 additions and 525 deletions.
4 changes: 2 additions & 2 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@ go:
- "1.10"
script:
- go test -v *.go
- go build -o tickerproxyd ./bin/*.go
- go build -o tickerfetcher ./fetch/*.go
deploy:
provider: releases
api_key:
secure: iky7k3WTXwesvEfoMzjBhLPezb3MbRk6UF2KTnpEJM757uQppbNRFimLzAlGzj1zuQEn3RpSON8foN0oATH+07cWXAgwDUB5j1WXlJW6Sf2iQoTLXKOv9YKT0X9Rz7gxogKSELxKgJFanyyPTW2kAGJhtjAtmhi2MLcnSU9yVBQ6IQmWjAtQLSWm51bm191QPj9Dc434fGZDtB6obg+Ncw7a+Ri+872rh7EWvEtLdi5A/R2t4DgLBgRpLqo6t12W/isMyKt6pmZgqzg+1vJZtxmtl/keg6yb1hwxmlZiKFn3AXDTD7Hhl6pKYdmv+tp9N/AdTlYXBHoQ+fVEmVf+dl08E20zRn88QDGBkT0lbVrQlx68DqO9F/90mELhWfMjOfycoCykJHh+U5QYuXGSEMMOTrXQ4v3AQ02VYwrvhLhF/VfOQsOj4T8FfLjOhC2uai60EJs8UEPAZ2q3V3o8OTZ8FXR56K1dzz+dews4iwLp8H1Mtst7MGSU7/lTi4ciwL3FRmKFU8QJojbn9fEk1dI3jQg6aCQ33PY3nVZc+BBaK3e0HobMTJd0rZGD5Qt5UXTUHRk6Ou9Kal7cnnFLwhfLRi7MlmpoR15TVzGBlts3grxdfYmD5crjLEeQqgkiTpgYYkmILbGJ3EYGX/me9TngJujGbNx3AWsnvXuOPQs=
file: tickerproxyd
file: tickerfetcher
skip_cleanup: true
on:
tags: true
Expand Down
10 changes: 5 additions & 5 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
FROM golang:1.10
WORKDIR /go/src/github.com/OpenBazaar/tickerproxy
COPY . .
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build --ldflags '-extldflags "-static"' -o /opt/tickerproxy ./bin/*.go
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build --ldflags '-extldflags "-static"' -o /opt/tickerfetcher ./fetch/*.go

FROM scratch
VOLUME /var/lib/tickerproxy
COPY --from=0 /opt/tickerproxy /opt/tickerproxy
COPY --from=0 /etc/ssl/certs/ /etc/ssl/certs/
CMD ["/opt/tickerproxy"]
WORKDIR /var/lib/ticker
COPY --from=0 /opt/tickerfetcher /opt/tickerfetcher
COPY --from=0 /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt
CMD ["/opt/tickerfetcher"]
69 changes: 0 additions & 69 deletions bin/main.go

This file was deleted.

161 changes: 161 additions & 0 deletions btcavg.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
package ticker

import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"strings"
"sync"
"time"
)

const (
btcavgFiatEndpoint = "https://apiv2.bitcoinaverage.com/indices/global/ticker/all?crypto=BTC"
btcavgCryptoEndpoint = "https://apiv2.bitcoinaverage.com/indices/crypto/ticker/all"
)

type btcavgFetcher struct {
pubkey string
privkey string
}

func NewBTCAVGFetcher(pubkey string, privkey string) fetchFn {
return func() (exchangeRates, error) {
output := exchangeRates{}
errCh := make(chan error, 2)

// Request both endpoints and save their responses
wg := sync.WaitGroup{}
wg.Add(2)
go func() {
defer wg.Done()
rates, err := fetchBTCAVGResource(btcavgFiatEndpoint, pubkey, privkey)
if err != nil {
errCh <- err
return
}

formatBTCAVGFiatOutput(output, rates)
}()

go func() {
defer wg.Done()
rates, err := fetchBTCAVGResource(btcavgCryptoEndpoint, pubkey, privkey)
if err != nil {
errCh <- err
return
}

err = formatBTCAVGCryptoOutput(output, rates)
if err != nil {
errCh <- err
return
}
}()
wg.Wait()
close(errCh)

for err := range errCh {
return nil, err
}

return output, nil
}
}

// fetchBTCAVGResource gets the response for a given BitcoinAverage endpoint
func fetchBTCAVGResource(url string, pubkey string, privkey string) (exchangeRates, error) {
// Create signed request
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return nil, err
}
req.Header.Add("X-signature", createBTCAVGSignature(pubkey, privkey))

// Send the requests
resp, err := httpClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()

// Read the response body
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err
}

// Return an error if no success, otherwise deserialize and return our body
if resp.StatusCode != 200 {
return nil, err
}

rates := make(exchangeRates)
err = json.Unmarshal(body, &rates)
if err != nil {
return nil, err
}

return rates, nil
}

// formatBTCAVGFiatOutput formats BTC->fiat pairs
func formatBTCAVGFiatOutput(outgoing exchangeRates, incoming exchangeRates) {
for k, v := range incoming {
if strings.HasPrefix(k, "BTC") {
outgoing[strings.TrimPrefix(k, "BTC")] = v
}
}
}

// formatBTCAVGCryptoOutput formats BTC->crypto pairs
func formatBTCAVGCryptoOutput(outgoing exchangeRates, incoming exchangeRates) error {
for symbol, entry := range incoming {
trimmedSymbol := strings.TrimSuffix(symbol, "BTC")
if symbol == trimmedSymbol {
continue
}
symbol := CanonicalizeSymbol(trimmedSymbol)

if !IsCorrectIDForSymbol(symbol, entry.ID) {
continue
}

if entry.Ask == "" || entry.Bid == "" || entry.Last == "" {
continue
}

ask, err := invertAndFormatPrice(entry.Ask)
if err != nil {
return err
}
bid, err := invertAndFormatPrice(entry.Bid)
if err != nil {
return err
}
last, err := invertAndFormatPrice(entry.Last)
if err != nil {
return err
}

outgoing[symbol] = exchangeRate{Ask: ask, Bid: bid, Last: last}
}
return nil
}

func createBTCAVGSignature(pubkey string, privkey string) string {
// Build payload
payload := fmt.Sprintf("%d.%s", time.Now().Unix(), pubkey)

// Generate the HMAC-sha256 signature
mac := hmac.New(sha256.New, []byte(privkey))
mac.Write([]byte(payload))
signature := hex.EncodeToString(mac.Sum(nil))

// Return the final payload
return fmt.Sprintf("%s.%s", payload, signature)
}
95 changes: 95 additions & 0 deletions cmc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
package ticker

import (
"encoding/json"
"fmt"
"io/ioutil"
)

const (
cmcQueryEndpointTempalte = "https://api.coinmarketcap.com/v2/ticker?convert=BTC&start=%d&limit=%d"
cmcQueryFirstID = 1
defaultCMCQueryLimit = 100
)

type cmcCoinData struct {
ID int64 `json:"id"`
Symbol string `json:"symbol"`
Name string `json:"name"`
Quotes struct {
BTC struct {
Price json.Number `json:"price"`
MarketCap float64 `json:"market_cap"`
} `json:"BTC"`
} `json:"quotes"`
}

type cmcResponse struct {
Data map[json.Number]cmcCoinData `json:"data"`

Metadata struct {
Count int `json:"num_cryptocurrencies"`
} `json:"metadata"`
}

func FetchCMC() (exchangeRates, error) {
output := exchangeRates{}

resp, err := fetchCMCResource(cmcQueryFirstID, output)
if err != nil {
return nil, err
}

for i := defaultCMCQueryLimit + cmcQueryFirstID; i < resp.Metadata.Count; i += defaultCMCQueryLimit {
_, err = fetchCMCResource(i, output)
if err != nil {
return nil, err
}
}

return output, nil
}

func fetchCMCResource(start int, output exchangeRates) (*cmcResponse, error) {
resp, err := httpClient.Get(buildCMCQueryEndpoint(start, defaultCMCQueryLimit))
if err != nil {
return nil, err
}
defer resp.Body.Close()

body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err
}

payload := &cmcResponse{}
err = json.Unmarshal(body, payload)
if err != nil {
return nil, err
}

for _, entry := range payload.Data {
entry.Symbol = CanonicalizeSymbol(entry.Symbol)

if !IsCorrectIDForSymbol(entry.Symbol, entry.ID) {
continue
}

if entry.Quotes.BTC.Price == "" {
continue
}

price, err := invertAndFormatPrice(entry.Quotes.BTC.Price)
if err != nil {
return nil, err
}

output[entry.Symbol] = exchangeRate{Ask: price, Bid: price, Last: price}
}

return payload, nil
}

func buildCMCQueryEndpoint(start int, limit int) string {
return fmt.Sprintf(cmcQueryEndpointTempalte, start, limit)
}
Loading

0 comments on commit 90aff6e

Please sign in to comment.