Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(market command): add price sub command and market command #9

Merged
merged 1 commit into from
Jun 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading