Skip to content

Commit

Permalink
feat(market command): add price sub command and market command
Browse files Browse the repository at this point in the history
  • Loading branch information
mj committed Jun 13, 2024
1 parent 71d8821 commit 83b8798
Show file tree
Hide file tree
Showing 14 changed files with 676 additions and 0 deletions.
1 change: 1 addition & 0 deletions cmd/discord/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ func runCommand(parentCmd *cobra.Command) {

// Initialize global logger.
log.InitGlobalLogger(configs.Logger)

// starting botEngine.
botEngine, err := engine.NewBotEngine(configs)
pCmd.ExitOnError(cmd, err)
Expand Down
5 changes: 5 additions & 0 deletions config/global.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package config

const (
PriceCacheKey = "PriceCacheKey"
)
52 changes: 52 additions & 0 deletions internal/engine/command/market/market.go
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
}
24 changes: 24 additions & 0 deletions internal/engine/command/market/price.go
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)
}
37 changes: 37 additions & 0 deletions internal/engine/command/market/price_test.go
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)
}
16 changes: 16 additions & 0 deletions internal/engine/engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@ package engine

import (
"context"
"time"

"github.com/pagu-project/Pagu/internal/engine/command/market"
"github.com/pagu-project/Pagu/internal/entity"
"github.com/pagu-project/Pagu/internal/job"
"github.com/pagu-project/Pagu/pkg/cache"

"github.com/pagu-project/Pagu/internal/repository"

Expand Down Expand Up @@ -29,6 +35,7 @@ type BotEngine struct {
networkCmd network.Network
phoenixCmd phoenixtestnet.Phoenix
zealyCmd zealy.Zealy
marketCmd market.Market
}

func NewBotEngine(cfg *config.Config) (*BotEngine, error) {
Expand Down Expand Up @@ -117,10 +124,18 @@ func newBotEngine(cm, ptcm *client2.Mgr, wallet *wallet.Wallet, phoenixWal *wall
SubCommands: make([]command.Command, 3),
}

// price caching job
priceCache := cache.NewBasic[string, entity.Price](0 * time.Second)
priceJob := job.NewPrice(priceCache)
priceJobSched := job.NewScheduler()
priceJobSched.Submit(priceJob)
go priceJobSched.Run()

netCmd := network.NewNetwork(ctx, cm)
bcCmd := blockchain.NewBlockchain(cm)
ptCmd := phoenixtestnet.NewPhoenix(phoenixWal, ptcm, *db)
zCmd := zealy.NewZealy(db, wallet)
marketCmd := market.NewMarket(cm, priceCache)

return &BotEngine{
ctx: ctx,
Expand All @@ -132,6 +147,7 @@ func newBotEngine(cm, ptcm *client2.Mgr, wallet *wallet.Wallet, phoenixWal *wall
phoenixCmd: ptCmd,
phoenixClientMgr: ptcm,
zealyCmd: zCmd,
marketCmd: marketCmd,
}
}

Expand Down
69 changes: 69 additions & 0 deletions internal/entity/price.go
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{}
}
6 changes: 6 additions & 0 deletions internal/job/job.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package job

type Job interface {
Start()
Stop()
}
122 changes: 122 additions & 0 deletions internal/job/price.go
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()
}
Loading

0 comments on commit 83b8798

Please sign in to comment.