Skip to content

Commit 90aff6e

Browse files
committed
REFACTOR: Add coinmarketcap data, add normalization, and rename from tickerproxy to ticker since we do more than proxy now.
1 parent 5abf5c6 commit 90aff6e

15 files changed

+811
-525
lines changed

.travis.yml

+2-2
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,12 @@ go:
33
- "1.10"
44
script:
55
- go test -v *.go
6-
- go build -o tickerproxyd ./bin/*.go
6+
- go build -o tickerfetcher ./fetch/*.go
77
deploy:
88
provider: releases
99
api_key:
1010
secure: iky7k3WTXwesvEfoMzjBhLPezb3MbRk6UF2KTnpEJM757uQppbNRFimLzAlGzj1zuQEn3RpSON8foN0oATH+07cWXAgwDUB5j1WXlJW6Sf2iQoTLXKOv9YKT0X9Rz7gxogKSELxKgJFanyyPTW2kAGJhtjAtmhi2MLcnSU9yVBQ6IQmWjAtQLSWm51bm191QPj9Dc434fGZDtB6obg+Ncw7a+Ri+872rh7EWvEtLdi5A/R2t4DgLBgRpLqo6t12W/isMyKt6pmZgqzg+1vJZtxmtl/keg6yb1hwxmlZiKFn3AXDTD7Hhl6pKYdmv+tp9N/AdTlYXBHoQ+fVEmVf+dl08E20zRn88QDGBkT0lbVrQlx68DqO9F/90mELhWfMjOfycoCykJHh+U5QYuXGSEMMOTrXQ4v3AQ02VYwrvhLhF/VfOQsOj4T8FfLjOhC2uai60EJs8UEPAZ2q3V3o8OTZ8FXR56K1dzz+dews4iwLp8H1Mtst7MGSU7/lTi4ciwL3FRmKFU8QJojbn9fEk1dI3jQg6aCQ33PY3nVZc+BBaK3e0HobMTJd0rZGD5Qt5UXTUHRk6Ou9Kal7cnnFLwhfLRi7MlmpoR15TVzGBlts3grxdfYmD5crjLEeQqgkiTpgYYkmILbGJ3EYGX/me9TngJujGbNx3AWsnvXuOPQs=
11-
file: tickerproxyd
11+
file: tickerfetcher
1212
skip_cleanup: true
1313
on:
1414
tags: true

Dockerfile

+5-5
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
FROM golang:1.10
22
WORKDIR /go/src/github.com/OpenBazaar/tickerproxy
33
COPY . .
4-
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build --ldflags '-extldflags "-static"' -o /opt/tickerproxy ./bin/*.go
4+
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build --ldflags '-extldflags "-static"' -o /opt/tickerfetcher ./fetch/*.go
55

66
FROM scratch
7-
VOLUME /var/lib/tickerproxy
8-
COPY --from=0 /opt/tickerproxy /opt/tickerproxy
9-
COPY --from=0 /etc/ssl/certs/ /etc/ssl/certs/
10-
CMD ["/opt/tickerproxy"]
7+
WORKDIR /var/lib/ticker
8+
COPY --from=0 /opt/tickerfetcher /opt/tickerfetcher
9+
COPY --from=0 /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt
10+
CMD ["/opt/tickerfetcher"]

bin/main.go

-69
This file was deleted.

btcavg.go

+161
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
package ticker
2+
3+
import (
4+
"crypto/hmac"
5+
"crypto/sha256"
6+
"encoding/hex"
7+
"encoding/json"
8+
"fmt"
9+
"io/ioutil"
10+
"net/http"
11+
"strings"
12+
"sync"
13+
"time"
14+
)
15+
16+
const (
17+
btcavgFiatEndpoint = "https://apiv2.bitcoinaverage.com/indices/global/ticker/all?crypto=BTC"
18+
btcavgCryptoEndpoint = "https://apiv2.bitcoinaverage.com/indices/crypto/ticker/all"
19+
)
20+
21+
type btcavgFetcher struct {
22+
pubkey string
23+
privkey string
24+
}
25+
26+
func NewBTCAVGFetcher(pubkey string, privkey string) fetchFn {
27+
return func() (exchangeRates, error) {
28+
output := exchangeRates{}
29+
errCh := make(chan error, 2)
30+
31+
// Request both endpoints and save their responses
32+
wg := sync.WaitGroup{}
33+
wg.Add(2)
34+
go func() {
35+
defer wg.Done()
36+
rates, err := fetchBTCAVGResource(btcavgFiatEndpoint, pubkey, privkey)
37+
if err != nil {
38+
errCh <- err
39+
return
40+
}
41+
42+
formatBTCAVGFiatOutput(output, rates)
43+
}()
44+
45+
go func() {
46+
defer wg.Done()
47+
rates, err := fetchBTCAVGResource(btcavgCryptoEndpoint, pubkey, privkey)
48+
if err != nil {
49+
errCh <- err
50+
return
51+
}
52+
53+
err = formatBTCAVGCryptoOutput(output, rates)
54+
if err != nil {
55+
errCh <- err
56+
return
57+
}
58+
}()
59+
wg.Wait()
60+
close(errCh)
61+
62+
for err := range errCh {
63+
return nil, err
64+
}
65+
66+
return output, nil
67+
}
68+
}
69+
70+
// fetchBTCAVGResource gets the response for a given BitcoinAverage endpoint
71+
func fetchBTCAVGResource(url string, pubkey string, privkey string) (exchangeRates, error) {
72+
// Create signed request
73+
req, err := http.NewRequest("GET", url, nil)
74+
if err != nil {
75+
return nil, err
76+
}
77+
req.Header.Add("X-signature", createBTCAVGSignature(pubkey, privkey))
78+
79+
// Send the requests
80+
resp, err := httpClient.Do(req)
81+
if err != nil {
82+
return nil, err
83+
}
84+
defer resp.Body.Close()
85+
86+
// Read the response body
87+
body, err := ioutil.ReadAll(resp.Body)
88+
if err != nil {
89+
return nil, err
90+
}
91+
92+
// Return an error if no success, otherwise deserialize and return our body
93+
if resp.StatusCode != 200 {
94+
return nil, err
95+
}
96+
97+
rates := make(exchangeRates)
98+
err = json.Unmarshal(body, &rates)
99+
if err != nil {
100+
return nil, err
101+
}
102+
103+
return rates, nil
104+
}
105+
106+
// formatBTCAVGFiatOutput formats BTC->fiat pairs
107+
func formatBTCAVGFiatOutput(outgoing exchangeRates, incoming exchangeRates) {
108+
for k, v := range incoming {
109+
if strings.HasPrefix(k, "BTC") {
110+
outgoing[strings.TrimPrefix(k, "BTC")] = v
111+
}
112+
}
113+
}
114+
115+
// formatBTCAVGCryptoOutput formats BTC->crypto pairs
116+
func formatBTCAVGCryptoOutput(outgoing exchangeRates, incoming exchangeRates) error {
117+
for symbol, entry := range incoming {
118+
trimmedSymbol := strings.TrimSuffix(symbol, "BTC")
119+
if symbol == trimmedSymbol {
120+
continue
121+
}
122+
symbol := CanonicalizeSymbol(trimmedSymbol)
123+
124+
if !IsCorrectIDForSymbol(symbol, entry.ID) {
125+
continue
126+
}
127+
128+
if entry.Ask == "" || entry.Bid == "" || entry.Last == "" {
129+
continue
130+
}
131+
132+
ask, err := invertAndFormatPrice(entry.Ask)
133+
if err != nil {
134+
return err
135+
}
136+
bid, err := invertAndFormatPrice(entry.Bid)
137+
if err != nil {
138+
return err
139+
}
140+
last, err := invertAndFormatPrice(entry.Last)
141+
if err != nil {
142+
return err
143+
}
144+
145+
outgoing[symbol] = exchangeRate{Ask: ask, Bid: bid, Last: last}
146+
}
147+
return nil
148+
}
149+
150+
func createBTCAVGSignature(pubkey string, privkey string) string {
151+
// Build payload
152+
payload := fmt.Sprintf("%d.%s", time.Now().Unix(), pubkey)
153+
154+
// Generate the HMAC-sha256 signature
155+
mac := hmac.New(sha256.New, []byte(privkey))
156+
mac.Write([]byte(payload))
157+
signature := hex.EncodeToString(mac.Sum(nil))
158+
159+
// Return the final payload
160+
return fmt.Sprintf("%s.%s", payload, signature)
161+
}

cmc.go

+95
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
package ticker
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
"io/ioutil"
7+
)
8+
9+
const (
10+
cmcQueryEndpointTempalte = "https://api.coinmarketcap.com/v2/ticker?convert=BTC&start=%d&limit=%d"
11+
cmcQueryFirstID = 1
12+
defaultCMCQueryLimit = 100
13+
)
14+
15+
type cmcCoinData struct {
16+
ID int64 `json:"id"`
17+
Symbol string `json:"symbol"`
18+
Name string `json:"name"`
19+
Quotes struct {
20+
BTC struct {
21+
Price json.Number `json:"price"`
22+
MarketCap float64 `json:"market_cap"`
23+
} `json:"BTC"`
24+
} `json:"quotes"`
25+
}
26+
27+
type cmcResponse struct {
28+
Data map[json.Number]cmcCoinData `json:"data"`
29+
30+
Metadata struct {
31+
Count int `json:"num_cryptocurrencies"`
32+
} `json:"metadata"`
33+
}
34+
35+
func FetchCMC() (exchangeRates, error) {
36+
output := exchangeRates{}
37+
38+
resp, err := fetchCMCResource(cmcQueryFirstID, output)
39+
if err != nil {
40+
return nil, err
41+
}
42+
43+
for i := defaultCMCQueryLimit + cmcQueryFirstID; i < resp.Metadata.Count; i += defaultCMCQueryLimit {
44+
_, err = fetchCMCResource(i, output)
45+
if err != nil {
46+
return nil, err
47+
}
48+
}
49+
50+
return output, nil
51+
}
52+
53+
func fetchCMCResource(start int, output exchangeRates) (*cmcResponse, error) {
54+
resp, err := httpClient.Get(buildCMCQueryEndpoint(start, defaultCMCQueryLimit))
55+
if err != nil {
56+
return nil, err
57+
}
58+
defer resp.Body.Close()
59+
60+
body, err := ioutil.ReadAll(resp.Body)
61+
if err != nil {
62+
return nil, err
63+
}
64+
65+
payload := &cmcResponse{}
66+
err = json.Unmarshal(body, payload)
67+
if err != nil {
68+
return nil, err
69+
}
70+
71+
for _, entry := range payload.Data {
72+
entry.Symbol = CanonicalizeSymbol(entry.Symbol)
73+
74+
if !IsCorrectIDForSymbol(entry.Symbol, entry.ID) {
75+
continue
76+
}
77+
78+
if entry.Quotes.BTC.Price == "" {
79+
continue
80+
}
81+
82+
price, err := invertAndFormatPrice(entry.Quotes.BTC.Price)
83+
if err != nil {
84+
return nil, err
85+
}
86+
87+
output[entry.Symbol] = exchangeRate{Ask: price, Bid: price, Last: price}
88+
}
89+
90+
return payload, nil
91+
}
92+
93+
func buildCMCQueryEndpoint(start int, limit int) string {
94+
return fmt.Sprintf(cmcQueryEndpointTempalte, start, limit)
95+
}

0 commit comments

Comments
 (0)