Skip to content

Commit

Permalink
Add exchange rates to currencies
Browse files Browse the repository at this point in the history
  • Loading branch information
zensh committed Sep 6, 2023
1 parent ed32873 commit f368891
Show file tree
Hide file tree
Showing 14 changed files with 359 additions and 52 deletions.
9 changes: 8 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,14 @@ jobs:
matrix:
os: [ubuntu-latest]
go-version: ['1.21.x']

services:
keydb:
image: eqalpha/keydb:latest
ports:
- 6379:6379
options: --health-cmd "redis-cli ping " --health-interval 5s --health-retries 10
volumes:
- ${{ github.workspace }}:/workspace
steps:
- name: Install Go
uses: actions/setup-go@v4
Expand Down
4 changes: 4 additions & 0 deletions config/default.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ addr = ":8080"
# The maximum number of seconds to wait for graceful shutdown.
graceful_shutdown = 10

[redis]
prefix = "WALLET:"
node = "127.0.0.1:6379"

[base]
userbase = "http://127.0.0.1:8080"
logbase = "http://127.0.0.1:8080"
Expand Down
13 changes: 8 additions & 5 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,20 @@ go 1.21
require (
github.com/BurntSushi/toml v1.3.2
github.com/fxamacker/cbor/v2 v2.5.0
github.com/go-playground/validator/v10 v10.15.1
github.com/go-playground/validator/v10 v10.15.3
github.com/google/uuid v1.3.1
github.com/klauspost/compress v1.16.7
github.com/redis/go-redis/v9 v9.1.0
github.com/rs/xid v1.5.0
github.com/stretchr/testify v1.8.4
github.com/teambition/gear v1.27.3
go.uber.org/dig v1.17.0
)

require (
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/gabriel-vasile/mimetype v1.4.2 // indirect
github.com/go-http-utils/cookie v1.3.1 // indirect
github.com/go-http-utils/negotiator v1.0.0 // indirect
Expand All @@ -25,9 +28,9 @@ require (
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/teambition/trie-mux v1.5.2 // indirect
github.com/x448/float16 v0.8.4 // indirect
golang.org/x/crypto v0.12.0 // indirect
golang.org/x/net v0.14.0 // indirect
golang.org/x/sys v0.11.0 // indirect
golang.org/x/text v0.12.0 // indirect
golang.org/x/crypto v0.13.0 // indirect
golang.org/x/net v0.15.0 // indirect
golang.org/x/sys v0.12.0 // indirect
golang.org/x/text v0.13.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
22 changes: 20 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,8 +1,16 @@
github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8=
github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
github.com/bsm/ginkgo/v2 v2.9.5 h1:rtVBYPs3+TC5iLUVOis1B9tjLTup7Cj5IfzosKtvTJ0=
github.com/bsm/ginkgo/v2 v2.9.5/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
github.com/bsm/gomega v1.26.0 h1:LhQm+AFcgV2M0WyKroMASzAzCAJVpAxQXv4SaI9a69Y=
github.com/bsm/gomega v1.26.0/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
github.com/dimfeld/httptreemux v5.0.1+incompatible/go.mod h1:rbUlSV+CCpv/SuqUTP/8Bk2O3LyUV436/yaRGkhP6Z0=
github.com/fxamacker/cbor/v2 v2.5.0 h1:oHsG0V/Q6E/wqTS2O1Cozzsy69nqCiguo5Q1a1ADivE=
github.com/fxamacker/cbor/v2 v2.5.0/go.mod h1:TA1xS00nchWmaBnEIxPSE5oHLuJBAVvqrtAnWBwBCVo=
Expand All @@ -18,8 +26,8 @@ github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/o
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.15.1 h1:BSe8uhN+xQ4r5guV/ywQI4gO59C2raYcGffYWZEjZzM=
github.com/go-playground/validator/v10 v10.15.1/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
github.com/go-playground/validator/v10 v10.15.3 h1:S+sSpunYjNPDuXkWbK+x+bA7iXiW296KG4dL3X7xUZo=
github.com/go-playground/validator/v10 v10.15.3/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4=
github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
Expand All @@ -29,6 +37,8 @@ github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q=
github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/redis/go-redis/v9 v9.1.0 h1:137FnGdk+EQdCbye1FW+qOEcY5S+SpY9T0NiuqvtfMY=
github.com/redis/go-redis/v9 v9.1.0/go.mod h1:urWj3He21Dj5k4TK1y59xH8Uj6ATueP8AH1cY3lZl4c=
github.com/rs/xid v1.5.0 h1:mKX4bl4iPYJtEIxp6CYiUuLQ/8DYMoz0PUdtGgMFRVc=
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
Expand All @@ -50,12 +60,20 @@ go.uber.org/dig v1.17.0 h1:5Chju+tUvcC+N7N6EV08BJz41UZuO3BmHcN4A287ZLI=
go.uber.org/dig v1.17.0/go.mod h1:rTxpf7l5I0eBTlE6/9RL+lDybC7WFwY2QH55ZSjy1mU=
golang.org/x/crypto v0.12.0 h1:tFM/ta59kqch6LlvYnPa0yx5a83cL2nHflFhYKvv9Yk=
golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw=
golang.org/x/crypto v0.13.0 h1:mvySKfSWJ+UKUii46M40LOvyWfN0s2U+46/jDd0e6Ck=
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
golang.org/x/net v0.14.0 h1:BONx9s002vGdD9umnlX1Po8vOZmrgH34qlHcD1MfK14=
golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI=
golang.org/x/net v0.15.0 h1:ugBLEUaxABaB5AJqW9enI0ACdci2RUd4eP51NTBvuJ8=
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
golang.org/x/sys v0.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM=
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/text v0.12.0 h1:k+n5B8goJNdU7hSvEtMUz3d1Q6D/XW4COJSJR6fN0mc=
golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
Expand Down
17 changes: 16 additions & 1 deletion src/api/app.go
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
package api

import (
"context"
"log"
"net/http"
"strings"
"time"

"github.com/fxamacker/cbor/v2"
"github.com/teambition/gear"

"github.com/yiwen-ai/wallet-api/src/bll"
"github.com/yiwen-ai/wallet-api/src/conf"
"github.com/yiwen-ai/wallet-api/src/logging"
"github.com/yiwen-ai/wallet-api/src/util"
Expand All @@ -27,10 +30,22 @@ func NewApp() *gear.App {
app.Set(gear.SetEnv, conf.Config.Env)

app.UseHandler(logging.AccessLogger)
err := util.DigInvoke(func(routers []*gear.Router) error {
err := util.DigInvoke(func(blls *bll.Blls, routers []*gear.Router) error {
for _, router := range routers {
app.UseHandler(router)
}

ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()

headers := http.Header{}
headers.Set("x-auth-user", util.JARVIS.String())
headers.Set("x-auth-app", util.JARVIS.String())
ctxHeader := util.ContextHTTPHeader(headers)
ctx = gear.CtxWith[util.ContextHTTPHeader](ctx, &ctxHeader)
if err := blls.Walletbase.InitApp(ctx, app); err != nil {
return err
}
return nil
})

Expand Down
11 changes: 9 additions & 2 deletions src/api/wallet.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,19 @@ type Wallet struct {
}

func (a *Wallet) ListCurrencies(ctx *gear.Context) error {
output, err := a.blls.Walletbase.ListCurrencies(ctx)
currencies := make(bll.Currencies, 0, len(a.blls.Walletbase.Currencies))
rates, err := a.blls.ExternalAPI.ExchangeRate(ctx)
if err != nil {
return gear.ErrInternalServerError.From(err)
}

return ctx.OkSend(bll.SuccessResponse[[]bll.Currency]{Result: output})
var ok bool
for _, currency := range a.blls.Walletbase.Currencies {
if currency.Rate, ok = rates.Rates[currency.Alpha]; ok {
currencies = append(currencies, currency)
}
}
return ctx.OkSend(bll.SuccessResponse[bll.Currencies]{Result: currencies})
}

func (a *Wallet) Get(ctx *gear.Context) error {
Expand Down
30 changes: 21 additions & 9 deletions src/bll/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,20 +16,22 @@ func init() {

// Blls ...
type Blls struct {
Logbase *Logbase
Taskbase *Taskbase
Userbase *Userbase
Walletbase *Walletbase
ExternalAPI *ExternalAPI
Logbase *Logbase
Taskbase *Taskbase
Userbase *Userbase
Walletbase *Walletbase
}

// NewBlls ...
func NewBlls() *Blls {
func NewBlls(redis *service.Redis) *Blls {
cfg := conf.Config.Base
return &Blls{
Logbase: &Logbase{svc: service.APIHost(cfg.Logbase)},
Taskbase: &Taskbase{svc: service.APIHost(cfg.Taskbase)},
Userbase: &Userbase{svc: service.APIHost(cfg.Userbase)},
Walletbase: &Walletbase{svc: service.APIHost(cfg.Walletbase)},
ExternalAPI: &ExternalAPI{redis: redis},
Logbase: &Logbase{svc: service.APIHost(cfg.Logbase)},
Taskbase: &Taskbase{svc: service.APIHost(cfg.Taskbase)},
Userbase: &Userbase{svc: service.APIHost(cfg.Userbase)},
Walletbase: &Walletbase{svc: service.APIHost(cfg.Walletbase)},
}
}

Expand Down Expand Up @@ -103,3 +105,13 @@ func (i *QueryIdCn) Validate() error {
}
return nil
}

type Currency struct {
Name string `json:"name" cbor:"name"`
Alpha string `json:"alpha" cbor:"alpha"`
Decimals uint8 `json:"decimals" cbor:"decimals"`
Code uint16 `json:"code" cbor:"code"`
Rate float32 `json:"exchange_rate" cbor:"exchange_rate"` // HKD: 10000
}

type Currencies []Currency
91 changes: 91 additions & 0 deletions src/bll/external_api.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
package bll

import (
"context"
"errors"
"net/http"
"sync/atomic"
"time"

"github.com/teambition/gear"
"github.com/yiwen-ai/wallet-api/src/service"
"github.com/yiwen-ai/wallet-api/src/util"
)

var userAgent string = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36"

type ExternalAPI struct {
redis *service.Redis
rates atomic.Pointer[ExchangeRatesOutput]
}

type ExchangeRatesOutput struct {
LastUpdate uint `json:"last_update" cbor:"last_update"` // unix timestamp in seconds
NextUpdate uint `json:"next_update" cbor:"next_update"`
Base string `json:"base" cbor:"base"` // base should be "HKD"
Rates map[string]float32 `json:"rates" cbor:"rates"`
}

func (b *ExternalAPI) ExchangeRate(ctx context.Context) (*ExchangeRatesOutput, error) {
v := b.rates.Load()
if v == nil {
v := &ExchangeRatesOutput{}
_ = b.redis.GetCBOR(ctx, "exchange_rates", &v)
}

r := util.Int63n(7200)
if v != nil && time.Now().Unix()-r < int64(v.LastUpdate) {
return v, nil
}

if v != nil && v.LastUpdate > 0 {
// we should update later
go b.exchangeRate(ctx)
return v, nil
}

// we should update now:
return b.exchangeRate(ctx)
}

func (b *ExternalAPI) exchangeRate(ctx context.Context) (*ExchangeRatesOutput, error) {

// https://www.exchangerate-api.com/docs
ctxHeader := make(util.ContextHTTPHeader)
http.Header(ctxHeader).Set("User-Agent", userAgent)
ctx = gear.CtxWith[util.ContextHTTPHeader](ctx, &ctxHeader)

type exchangeRateOutput struct {
Result string `json:"result"`
LastUpdate uint `json:"time_last_update_unix"`
NextUpdate uint `json:"time_next_update_unix"`
// should be "HKD"
Base string `json:"base_code"`
Rates map[string]float32 `json:"conversion_rates"`
}

output := &exchangeRateOutput{}
api := "https://v6.exchangerate-api.com/v6/245ef0a5e7b4a1799b2d9a64/latest/HKD"
err := util.RequestJSON(ctx, util.ExternalHTTPClient, http.MethodGet, api, nil, output)
if err != nil {
return nil, err
}

if output.Result != "success" {
return nil, errors.New("fetch exchange rate failed")
}

rate := &ExchangeRatesOutput{
LastUpdate: output.LastUpdate,
NextUpdate: output.NextUpdate,
Base: output.Base,
Rates: output.Rates,
}

b.rates.Store(rate)
if err = b.redis.SetCBOR(ctx, "exchange_rates", rate, 0); err != nil {
return nil, err
}

return rate, nil
}
19 changes: 10 additions & 9 deletions src/bll/walletbase.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,19 +10,20 @@ import (
)

type Walletbase struct {
svc service.APIHost
svc service.APIHost
Currencies Currencies
}

type Currency struct {
Name string `json:"name" cbor:"name"`
Alpha string `json:"alpha" cbor:"alpha"`
Decimals uint8 `json:"decimals" cbor:"decimals"`
Code uint16 `json:"code" cbor:"code"`
MinAmount uint `json:"min_amount" cbor:"min_amount"`
MaxAmount uint `json:"max_amount" cbor:"max_amount"`
func (b *Walletbase) InitApp(ctx context.Context, _ *gear.App) error {
output, err := b.listCurrencies(ctx)
if err != nil {
return err
}
b.Currencies = output
return nil
}

func (b *Walletbase) ListCurrencies(ctx context.Context) ([]Currency, error) {
func (b *Walletbase) listCurrencies(ctx context.Context) ([]Currency, error) {
output := SuccessResponse[[]Currency]{}
if err := b.svc.Get(ctx, "/currencies", &output); err != nil {
return nil, err
Expand Down
6 changes: 6 additions & 0 deletions src/conf/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,11 @@ type Base struct {
Walletbase string `json:"walletbase" toml:"walletbase"`
}

type Redis struct {
Prefix string `json:"prefix" toml:"prefix"`
Node string `json:"node" toml:"node"`
}

// ConfigTpl ...
type ConfigTpl struct {
Rand *rand.Rand
Expand All @@ -64,6 +69,7 @@ type ConfigTpl struct {
Env string `json:"env" toml:"env"`
Logger Logger `json:"log" toml:"log"`
Server Server `json:"server" toml:"server"`
Redis Redis `json:"redis" toml:"redis"`
Base Base `json:"base" toml:"base"`

globalJobs int64 // global async jobs counter for graceful shutdown
Expand Down
Loading

0 comments on commit f368891

Please sign in to comment.