diff --git a/config/config.go b/config/config.go index cd70040..eee9623 100644 --- a/config/config.go +++ b/config/config.go @@ -24,6 +24,7 @@ type Config struct { DiscordBot *DiscordBot `yaml:"discord"` Telegram *Telegram `yaml:"telegram"` Notification *Notification `yaml:"notification"` + Market *Market `yaml:"market"` } type Database struct { @@ -86,6 +87,15 @@ type ZapToMail struct { Templates map[string]string `yaml:"templates"` } +type Market struct { + P2B *P2B `yaml:"p2b"` +} + +type P2B struct { + APIKey string `yaml:"api_key"` + SecretKey string `yaml:"secret_key"` +} + func Load(path string) (*Config, error) { payload, err := os.ReadFile(path) if err != nil { diff --git a/internal/engine/command/market/price.go b/internal/engine/command/market/price.go index 77cc2bd..5b723b0 100644 --- a/internal/engine/command/market/price.go +++ b/internal/engine/command/market/price.go @@ -15,12 +15,17 @@ func (m *Market) getPrice(_ *entity.User, cmd *command.Command, _ map[string]str 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")) + xeggexPrice, xeggexErr := strconv.ParseFloat(priceData.XeggexPacToUSDT.LastPrice, 64) + p2bPrice, p2bErr := strconv.ParseFloat(priceData.P2BPacToUSDT.LastPrice, 64) + + if xeggexErr != nil && p2bErr != nil { + return cmd.ErrorResult(fmt.Errorf("pagu can not calculate the price. Please try again later")) } - return cmd.SuccessfulResult("PAC Price: %f USDT"+ - "\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) + return cmd.SuccessfulResult("Xeggex Price: %f USDT\n P2B Price: %f USDT"+ + "\n\n\n See below markets link for more details: \n "+ + "p2b: https://p2pb2b.com/trade/PAC_USDT/ \n "+ + "xeggex: https://xeggex.com/market/PACTUS_USDT \n "+ + "exbitron: https://exbitron.com/trade?market=PAC-USDT", + xeggexPrice, p2bPrice) } diff --git a/internal/engine/command/market/price_test.go b/internal/engine/command/market/price_test.go index 7da7a25..2a0aff7 100644 --- a/internal/engine/command/market/price_test.go +++ b/internal/engine/command/market/price_test.go @@ -13,7 +13,7 @@ import ( func setup() (*Market, *command.Command) { priceCache := cache.NewBasic[string, entity.Price](1 * time.Second) - priceJob := job.NewPrice(priceCache) + priceJob := job.NewPrice(priceCache, "", "") priceJobSched := job.NewScheduler() priceJobSched.Submit(priceJob) go priceJobSched.Run() diff --git a/internal/engine/engine.go b/internal/engine/engine.go index ad6fbd1..c3bd862 100644 --- a/internal/engine/engine.go +++ b/internal/engine/engine.go @@ -16,7 +16,6 @@ import ( "github.com/pagu-project/Pagu/internal/entity" "github.com/pagu-project/Pagu/internal/job" "github.com/pagu-project/Pagu/internal/repository" - "github.com/pagu-project/Pagu/pkg/amount" "github.com/pagu-project/Pagu/pkg/cache" "github.com/pagu-project/Pagu/pkg/client" "github.com/pagu-project/Pagu/pkg/log" @@ -105,7 +104,40 @@ func NewBotEngine(cfg *config.Config) (*BotEngine, error) { go mailSenderSched.Run() } - return newBotEngine(ctx, cancel, db, cm, wlt, cfg.Phoenix.FaucetAmount), nil + priceCache := cache.NewBasic[string, entity.Price](10 * time.Second) + if cfg.BotName == config.BotNamePaguMainnet { + priceJob := job.NewPrice(priceCache, cfg.Market.P2B.APIKey, cfg.Market.P2B.SecretKey) + priceJobSched := job.NewScheduler() + priceJobSched.Submit(priceJob) + go priceJobSched.Run() + } + + netCmd := network.NewNetwork(ctx, cm) + calcCmd := calculator.NewCalculator(cm) + phoenixCmd := phoenixtestnet.NewPhoenix(ctx, wlt, cfg.Phoenix.FaucetAmount, cm, db) + voucherCmd := voucher.NewVoucher(db, wlt, cm) + marketCmd := market.NewMarket(cm, priceCache) + + rootCmd := &command.Command{ + Emoji: "🤖", + Name: "pagu", + Help: "Root Command", + AppIDs: entity.AllAppIDs(), + SubCommands: make([]*command.Command, 0), + } + + return &BotEngine{ + ctx: ctx, + cancel: cancel, + clientMgr: cm, + db: db, + rootCmd: rootCmd, + networkCmd: netCmd, + calculatorCmd: calcCmd, + phoenixCmd: phoenixCmd, + voucherCmd: voucherCmd, + marketCmd: marketCmd, + }, nil } func (be *BotEngine) Commands() []*command.Command { @@ -237,45 +269,3 @@ func (be *BotEngine) Start() { be.clientMgr.Start() } - -func newBotEngine(ctx context.Context, - cnl context.CancelFunc, - db repository.Database, - cm client.Manager, - wlt wallet.IWallet, - phoenixFaucetAmount amount.Amount, -) *BotEngine { - rootCmd := &command.Command{ - Emoji: "🤖", - Name: "pagu", - Help: "Root Command", - AppIDs: entity.AllAppIDs(), - SubCommands: make([]*command.Command, 0), - } - - // price caching job - priceCache := cache.NewBasic[string, entity.Price](10 * time.Second) - priceJob := job.NewPrice(priceCache) - priceJobSched := job.NewScheduler() - priceJobSched.Submit(priceJob) - go priceJobSched.Run() - - netCmd := network.NewNetwork(ctx, cm) - calcCmd := calculator.NewCalculator(cm) - phoenixCmd := phoenixtestnet.NewPhoenix(ctx, wlt, phoenixFaucetAmount, cm, db) - voucherCmd := voucher.NewVoucher(db, wlt, cm) - marketCmd := market.NewMarket(cm, priceCache) - - return &BotEngine{ - ctx: ctx, - cancel: cnl, - clientMgr: cm, - db: db, - rootCmd: rootCmd, - networkCmd: netCmd, - calculatorCmd: calcCmd, - phoenixCmd: phoenixCmd, - voucherCmd: voucherCmd, - marketCmd: marketCmd, - } -} diff --git a/internal/entity/price.go b/internal/entity/price.go index bf3b445..cf10833 100644 --- a/internal/entity/price.go +++ b/internal/entity/price.go @@ -3,6 +3,7 @@ package entity type Price struct { XeggexPacToUSDT XeggexPriceResponse ExbitronPacToUSDT ExbitronPriceResponse + P2BPacToUSDT P2BPriceResponse } type XeggexPriceResponse struct { @@ -19,6 +20,10 @@ type XeggexPriceResponse struct { MarketCap float64 `json:"marketcapNumber"` } +type P2BPriceResponse struct { + LastPrice string `json:"last"` +} + type ExbitronPriceResponse []struct { TickerID string `json:"ticker_id"` BaseCurrency string `json:"base_currency"` diff --git a/internal/job/price.go b/internal/job/price.go index 416aedc..efd39e2 100644 --- a/internal/job/price.go +++ b/internal/job/price.go @@ -1,7 +1,12 @@ package job import ( + "bytes" "context" + "crypto/hmac" + "crypto/sha512" + "encoding/base64" + "encoding/hex" "encoding/json" "fmt" "net/http" @@ -17,6 +22,12 @@ import ( const ( _defaultXeggexPriceEndpoint = "https://api.xeggex.com/api/v2/market/getbysymbol/Pactus%2Fusdt" _defaultExbitronPriceEndpoint = "https://api.exbitron.digital/api/v1/cg/tickers" + _defaultP2BPriceEndpoint = "https://api.p2pb2b.com/api/v2/all/ticker?market=PAC_USDT" +) + +var ( + P2BAPIKey string + P2BSecretKey string ) type price struct { @@ -28,8 +39,12 @@ type price struct { func NewPrice( cch cache.Cache[string, entity.Price], + p2bAPIKey, p2bSecretKey string, ) Job { ctx, cancel := context.WithCancel(context.Background()) + P2BAPIKey = p2bAPIKey + P2BSecretKey = p2bSecretKey + return &price{ cache: cch, ticker: time.NewTicker(128 * time.Second), @@ -49,10 +64,12 @@ func (p *price) start() { price entity.Price xeggex entity.XeggexPriceResponse exbitron entity.ExbitronPriceResponse + p2b entity.P2BPriceResponse ) ctx := context.Background() + // xeggex wg.Add(1) go func() { defer wg.Done() @@ -62,6 +79,7 @@ func (p *price) start() { } }() + // exbitron wg.Add(1) go func() { defer wg.Done() @@ -70,10 +88,23 @@ func (p *price) start() { } }() + // p2b + wg.Add(1) + go func() { + defer wg.Done() + p2pResp, err := p2bPrice(ctx, P2BAPIKey, P2BSecretKey) + if err != nil { + log.Error(err.Error()) + return + } + p2b = p2pResp + }() + wg.Wait() price.XeggexPacToUSDT = xeggex price.ExbitronPacToUSDT = exbitron + price.P2BPacToUSDT = p2b ok := p.cache.Exists(config.PriceCacheKey) if ok { @@ -121,3 +152,65 @@ func (p *price) getPrice(ctx context.Context, endpoint string, priceResponse any func (p *price) Stop() { p.ticker.Stop() } + +func p2bPrice(ctx context.Context, apiKey, secretKey string) (entity.P2BPriceResponse, error) { + payload := struct { + Request string `json:"request"` + Nonce string `json:"nonce"` + }{ + Request: "/api/v2/all/ticker", + Nonce: fmt.Sprintf("%d", time.Now().UnixMilli()), + } + + payloadByte, err := json.Marshal(payload) + if err != nil { + return entity.P2BPriceResponse{}, err + } + + cli := http.DefaultClient + req, err := http.NewRequestWithContext(ctx, http.MethodPost, _defaultP2BPriceEndpoint, bytes.NewBuffer(payloadByte)) + if err != nil { + return entity.P2BPriceResponse{}, err + } + + base64Data := base64.StdEncoding.EncodeToString(payloadByte) + h := hmac.New(sha512.New, []byte(secretKey)) + if _, err := h.Write([]byte(base64Data)); err != nil { + return entity.P2BPriceResponse{}, err + } + hmacData := h.Sum(nil) + hmacHex := hex.EncodeToString(hmacData) + + req.Header.Add("Content-Type", "application/json") + req.Header.Add("X-TXC-APIKEY", apiKey) + req.Header.Add("X-TXC-PAYLOAD", base64Data) + req.Header.Add("X-TXC-SIGNATURE", hmacHex) + + resp, err := cli.Do(req) + if err != nil { + return entity.P2BPriceResponse{}, err + } + + defer func() { + _ = resp.Body.Close() + }() + + if resp.StatusCode != http.StatusOK { + return entity.P2BPriceResponse{}, fmt.Errorf("p2b request failed with status: %v", resp.StatusCode) + } + + res := struct { + Result entity.P2BPriceResponse `json:"result"` + Success bool `json:"success"` + ErrorCode string `json:"errorCode"` + }{} + if err := json.NewDecoder(resp.Body).Decode(&res); err != nil { + return entity.P2BPriceResponse{}, err + } + + if !res.Success { + return entity.P2BPriceResponse{}, fmt.Errorf("p2b response failed with code: %s", res.ErrorCode) + } + + return res.Result, nil +}