forked from pagu-project/pagu
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(market command): add price sub command and market command
- Loading branch information
mj
committed
Jun 13, 2024
1 parent
71d8821
commit 83b8798
Showing
14 changed files
with
676 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
package config | ||
|
||
const ( | ||
PriceCacheKey = "PriceCacheKey" | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,52 @@ | ||
package market | ||
|
||
import ( | ||
"github.com/pagu-project/Pagu/internal/engine/command" | ||
"github.com/pagu-project/Pagu/internal/entity" | ||
"github.com/pagu-project/Pagu/pkg/cache" | ||
"github.com/pagu-project/Pagu/pkg/client" | ||
) | ||
|
||
const ( | ||
CommandName = "market" | ||
PriceCommandName = "price" | ||
HelpCommandName = "help" | ||
) | ||
|
||
type Market struct { | ||
clientMgr *client.Mgr | ||
priceCache cache.Cache[string, entity.Price] | ||
} | ||
|
||
func NewMarket(clientMgr *client.Mgr, priceCache cache.Cache[string, entity.Price]) Market { | ||
return Market{ | ||
clientMgr: clientMgr, | ||
priceCache: priceCache, | ||
} | ||
} | ||
|
||
func (m *Market) GetCommand() command.Command { | ||
subCmdPrice := command.Command{ | ||
Name: PriceCommandName, | ||
Desc: "Shows the last price of PAC coin on the markets", | ||
Help: "", | ||
Args: []command.Args{}, | ||
SubCommands: nil, | ||
AppIDs: command.AllAppIDs(), | ||
Handler: m.getPrice, | ||
} | ||
|
||
cmdMarket := command.Command{ | ||
Name: CommandName, | ||
Desc: "Blockchain data and information", | ||
Help: "", | ||
Args: nil, | ||
AppIDs: command.AllAppIDs(), | ||
SubCommands: make([]command.Command, 0), | ||
Handler: nil, | ||
} | ||
|
||
cmdMarket.AddSubCommand(subCmdPrice) | ||
|
||
return cmdMarket | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
package market | ||
|
||
import ( | ||
"fmt" | ||
"strconv" | ||
|
||
"github.com/pagu-project/Pagu/config" | ||
"github.com/pagu-project/Pagu/internal/engine/command" | ||
) | ||
|
||
func (m *Market) getPrice(cmd command.Command, _ command.AppID, _ string, _ ...string) command.CommandResult { | ||
priceData, ok := m.priceCache.Get(config.PriceCacheKey) | ||
if !ok { | ||
return cmd.ErrorResult(fmt.Errorf("failed to get price from markets. please try again later")) | ||
} | ||
|
||
lastPrice, err := strconv.ParseFloat(priceData.XeggexPacToUSDT.LastPrice, 64) | ||
if err != nil { | ||
return cmd.ErrorResult(fmt.Errorf("pagu can not calculate the price. please try again later")) | ||
} | ||
|
||
return cmd.SuccessfulResult("PAC Price: %f"+ | ||
"\n\n\n See below markets link for more details: \n xeggex: https://xeggex.com/market/PACTUS_USDT \n exbitron: https://exbitron.com/trade?market=PAC-USDT", lastPrice) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
package market | ||
|
||
import ( | ||
"testing" | ||
"time" | ||
|
||
"github.com/pagu-project/Pagu/internal/engine/command" | ||
"github.com/pagu-project/Pagu/internal/entity" | ||
"github.com/pagu-project/Pagu/internal/job" | ||
"github.com/pagu-project/Pagu/pkg/cache" | ||
"github.com/stretchr/testify/assert" | ||
) | ||
|
||
func setup() (Market, command.Command) { | ||
priceCache := cache.NewBasic[string, entity.Price](1 * time.Second) | ||
priceJob := job.NewPrice(priceCache) | ||
priceJobSched := job.NewScheduler() | ||
priceJobSched.Submit(priceJob) | ||
go priceJobSched.Run() | ||
m := NewMarket(nil, priceCache) | ||
|
||
return m, command.Command{ | ||
Name: PriceCommandName, | ||
Desc: "Shows the last price of PAC coin on the markets", | ||
Help: "", | ||
Args: []command.Args{}, | ||
SubCommands: nil, | ||
AppIDs: command.AllAppIDs(), | ||
} | ||
} | ||
|
||
func TestGetPrice(t *testing.T) { | ||
market, cmd := setup() | ||
time.Sleep(10 * time.Second) | ||
result := market.getPrice(cmd, command.AppIdDiscord, "") | ||
assert.Equal(t, result.Successful, true) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,69 @@ | ||
package entity | ||
|
||
type Price struct { | ||
XeggexPacToUSDT XeggexPriceResponse | ||
ExbitronPacToUSDT ExbitronPriceResponse | ||
} | ||
|
||
type XeggexPriceResponse struct { | ||
LastPrice string `json:"lastPrice"` | ||
YesterdayPrice string `json:"yesterdayPrice"` | ||
HighPrice string `json:"highPrice"` | ||
LowPrice string `json:"lowPrice"` | ||
Volume string `json:"volume"` | ||
Decimal int `json:"priceDecimals"` | ||
BestAsk string `json:"bestAsk"` | ||
BestBid string `json:"bestBid"` | ||
SpreadPercent string `json:"spreadPercent"` | ||
ChangePercent string `json:"changePercent"` | ||
MarketCap float64 `json:"marketcapNumber"` | ||
} | ||
|
||
type ExbitronPriceResponse []struct { | ||
TickerID string `json:"ticker_id"` | ||
BaseCurrency string `json:"base_currency"` | ||
TargetCurrency string `json:"target_currency"` | ||
LastPrice string `json:"last_price"` | ||
BaseVolume string `json:"base_volume"` | ||
TargetVolume string `json:"target_volume"` | ||
Bid string `json:"bid"` | ||
Ask string `json:"ask"` | ||
High string `json:"high"` | ||
Low string `json:"low"` | ||
} | ||
|
||
type ExbitronTicker struct { | ||
TickerId string `json:"ticker_id"` | ||
BaseCurrency string `json:"base_currency"` | ||
TargetCurrency string `json:"target_currency"` | ||
LastPrice string `json:"last_price"` | ||
BaseVolume string `json:"base_volume"` | ||
TargetVolume string `json:"target_volume"` | ||
Bid string `json:"bid"` | ||
Ask string `json:"ask"` | ||
High string `json:"high"` | ||
Low string `json:"low"` | ||
} | ||
|
||
func (e ExbitronPriceResponse) GetPacToUSDT() ExbitronTicker { | ||
const tickerId = "PAC-USDT" | ||
|
||
for _, ticker := range e { | ||
if ticker.TickerID == tickerId { | ||
return ExbitronTicker{ | ||
TickerId: tickerId, | ||
BaseCurrency: ticker.BaseCurrency, | ||
TargetCurrency: ticker.TargetCurrency, | ||
LastPrice: ticker.LastPrice, | ||
BaseVolume: ticker.BaseVolume, | ||
TargetVolume: ticker.TargetVolume, | ||
Bid: ticker.Bid, | ||
Ask: ticker.Ask, | ||
High: ticker.High, | ||
Low: ticker.Low, | ||
} | ||
} | ||
} | ||
|
||
return ExbitronTicker{} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
package job | ||
|
||
type Job interface { | ||
Start() | ||
Stop() | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,122 @@ | ||
package job | ||
|
||
import ( | ||
"context" | ||
"encoding/json" | ||
"fmt" | ||
"net/http" | ||
"sync" | ||
"time" | ||
|
||
"github.com/pagu-project/Pagu/config" | ||
"github.com/pagu-project/Pagu/internal/entity" | ||
"github.com/pagu-project/Pagu/pkg/cache" | ||
"github.com/pagu-project/Pagu/pkg/log" | ||
) | ||
|
||
const ( | ||
_defaultXeggexPriceEndpoint = "https://api.xeggex.com/api/v2/market/getbysymbol/Pactus%2Fusdt" | ||
_defaultExbitronPriceEndpoint = "https://api.exbitron.digital/api/v1/cg/tickers" | ||
) | ||
|
||
type price struct { | ||
cache cache.Cache[string, entity.Price] | ||
ticker *time.Ticker | ||
ctx context.Context | ||
cancel context.CancelFunc | ||
} | ||
|
||
func NewPrice( | ||
cache cache.Cache[string, entity.Price], | ||
) Job { | ||
ctx, cancel := context.WithCancel(context.Background()) | ||
return &price{ | ||
cache: cache, | ||
ticker: time.NewTicker(128 * time.Second), | ||
ctx: ctx, | ||
cancel: cancel, | ||
} | ||
} | ||
|
||
func (p *price) Start() { | ||
p.start() | ||
go p.runTicker() | ||
} | ||
|
||
func (p *price) start() { | ||
var ( | ||
wg sync.WaitGroup | ||
price entity.Price | ||
xeggex entity.XeggexPriceResponse | ||
exbitron entity.ExbitronPriceResponse | ||
) | ||
|
||
ctx := context.Background() | ||
|
||
wg.Add(1) | ||
go func() { | ||
defer wg.Done() | ||
if err := p.getPrice(ctx, _defaultXeggexPriceEndpoint, &xeggex); err != nil { | ||
log.Error(err.Error()) | ||
return | ||
} | ||
}() | ||
|
||
wg.Add(1) | ||
go func() { | ||
defer wg.Done() | ||
if err := p.getPrice(ctx, _defaultExbitronPriceEndpoint, &exbitron); err != nil { | ||
log.Error(err.Error()) | ||
} | ||
}() | ||
|
||
wg.Wait() | ||
|
||
price.XeggexPacToUSDT = xeggex | ||
price.ExbitronPacToUSDT = exbitron | ||
|
||
ok := p.cache.Exists(config.PriceCacheKey) | ||
if ok { | ||
p.cache.Update(config.PriceCacheKey, price, 0) | ||
} else { | ||
p.cache.Add(config.PriceCacheKey, price, 0) | ||
} | ||
} | ||
|
||
func (p *price) runTicker() { | ||
for { | ||
select { | ||
case <-p.ctx.Done(): | ||
return | ||
|
||
case <-p.ticker.C: | ||
p.start() | ||
} | ||
} | ||
} | ||
|
||
func (p *price) getPrice(ctx context.Context, endpoint string, priceResponse any) error { | ||
cli := http.DefaultClient | ||
|
||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
resp, err := cli.Do(req) | ||
if err != nil { | ||
return err | ||
} | ||
defer resp.Body.Close() | ||
|
||
if resp.StatusCode != http.StatusOK { | ||
return fmt.Errorf("response code is %v", resp.StatusCode) | ||
} | ||
|
||
dec := json.NewDecoder(resp.Body) | ||
return dec.Decode(priceResponse) | ||
} | ||
|
||
func (p *price) Stop() { | ||
p.ticker.Stop() | ||
} |
Oops, something went wrong.