From 3e60e5f116ae9326e548ef764c5097b9baf7a162 Mon Sep 17 00:00:00 2001 From: b00f Date: Fri, 16 Feb 2024 04:20:24 +0800 Subject: [PATCH] feat: add nowpayments gateway (#63) --- .env.example | 9 +- Makefile | 4 +- config/config.go | 37 ++--- discord/commands.go | 36 ++--- discord/embeds.go | 8 +- discord/handlers.go | 18 +-- engine/engine.go | 71 +++++++--- engine/engine_test.go | 64 ++++----- engine/run.go | 58 ++++---- nowpayments/config.go | 11 ++ nowpayments/interface.go | 8 ++ nowpayments/mock.go | 68 +++++++++ nowpayments/nowpayments.go | 238 ++++++++++++++++++++++++++++++++ nowpayments/nowpayments_test.go | 32 +++++ store/interface.go | 25 ++-- store/mock.go | 28 ++-- store/store.go | 8 +- twitter_api/mock.go | 8 +- twitter_api/twitter.go | 2 +- 19 files changed, 562 insertions(+), 171 deletions(-) create mode 100644 nowpayments/config.go create mode 100644 nowpayments/interface.go create mode 100644 nowpayments/mock.go create mode 100644 nowpayments/nowpayments.go create mode 100644 nowpayments/nowpayments_test.go diff --git a/.env.example b/.env.example index d7a58749..1ddf9555 100644 --- a/.env.example +++ b/.env.example @@ -9,6 +9,11 @@ DISCORD_TOKEN= DISCORD_GUILD_ID= TWITTER_BEARER_TOKEN= TWITTER_ID= -TURBOSWAP_API_TOKEN= -TURBOSWAP_URL= AUTHORIZED_DISCORD_IDS= +NOWPAYMENTS_LISTEN_PORT=50055 +NOWPAYMENTS_WEBHOOK= +NOWPAYMENTS_API_URL=https://api-sandbox.nowpayments.io +NOWPAYMENTS_API_KEY= +NOWPAYMENTS_IPN_SECRET= +NOWPAYMENTS_USERNAME= +NOWPAYMENTS_PASSWORD= \ No newline at end of file diff --git a/Makefile b/Makefile index cbb0ec56..6dc34ee1 100644 --- a/Makefile +++ b/Makefile @@ -25,13 +25,13 @@ mock: mockgen -source=./wallet/interface.go -destination=./wallet/mock.go -package=wallet mockgen -source=./store/interface.go -destination=./store/mock.go -package=store mockgen -source=./twitter_api/interface.go -destination=./twitter_api/mock.go -package=twitter_api - mockgen -source=./turboswap/interface.go -destination=./turboswap/mock.go -package=turboswap + mockgen -source=./nowpayments/interface.go -destination=./nowpayments/mock.go -package=nowpayments ### Formatting, linting, and vetting fmt: gofumpt -l -w . go mod tidy - + check: golangci-lint run --build-tags "${BUILD_TAG}" --timeout=20m0s diff --git a/config/config.go b/config/config.go index 008a0af1..a42f8e57 100644 --- a/config/config.go +++ b/config/config.go @@ -6,25 +6,21 @@ import ( "strings" "github.com/joho/godotenv" + "github.com/kehiy/RoboPac/nowpayments" "github.com/pactus-project/pactus/util" ) type Config struct { - Network string - WalletAddress string - WalletPath string - WalletPassword string - NetworkNodes []string - LocalNode string - StorePath string - DiscordBotCfg DiscordBotConfig - TwitterAPICfg TwitterAPIConfig - TurboswapConfig TurboswapConfig -} - -type TurboswapConfig struct { - APIToken string - URL string + Network string + WalletAddress string + WalletPath string + WalletPassword string + NetworkNodes []string + LocalNode string + StorePath string + DiscordBotCfg DiscordBotConfig + TwitterAPICfg TwitterAPIConfig + NowPaymentsConfig nowpayments.Config } type TwitterAPIConfig struct { @@ -60,9 +56,14 @@ func Load(filePaths ...string) (*Config, error) { BearerToken: os.Getenv("TWITTER_BEARER_TOKEN"), TwitterID: os.Getenv("TWITTER_ID"), }, - TurboswapConfig: TurboswapConfig{ - APIToken: os.Getenv("TURBOSWAP_API_TOKEN"), - URL: os.Getenv("TURBOSWAP_URL"), + NowPaymentsConfig: nowpayments.Config{ + ListenPort: os.Getenv("NOWPAYMENTS_LISTEN_PORT"), + Webhook: os.Getenv("NOWPAYMENTS_WEBHOOK"), + APIToken: os.Getenv("NOWPAYMENTS_API_KEY"), + APIUrl: os.Getenv("NOWPAYMENTS_API_URL"), + IPNSecret: os.Getenv("NOWPAYMENTS_IPN_SECRET"), + Username: os.Getenv("NOWPAYMENTS_USERNAME"), + Password: os.Getenv("NOWPAYMENTS_PASSWORD"), }, } diff --git a/discord/commands.go b/discord/commands.go index c53e0ca5..df00d80e 100644 --- a/discord/commands.go +++ b/discord/commands.go @@ -100,8 +100,8 @@ var commands = []*discordgo.ApplicationCommand{ Description: "TestNet reward claim status", }, { - Name: "twitter-campaign", - Description: "Get Twitter campaign discount code", + Name: "booster-payment", + Description: "Create payment link in Validator Booster Program", Options: []*discordgo.ApplicationCommandOption{ { Type: discordgo.ApplicationCommandOptionString, @@ -118,8 +118,8 @@ var commands = []*discordgo.ApplicationCommand{ }, }, { - Name: "twitter-campaign-status", - Description: "Status of Twitter campaign", + Name: "booster-claim", + Description: "Claim the stake PAC coin in Validator Booster Program", Options: []*discordgo.ApplicationCommandOption{ { Type: discordgo.ApplicationCommandOptionString, @@ -130,8 +130,8 @@ var commands = []*discordgo.ApplicationCommand{ }, }, { - Name: "twitter-campaign-whitelist", - Description: "Whitelist a non-active Twitter account for Twitter campaign", + Name: "booster-whitelist", + Description: "Whitelist a non-active Twitter account in Validator Booster Program", Options: []*discordgo.ApplicationCommandOption{ { Type: discordgo.ApplicationCommandOptionString, @@ -144,16 +144,16 @@ var commands = []*discordgo.ApplicationCommand{ } var commandHandlers = map[string]func(*DiscordBot, *discordgo.Session, *discordgo.InteractionCreate){ - "help": helpCommandHandler, - "claim": claimCommandHandler, - "claimer-info": claimerInfoCommandHandler, - "node-info": nodeInfoCommandHandler, - "network-health": networkHealthCommandHandler, - "network-status": networkStatusCommandHandler, - "wallet": walletCommandHandler, - "claim-status": claimStatusCommandHandler, - "reward-calc": rewardCalcCommandHandler, - "twitter-campaign": twitterCampaignCommandHandler, - "twitter-campaign-status": twitterCampaignStatusCommandHandler, - "twitter-campaign-whitelist": twitterCampaignWhitelistCommandHandler, + "help": helpCommandHandler, + "claim": claimCommandHandler, + "claimer-info": claimerInfoCommandHandler, + "node-info": nodeInfoCommandHandler, + "network-health": networkHealthCommandHandler, + "network-status": networkStatusCommandHandler, + "wallet": walletCommandHandler, + "claim-status": claimStatusCommandHandler, + "reward-calc": rewardCalcCommandHandler, + "booster-payment": boosterPaymentCommandHandler, + "booster-claim": boosterClaimCommandHandler, + "booster-whitelist": boosterWhitelistCommandHandler, } diff --git a/discord/embeds.go b/discord/embeds.go index e3054bb8..bd3955fd 100644 --- a/discord/embeds.go +++ b/discord/embeds.go @@ -23,8 +23,8 @@ func helpEmbed(s *discordgo.Session) *discordgo.MessageEmbed { "```/network-status``` Shows a brief info about network.\n" + "```/network-health``` Check and shows network health status.\n" + "```/wallet``` Shows RoboPac wallet address and balance.\n" + - "```/twitter-campaign``` Get discount code on Twitter Campaign.\n" + - "```/twitter-campaign-status``` Check the status of Twitter Campaign.\n", + "```/booster-payment``` Create payment link in Validator Booster Program.\n" + + "```/booster-claim``` Claim the stake PAC coin in Validator Booster Program.\n", Color: PACTUS, } } @@ -93,9 +93,9 @@ func rewardCalcEmbed(s *discordgo.Session, i *discordgo.InteractionCreate, resul } } -func twitterCampaignEmbed(s *discordgo.Session, i *discordgo.InteractionCreate, result string) *discordgo.MessageEmbed { +func boosterEmbed(s *discordgo.Session, i *discordgo.InteractionCreate, result string) *discordgo.MessageEmbed { return &discordgo.MessageEmbed{ - Title: "Twitter(X) Campaign ๐•", + Title: "Pactus Validator Booster Program โœจ", Description: result, Color: PACTUS, } diff --git a/discord/handlers.go b/discord/handlers.go index edafcbf2..4aa1238b 100644 --- a/discord/handlers.go +++ b/discord/handlers.go @@ -182,7 +182,7 @@ func rewardCalcCommandHandler(db *DiscordBot, s *discordgo.Session, i *discordgo db.respondEmbed(embed, s, i) } -func twitterCampaignCommandHandler(db *DiscordBot, s *discordgo.Session, i *discordgo.InteractionCreate) { +func boosterPaymentCommandHandler(db *DiscordBot, s *discordgo.Session, i *discordgo.InteractionCreate) { if !checkMessage(i, s, db.GuildID, i.Member.User.ID) { return } @@ -190,46 +190,46 @@ func twitterCampaignCommandHandler(db *DiscordBot, s *discordgo.Session, i *disc twitterName := i.ApplicationCommandData().Options[0].StringValue() valAddr := i.ApplicationCommandData().Options[1].StringValue() - result, err := db.BotEngine.Run(fmt.Sprintf("twitter-campaign %v %v %v", i.Member.User.ID, twitterName, valAddr)) + result, err := db.BotEngine.Run(fmt.Sprintf("booster-payment %v %v %v", i.Member.User.ID, twitterName, valAddr)) if err != nil { db.respondErrMsg(err, s, i) return } - embed := twitterCampaignEmbed(s, i, result) + embed := boosterEmbed(s, i, result) db.respondEmbed(embed, s, i) } -func twitterCampaignStatusCommandHandler(db *DiscordBot, s *discordgo.Session, i *discordgo.InteractionCreate) { +func boosterClaimCommandHandler(db *DiscordBot, s *discordgo.Session, i *discordgo.InteractionCreate) { if !checkMessage(i, s, db.GuildID, i.Member.User.ID) { return } twitterID := i.ApplicationCommandData().Options[0].StringValue() - result, err := db.BotEngine.Run(fmt.Sprintf("twitter-campaign-status %v", twitterID)) + result, err := db.BotEngine.Run(fmt.Sprintf("booster-payment-claim %v", twitterID)) if err != nil { db.respondErrMsg(err, s, i) return } - embed := twitterCampaignEmbed(s, i, result) + embed := boosterEmbed(s, i, result) db.respondEmbed(embed, s, i) } -func twitterCampaignWhitelistCommandHandler(db *DiscordBot, s *discordgo.Session, i *discordgo.InteractionCreate) { +func boosterWhitelistCommandHandler(db *DiscordBot, s *discordgo.Session, i *discordgo.InteractionCreate) { if !checkMessage(i, s, db.GuildID, i.Member.User.ID) { return } twitterName := i.ApplicationCommandData().Options[0].StringValue() - result, err := db.BotEngine.Run(fmt.Sprintf("twitter-campaign-whitelist %v %v", twitterName, i.Member.User.ID)) + result, err := db.BotEngine.Run(fmt.Sprintf("booster-payment-whitelist %v %v", twitterName, i.Member.User.ID)) if err != nil { db.respondErrMsg(err, s, i) return } - embed := twitterCampaignEmbed(s, i, result) + embed := boosterEmbed(s, i, result) db.respondEmbed(embed, s, i) } diff --git a/engine/engine.go b/engine/engine.go index 318172fc..f456d4b1 100644 --- a/engine/engine.go +++ b/engine/engine.go @@ -11,25 +11,28 @@ import ( "github.com/kehiy/RoboPac/client" "github.com/kehiy/RoboPac/config" "github.com/kehiy/RoboPac/log" + "github.com/kehiy/RoboPac/nowpayments" "github.com/kehiy/RoboPac/store" - "github.com/kehiy/RoboPac/turboswap" "github.com/kehiy/RoboPac/twitter_api" "github.com/kehiy/RoboPac/utils" "github.com/kehiy/RoboPac/wallet" "github.com/libp2p/go-libp2p/core/peer" gonanoid "github.com/matoous/go-nanoid/v2" putils "github.com/pactus-project/pactus/util" + "github.com/pactus-project/pactus/util/logger" ) +var BoosterPrice = 30 + type BotEngine struct { ctx context.Context //nolint cancel func() - wallet wallet.IWallet - store store.IStore - turboswap turboswap.ITurboSwap - clientMgr *client.Mgr - logger *log.SubLogger + wallet wallet.IWallet + store store.IStore + nowpayments nowpayments.INowpayment + clientMgr *client.Mgr + logger *log.SubLogger twitterClient twitter_api.IClient @@ -88,17 +91,17 @@ func NewBotEngine(cfg *config.Config) (IEngine, error) { } log.Info("twitterClient loaded successfully") - turboswap, err := turboswap.NewTurboswap(cfg.TurboswapConfig.APIToken, cfg.TurboswapConfig.URL) + nowpayments, err := nowpayments.NewNowPayments(&cfg.NowPaymentsConfig) if err != nil { log.Error("could not start twitter client", "err", err) } - log.Info("turboswap loaded successfully") + log.Info("nowpayments loaded successfully") - return newBotEngine(eSl, cm, wallet, store, twitterClient, turboswap), nil + return newBotEngine(eSl, cm, wallet, store, twitterClient, nowpayments), nil } func newBotEngine(logger *log.SubLogger, cm *client.Mgr, w wallet.IWallet, s store.IStore, - twitterClient twitter_api.IClient, turboswap turboswap.ITurboSwap, + twitterClient twitter_api.IClient, nowpayments nowpayments.INowpayment, ) *BotEngine { ctx, cancel := context.WithCancel(context.Background()) @@ -110,7 +113,7 @@ func newBotEngine(logger *log.SubLogger, cm *client.Mgr, w wallet.IWallet, s sto clientMgr: cm, store: s, twitterClient: twitterClient, - turboswap: turboswap, + nowpayments: nowpayments, } } @@ -312,7 +315,7 @@ func (be *BotEngine) RewardCalculate(stake int64, t string) (int64, string, int6 return reward, time, int64(utils.ChangeToCoin(bi.TotalPower)), nil } -func (be *BotEngine) TwitterCampaign(discordID, twitterName, valAddr string) (*store.TwitterParty, error) { +func (be *BotEngine) BoosterPayment(discordID, twitterName, valAddr string) (*store.TwitterParty, error) { be.Lock() defer be.Unlock() @@ -355,13 +358,13 @@ func (be *BotEngine) TwitterCampaign(discordID, twitterName, valAddr string) (*s return nil, err } - discountCode, err := gonanoid.Generate("0123456789", 6) + discountCode, err := gonanoid.Generate("0123456789", 8) if err != nil { return nil, err } - totalPrice := 50 - amountInPAC := 150 + totalPrice := BoosterPrice + amountInPAC := int64(150) if userInfo.Followers > 1000 { amountInPAC = 200 } @@ -379,12 +382,12 @@ func (be *BotEngine) TwitterCampaign(discordID, twitterName, valAddr string) (*s CreatedAt: time.Now().Unix(), } - err = be.turboswap.SendDiscountCode(be.ctx, party) + err = be.nowpayments.CreatePayment(party) if err != nil { return nil, err } - err = be.store.AddTwitterParty(party) + err = be.store.SaveTwitterParty(party) if err != nil { return nil, err } @@ -392,20 +395,42 @@ func (be *BotEngine) TwitterCampaign(discordID, twitterName, valAddr string) (*s return party, nil } -func (be *BotEngine) TwitterCampaignStatus(twitterName string) (*store.TwitterParty, *turboswap.DiscountStatus, error) { +func (be *BotEngine) BoosterClaim(twitterName string) (*store.TwitterParty, error) { party := be.store.FindTwitterParty(twitterName) if party == nil { - return nil, nil, fmt.Errorf("no discount code generated for this Twitter account: `%v`", twitterName) + return nil, fmt.Errorf("no discount code generated for this Twitter account: `%v`", twitterName) } - status, err := be.turboswap.GetStatus(be.ctx, party) + err := be.nowpayments.UpdatePayment(party) if err != nil { - return nil, nil, err + return nil, err + } + + if party.NowPaymentsFinished { + if party.TransactionID == "" { + logger.Info("sending bond transaction", "receiver", party.ValAddr, "amount", party.AmountInPAC) + memo := "Booster Program" + txID, err := be.wallet.BondTransaction(party.ValPubKey, party.ValAddr, memo, utils.CoinToChange(float64(party.AmountInPAC))) + if err != nil { + return nil, err + } + + if txID == "" { + return nil, errors.New("can't send bond transaction") + } + + party.TransactionID = txID + + err = be.store.SaveTwitterParty(party) + if err != nil { + return nil, err + } + } } - return party, status, nil + return party, nil } -func (be *BotEngine) TwitterCampaignWhitelist(twitterName string, authorizedDiscordID string) error { +func (be *BotEngine) BoosterWhitelist(twitterName string, authorizedDiscordID string) error { authorizedIDs := []string{} if !slices.Contains(authorizedIDs, authorizedDiscordID) { diff --git a/engine/engine_test.go b/engine/engine_test.go index 48a1ff8b..391aa6a3 100644 --- a/engine/engine_test.go +++ b/engine/engine_test.go @@ -8,8 +8,8 @@ import ( "github.com/kehiy/RoboPac/client" "github.com/kehiy/RoboPac/log" + "github.com/kehiy/RoboPac/nowpayments" rpstore "github.com/kehiy/RoboPac/store" - "github.com/kehiy/RoboPac/turboswap" "github.com/kehiy/RoboPac/twitter_api" "github.com/kehiy/RoboPac/utils" "github.com/kehiy/RoboPac/wallet" @@ -19,7 +19,7 @@ import ( "go.uber.org/mock/gomock" ) -func setup(t *testing.T) (*BotEngine, *client.MockIClient, *rpstore.MockIStore, *wallet.MockIWallet, *twitter_api.MockIClient, *turboswap.MockITurboSwap) { +func setup(t *testing.T) (*BotEngine, *client.MockIClient, *rpstore.MockIStore, *wallet.MockIWallet, *twitter_api.MockIClient, *nowpayments.MockINowpayment) { t.Helper() ctrl := gomock.NewController(t) @@ -33,10 +33,10 @@ func setup(t *testing.T) (*BotEngine, *client.MockIClient, *rpstore.MockIStore, mockWallet := wallet.NewMockIWallet(ctrl) mockStore := rpstore.NewMockIStore(ctrl) mockTwitter := twitter_api.NewMockIClient(ctrl) - mockTurboswap := turboswap.NewMockITurboSwap(ctrl) + mockNowPayments := nowpayments.NewMockINowpayment(ctrl) - eng := newBotEngine(sl, cm, mockWallet, mockStore, mockTwitter, mockTurboswap) - return eng, mockClient, mockStore, mockWallet, mockTwitter, mockTurboswap + eng := newBotEngine(sl, cm, mockWallet, mockStore, mockTwitter, mockNowPayments) + return eng, mockClient, mockStore, mockWallet, mockTwitter, mockNowPayments } func TestNetworkStatus(t *testing.T) { @@ -497,7 +497,7 @@ func TestClaim(t *testing.T) { }) } -func TestTwitterCampaign(t *testing.T) { +func TestBoosterProgram(t *testing.T) { t.Run("staked validator", func(t *testing.T) { eng, client, store, _, _, _ := setup(t) @@ -513,7 +513,7 @@ func TestTwitterCampaign(t *testing.T) { &pactus.GetValidatorResponse{}, nil, ) - _, err := eng.TwitterCampaign(discordID, twitterName, valAddr) + _, err := eng.BoosterPayment(discordID, twitterName, valAddr) assert.Error(t, err) }) @@ -536,7 +536,7 @@ func TestTwitterCampaign(t *testing.T) { nil, nil, ) - _, err := eng.TwitterCampaign(discordID, twitterName, valAddr) + _, err := eng.BoosterPayment(discordID, twitterName, valAddr) assert.Error(t, err) }) @@ -572,7 +572,7 @@ func TestTwitterCampaign(t *testing.T) { nil, expectedErr, ) - _, err := eng.TwitterCampaign(discordID, twitterName, valAddr) + _, err := eng.BoosterPayment(discordID, twitterName, valAddr) assert.ErrorIs(t, err, expectedErr) }) @@ -615,7 +615,7 @@ func TestTwitterCampaign(t *testing.T) { }, nil, ) - _, err := eng.TwitterCampaign(discordID, twitterName, valAddr) + _, err := eng.BoosterPayment(discordID, twitterName, valAddr) assert.Error(t, err) }) @@ -659,7 +659,7 @@ func TestTwitterCampaign(t *testing.T) { }, nil, ) - _, err := eng.TwitterCampaign(discordID, twitterName, valAddr) + _, err := eng.BoosterPayment(discordID, twitterName, valAddr) assert.Error(t, err) }) @@ -708,12 +708,12 @@ func TestTwitterCampaign(t *testing.T) { nil, fmt.Errorf("not found"), ) - _, err := eng.TwitterCampaign(discordID, twitterName, valAddr) + _, err := eng.BoosterPayment(discordID, twitterName, valAddr) assert.Error(t, err) }) t.Run("active account, less that 1000 followers", func(t *testing.T) { - eng, client, store, _, twitter, turboswap := setup(t) + eng, client, store, _, twitter, nowPayments := setup(t) twitterName := "abcd" discordID := "123456789" @@ -759,19 +759,19 @@ func TestTwitterCampaign(t *testing.T) { CreatedAt: time.Now().AddDate(0, 0, -2), }, nil, ) - turboswap.EXPECT().SendDiscountCode(eng.ctx, gomock.Any()).Return( + nowPayments.EXPECT().CreatePayment(gomock.Any()).Return( nil, ) - store.EXPECT().AddTwitterParty(gomock.Any()).Return( + store.EXPECT().SaveTwitterParty(gomock.Any()).Return( nil, ) - party, err := eng.TwitterCampaign(discordID, twitterName, valAddr) + party, err := eng.BoosterPayment(discordID, twitterName, valAddr) assert.NoError(t, err) - assert.Equal(t, 150, party.AmountInPAC) - assert.Equal(t, 50, party.TotalPrice) + assert.Equal(t, int64(150), party.AmountInPAC) + assert.Equal(t, BoosterPrice, party.TotalPrice) assert.Equal(t, twitterName, party.TwitterName) assert.Equal(t, twitterID, party.TwitterID) assert.Equal(t, valAddr, party.ValAddr) @@ -779,7 +779,7 @@ func TestTwitterCampaign(t *testing.T) { }) t.Run("active account, more that 1000 followers", func(t *testing.T) { - eng, client, store, _, twitter, turboswap := setup(t) + eng, client, store, _, twitter, nowPayments := setup(t) twitterName := "abcd" discordID := "123456789" @@ -825,19 +825,19 @@ func TestTwitterCampaign(t *testing.T) { CreatedAt: time.Now().AddDate(0, 0, -2), }, nil, ) - turboswap.EXPECT().SendDiscountCode(eng.ctx, gomock.Any()).Return( + nowPayments.EXPECT().CreatePayment(gomock.Any()).Return( nil, ) - store.EXPECT().AddTwitterParty(gomock.Any()).Return( + store.EXPECT().SaveTwitterParty(gomock.Any()).Return( nil, ) - party, err := eng.TwitterCampaign(discordID, twitterName, valAddr) + party, err := eng.BoosterPayment(discordID, twitterName, valAddr) assert.NoError(t, err) - assert.Equal(t, 200, party.AmountInPAC) - assert.Equal(t, 50, party.TotalPrice) + assert.Equal(t, int64(200), party.AmountInPAC) + assert.Equal(t, BoosterPrice, party.TotalPrice) assert.Equal(t, twitterName, party.TwitterName) assert.Equal(t, twitterID, party.TwitterID) assert.Equal(t, valAddr, party.ValAddr) @@ -845,7 +845,7 @@ func TestTwitterCampaign(t *testing.T) { }) t.Run("verified account", func(t *testing.T) { - eng, client, store, _, twitter, turboswap := setup(t) + eng, client, store, _, twitter, nowPayments := setup(t) twitterName := "abcd" discordID := "123456789" @@ -888,20 +888,20 @@ func TestTwitterCampaign(t *testing.T) { }, nil, ) - turboswap.EXPECT().SendDiscountCode(eng.ctx, gomock.Any()).Return( + nowPayments.EXPECT().CreatePayment(gomock.Any()).Return( nil, ) - store.EXPECT().AddTwitterParty(gomock.Any()).Return( + store.EXPECT().SaveTwitterParty(gomock.Any()).Return( nil, ) - _, err := eng.TwitterCampaign(discordID, twitterName, valAddr) + _, err := eng.BoosterPayment(discordID, twitterName, valAddr) assert.NoError(t, err) }) t.Run("whitelisted account", func(t *testing.T) { - eng, client, store, _, twitter, turboswap := setup(t) + eng, client, store, _, twitter, nowPayments := setup(t) twitterName := "abcd" discordID := "123456789" @@ -948,15 +948,15 @@ func TestTwitterCampaign(t *testing.T) { }, nil, ) - turboswap.EXPECT().SendDiscountCode(eng.ctx, gomock.Any()).Return( + nowPayments.EXPECT().CreatePayment(gomock.Any()).Return( nil, ) - store.EXPECT().AddTwitterParty(gomock.Any()).Return( + store.EXPECT().SaveTwitterParty(gomock.Any()).Return( nil, ) - _, err := eng.TwitterCampaign(discordID, twitterName, valAddr) + _, err := eng.BoosterPayment(discordID, twitterName, valAddr) assert.NoError(t, err) }) } diff --git a/engine/run.go b/engine/run.go index 34f126ce..98a635e9 100644 --- a/engine/run.go +++ b/engine/run.go @@ -11,17 +11,17 @@ import ( ) const ( - CmdClaim = "claim" //! - CmdClaimerInfo = "claimer-info" //! - CmdNodeInfo = "node-info" //! - CmdNetworkStatus = "network" //! - CmdNetworkHealth = "network-health" //! - CmdBotWallet = "wallet" //! - CmdClaimStatus = "claim-status" //! - CmdRewardCalc = "calc-reward" //! - CmdTwitterCampaign = "twitter-campaign" //! - CmdTwitterCampaignStatus = "twitter-campaign-status" //! - CmdTwitterCampaignWhitelist = "twitter-campaign-whitelist" //! + CmdClaim = "claim" //! + CmdClaimerInfo = "claimer-info" //! + CmdNodeInfo = "node-info" //! + CmdNetworkStatus = "network" //! + CmdNetworkHealth = "network-health" //! + CmdBotWallet = "wallet" //! + CmdClaimStatus = "claim-status" //! + CmdRewardCalc = "calc-reward" //! + CmdBoosterPayment = "booster-payment" //! + CmdBoosterClaim = "booster-claim" //! + CmdBoosterWhitelist = "booster-whitelist" //! ) // The input is always string. @@ -144,7 +144,7 @@ func (be *BotEngine) Run(input string) (string, error) { "\n\n> Note๐Ÿ“: This is an estimation and the number can get changed by changes of your stake amount, total power and ...", utils.FormatNumber(reward), utils.FormatNumber(int64(stake)), time, utils.FormatNumber(totalPower)), nil - case CmdTwitterCampaign: + case CmdBoosterPayment: if err := CheckArgs(3, args); err != nil { return "", err } @@ -153,44 +153,50 @@ func (be *BotEngine) Run(input string) (string, error) { twitterName := args[1] valAddr := args[2] - party, err := be.TwitterCampaign(discordID, twitterName, valAddr) + party, err := be.BoosterPayment(discordID, twitterName, valAddr) if err != nil { return "", err } expiryDate := time.Unix(party.CreatedAt, 0).AddDate(0, 0, 7) - msg := fmt.Sprintf("Validator `%s` registered with Discount code `%v`."+ - " Visit https://app.turboswap.io/ to claim your discounted stake-PAC coins."+ + msg := fmt.Sprintf("Validator `%s` registered to receive %v stake-PAC coins in total price of $%v."+ + " Visit https://nowpayments.io/payment/?iid=%v to pay it."+ " The Discount code will expire on %v", - party.ValAddr, party.DiscountCode, expiryDate.Format("2006-01-02")) + party.ValAddr, party.AmountInPAC, party.TotalPrice, party.NowPaymentsInvoiceID, expiryDate.Format("2006-01-02")) return msg, nil - case CmdTwitterCampaignStatus: + case CmdBoosterClaim: if err := CheckArgs(1, args); err != nil { return "", err } twitterName := args[0] - party, _, err := be.TwitterCampaignStatus(twitterName) + party, err := be.BoosterClaim(twitterName) if err != nil { return "", err } - // check the status (tOdO) - expiryDate := time.Unix(party.CreatedAt, 0).AddDate(0, 0, 7) - msg := fmt.Sprintf("Validator `%s` registered with Discount code `%v`."+ - " Visit https://app.turboswap.io/ to claim your discounted stake-PAC coins."+ - " The Discount code will expire on %v", - party.ValAddr, party.DiscountCode, expiryDate.Format("2006-01-02")) + var msg string + if party.NowPaymentsFinished { + msg = fmt.Sprintf("Validator `%s` received %v stake-PAC coins."+ + " Transaction: https://http://pacscan.org/transactions/%v.", + party.ValAddr, party.AmountInPAC, party.TransactionID) + } else { + expiryDate := time.Unix(party.CreatedAt, 0).AddDate(0, 0, 7) + msg = fmt.Sprintf("Validator `%s` registered to receive %v stake-PAC coins in total price of $%v."+ + " Visit https://nowpayments.io/payment/?iid=%v and pay the total amount."+ + " The Discount code will expire on %v", + party.ValAddr, party.AmountInPAC, party.TotalPrice, party.NowPaymentsInvoiceID, expiryDate.Format("2006-01-02")) + } return msg, nil - case CmdTwitterCampaignWhitelist: + case CmdBoosterWhitelist: if err := CheckArgs(2, args); err != nil { return "", err } twitterName := args[0] authorizedDiscordID := args[1] - err := be.TwitterCampaignWhitelist(twitterName, authorizedDiscordID) + err := be.BoosterWhitelist(twitterName, authorizedDiscordID) if err != nil { return "", err } diff --git a/nowpayments/config.go b/nowpayments/config.go new file mode 100644 index 00000000..e99a1c1c --- /dev/null +++ b/nowpayments/config.go @@ -0,0 +1,11 @@ +package nowpayments + +type Config struct { + Webhook string + ListenPort string + APIToken string + IPNSecret string + APIUrl string + Username string + Password string +} diff --git a/nowpayments/interface.go b/nowpayments/interface.go new file mode 100644 index 00000000..d5e77efb --- /dev/null +++ b/nowpayments/interface.go @@ -0,0 +1,8 @@ +package nowpayments + +import "github.com/kehiy/RoboPac/store" + +type INowpayment interface { + CreatePayment(party *store.TwitterParty) error + UpdatePayment(party *store.TwitterParty) error +} diff --git a/nowpayments/mock.go b/nowpayments/mock.go new file mode 100644 index 00000000..57d471a5 --- /dev/null +++ b/nowpayments/mock.go @@ -0,0 +1,68 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: ./nowpayments/interface.go +// +// Generated by this command: +// +// mockgen -source=./nowpayments/interface.go -destination=./nowpayments/mock.go -package=nowpayments +// + +// Package nowpayments is a generated GoMock package. +package nowpayments + +import ( + reflect "reflect" + + store "github.com/kehiy/RoboPac/store" + gomock "go.uber.org/mock/gomock" +) + +// MockINowpayment is a mock of INowpayment interface. +type MockINowpayment struct { + ctrl *gomock.Controller + recorder *MockINowpaymentMockRecorder +} + +// MockINowpaymentMockRecorder is the mock recorder for MockINowpayment. +type MockINowpaymentMockRecorder struct { + mock *MockINowpayment +} + +// NewMockINowpayment creates a new mock instance. +func NewMockINowpayment(ctrl *gomock.Controller) *MockINowpayment { + mock := &MockINowpayment{ctrl: ctrl} + mock.recorder = &MockINowpaymentMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockINowpayment) EXPECT() *MockINowpaymentMockRecorder { + return m.recorder +} + +// CreatePayment mocks base method. +func (m *MockINowpayment) CreatePayment(party *store.TwitterParty) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CreatePayment", party) + ret0, _ := ret[0].(error) + return ret0 +} + +// CreatePayment indicates an expected call of CreatePayment. +func (mr *MockINowpaymentMockRecorder) CreatePayment(party any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreatePayment", reflect.TypeOf((*MockINowpayment)(nil).CreatePayment), party) +} + +// UpdatePayment mocks base method. +func (m *MockINowpayment) UpdatePayment(party *store.TwitterParty) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "UpdatePayment", party) + ret0, _ := ret[0].(error) + return ret0 +} + +// UpdatePayment indicates an expected call of UpdatePayment. +func (mr *MockINowpaymentMockRecorder) UpdatePayment(party any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdatePayment", reflect.TypeOf((*MockINowpayment)(nil).UpdatePayment), party) +} diff --git a/nowpayments/nowpayments.go b/nowpayments/nowpayments.go new file mode 100644 index 00000000..baaa3388 --- /dev/null +++ b/nowpayments/nowpayments.go @@ -0,0 +1,238 @@ +package nowpayments + +import ( + "bytes" + "crypto/hmac" + "crypto/sha512" + "encoding/base64" + "encoding/hex" + "encoding/json" + "fmt" + "io" + "net/http" + + "github.com/kehiy/RoboPac/store" + "github.com/pactus-project/pactus/util/logger" +) + +type NowPayments struct { + apiToken string + ipnSecret []byte + webhook string + apiURL string + username string + password string +} + +func NewNowPayments(cfg *Config) (*NowPayments, error) { + ipnSecret, err := base64.StdEncoding.DecodeString(cfg.IPNSecret) + if err != nil { + return nil, err + } + s := &NowPayments{ + apiToken: cfg.APIToken, + ipnSecret: ipnSecret, + apiURL: cfg.APIUrl, + webhook: cfg.Webhook, + username: cfg.Username, + password: cfg.Password, + } + http.HandleFunc("/nowpayments", s.webhookFunc) + + go func() { + for { + logger.Info("starting NowPayments webhook", "port", cfg.ListenPort) + err = http.ListenAndServe(fmt.Sprintf(":%v", cfg.ListenPort), nil) + if err != nil { + logger.Error("unable to start NowPayments webhook", "error", err) + } + } + }() + + return s, nil +} + +func (s *NowPayments) webhookFunc(w http.ResponseWriter, r *http.Request) { + logger.Debug("NowPayment webhook called") + + data, err := io.ReadAll(r.Body) + if err != nil { + w.WriteHeader(http.StatusBadRequest) + logger.Error("Callback read error", "error", err) + return + } + + logger.Debug("Callback result", "data", data) + msgMACHex := r.Header.Get("x-nowpayments-sig") + msgMAC, err := hex.DecodeString(msgMACHex) + if err != nil { + logger.Error("Invalid sig hex", "error", err) + w.WriteHeader(http.StatusBadRequest) + return + } + + mac := hmac.New(sha512.New, s.ipnSecret) + + var result map[string]interface{} + err = json.Unmarshal(data, &result) + if err != nil { + w.WriteHeader(http.StatusBadRequest) + logger.Error("json.Unmarshal read error", "error", err) + return + } + // marshal it again, it will be sorted + sortedData, _ := json.Marshal(result) + + if !bytes.Equal(sortedData, data) { + logger.Debug("data was not sorted") + } + + _, err = mac.Write(sortedData) + if err != nil { + w.WriteHeader(http.StatusBadRequest) + logger.Error("mac.Write read error", "error", err) + return + } + expectedMAC := mac.Sum(nil) + if !hmac.Equal(expectedMAC, msgMAC) { + /// TODO: fix me + // w.WriteHeader(http.StatusBadRequest) + logger.Error("HMAC is invalid", "expectedMAC", expectedMAC, "msgMAC", msgMAC) + // return + } + + w.WriteHeader(http.StatusOK) +} + +func (s *NowPayments) CreatePayment(party *store.TwitterParty) error { + url := fmt.Sprintf("%v/v1/invoice", s.apiURL) + // jsonStr := fmt.Sprintf(`{"price_amount":%v,"price_currency":"usd","ipn_callback_url":"%v","order_id":"%v"}`, + // party.TotalPrice, s.webhook, party.DiscountCode) + + jsonStr := fmt.Sprintf(`{"price_amount":%v,"price_currency":"usd","order_id":"%v"}`, + party.TotalPrice, party.DiscountCode) + + req, err := http.NewRequest("POST", url, bytes.NewBuffer([]byte(jsonStr))) + if err != nil { + return err + } + + req.Header.Set("Content-Type", "application/json") + req.Header.Set("x-api-key", s.apiToken) + + logger.Info("calling NowPayments:CreatePayment", "twitter", party.TwitterName, "json", jsonStr) + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + + data, err := io.ReadAll(resp.Body) + if err != nil { + return err + } + + logger.Debug("CreatePayment Response", "res", string(data)) + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("failed to call NowPayments. Status code: %v, status: %v", resp.StatusCode, resp.Status) + } + + var resultJSON map[string]interface{} + err = json.Unmarshal(data, &resultJSON) + if err != nil { + return err + } + + party.NowPaymentsInvoiceID = resultJSON["id"].(string) + + return nil +} + +func (s *NowPayments) UpdatePayment(party *store.TwitterParty) error { + token, err := s.getJWTToken() + if err != nil { + return err + } + url := fmt.Sprintf("%v/v1/payment/?invoiceId=%v", + s.apiURL, party.NowPaymentsInvoiceID) + fmt.Println(url) + req, err := http.NewRequest("GET", url, nil) + if err != nil { + return err + } + + req.Header.Set("x-api-key", s.apiToken) + req.Header.Set("Authorization", "Bearer "+token) + + logger.Info("calling NowPayments:ListOfPayments", "Twitter", party.TwitterName, "NowPaymentsInvoiceID", party.NowPaymentsInvoiceID) + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + + data, err := io.ReadAll(resp.Body) + if err != nil { + return err + } + + logger.Debug("ListOfPayments Response", "res", string(data)) + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("failed to call NowPayments:Payment. Status code: %v", resp.StatusCode) + } + + var resultJSON map[string]interface{} + err = json.Unmarshal(data, &resultJSON) + if err != nil { + return err + } + + results := resultJSON["data"].([]interface{}) + for _, payment := range results { + paymentStatus := payment.(map[string]interface{})["payment_status"] + + if paymentStatus == "finished" { + party.NowPaymentsFinished = true + } + } + + return nil +} + +func (s *NowPayments) getJWTToken() (string, error) { + url := fmt.Sprintf("%v/v1/auth", s.apiURL) + jsonStr := fmt.Sprintf(`{"email":"%v","password":"%v"}`, s.username, s.password) + req, err := http.NewRequest("POST", url, bytes.NewBuffer([]byte(jsonStr))) + if err != nil { + return "", err + } + + req.Header.Set("Content-Type", "application/json") + + logger.Info("calling NowPayments:auth") + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + return "", err + } + defer resp.Body.Close() + + data, err := io.ReadAll(resp.Body) + if err != nil { + return "", err + } + + if resp.StatusCode != http.StatusOK { + return "", fmt.Errorf("failed to call Auth. Status code: %v, status: %v", resp.StatusCode, resp.Status) + } + + var resultJSON map[string]interface{} + err = json.Unmarshal(data, &resultJSON) + if err != nil { + return "", err + } + + return resultJSON["token"].(string), nil +} diff --git a/nowpayments/nowpayments_test.go b/nowpayments/nowpayments_test.go new file mode 100644 index 00000000..865343cc --- /dev/null +++ b/nowpayments/nowpayments_test.go @@ -0,0 +1,32 @@ +package nowpayments + +import ( + "net/http" + "net/http/httptest" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestNetworkInfo(t *testing.T) { + cfg := Config{ + IPNSecret: "wQRl2P7/nvgjlqoRhcARpJbFQR6/hZ92", + } + + nowPayments, err := NewNowPayments(&cfg) + require.NoError(t, err) + + w := httptest.NewRecorder() + data := `{"actually_paid":0,"actually_paid_at_fiat":0,"fee":{"currency":"usdtbsc","depositFee":0,"serviceFee":0,"withdrawalFee":0},"invoice_id":4978049764,"order_description":null,"order_id":"181563","outcome_amount":46.5258178,"outcome_currency":"usdtbsc","parent_payment_id":null,"pay_address":"35AW2C7VeU6z6dxG1rrWEDR3qHBarkHGYw","pay_amount":0.00096324,"pay_currency":"btc","payin_extra_id":null,"payment_extra_ids":null,"payment_id":5613681154,"payment_status":"finished","price_amount":50,"price_currency":"usd","purchase_id":"4621639450","updated_at":1708011564521}` + jsonRaeder := strings.NewReader(data) + request, err := http.NewRequest("POST", "something-url", jsonRaeder) + if err != nil { + t.Fatal(err) + } + request.Header.Set("x-nowpayments-sig", "4f0d652781345b093c82ad1bf2812ab4cfcde9cf670d5d11147e908d4ea99d9f0ac8425346c4f917be97c441546f8b102be86dd4eb57127d8642ae162851627d") + nowPayments.webhookFunc(w, request) + + assert.Equal(t, w.Code, 200) +} diff --git a/store/interface.go b/store/interface.go index f3433244..f0032a25 100644 --- a/store/interface.go +++ b/store/interface.go @@ -7,16 +7,19 @@ type Claimer struct { } type TwitterParty struct { - TwitterID string `json:"twitter_id"` - TwitterName string `json:"twitter_name"` - RetweetID string `json:"retweet_id"` - ValAddr string `json:"val_addr"` - ValPubKey string `json:"val_pub"` - DiscordID string `json:"discord_id"` - DiscountCode string `json:"discount_code"` - TotalPrice int `json:"total_price"` - AmountInPAC int `json:"amount_in_pac"` - CreatedAt int64 `json:"created_at"` + TwitterID string `json:"twitter_id"` + TwitterName string `json:"twitter_name"` + RetweetID string `json:"retweet_id"` + ValAddr string `json:"val_addr"` + ValPubKey string `json:"val_pub"` + DiscordID string `json:"discord_id"` + DiscountCode string `json:"discount_code"` + TotalPrice int `json:"total_price"` + AmountInPAC int64 `json:"amount_in_pac"` + CreatedAt int64 `json:"created_at"` + NowPaymentsInvoiceID string `json:"nowpayments_id"` + NowPaymentsFinished bool `json:"nowpayments_finished"` + TransactionID string `json:"tx_id"` } type WhitelistInfo struct { @@ -34,7 +37,7 @@ type IStore interface { AddClaimTransaction(testNetValAddr string, txID string) error ClaimStatus() (int64, int64, int64, int64) - AddTwitterParty(party *TwitterParty) error + SaveTwitterParty(party *TwitterParty) error FindTwitterParty(twitterName string) *TwitterParty WhitelistTwitterAccount(twitterID, twitterName, authorizedDiscordID string) error diff --git a/store/mock.go b/store/mock.go index befcf7a9..e04ce4a9 100644 --- a/store/mock.go +++ b/store/mock.go @@ -52,20 +52,6 @@ func (mr *MockIStoreMockRecorder) AddClaimTransaction(testNetValAddr, txID any) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddClaimTransaction", reflect.TypeOf((*MockIStore)(nil).AddClaimTransaction), testNetValAddr, txID) } -// AddTwitterParty mocks base method. -func (m *MockIStore) AddTwitterParty(party *TwitterParty) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "AddTwitterParty", party) - ret0, _ := ret[0].(error) - return ret0 -} - -// AddTwitterParty indicates an expected call of AddTwitterParty. -func (mr *MockIStoreMockRecorder) AddTwitterParty(party any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddTwitterParty", reflect.TypeOf((*MockIStore)(nil).AddTwitterParty), party) -} - // ClaimStatus mocks base method. func (m *MockIStore) ClaimStatus() (int64, int64, int64, int64) { m.ctrl.T.Helper() @@ -125,6 +111,20 @@ func (mr *MockIStoreMockRecorder) IsWhitelisted(twitterID any) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsWhitelisted", reflect.TypeOf((*MockIStore)(nil).IsWhitelisted), twitterID) } +// SaveTwitterParty mocks base method. +func (m *MockIStore) SaveTwitterParty(party *TwitterParty) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SaveTwitterParty", party) + ret0, _ := ret[0].(error) + return ret0 +} + +// SaveTwitterParty indicates an expected call of SaveTwitterParty. +func (mr *MockIStoreMockRecorder) SaveTwitterParty(party any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SaveTwitterParty", reflect.TypeOf((*MockIStore)(nil).SaveTwitterParty), party) +} + // WhitelistTwitterAccount mocks base method. func (m *MockIStore) WhitelistTwitterAccount(twitterID, twitterName, authorizedDiscordID string) error { m.ctrl.T.Helper() diff --git a/store/store.go b/store/store.go index 6e9f954f..ed0e0ef8 100644 --- a/store/store.go +++ b/store/store.go @@ -143,13 +143,7 @@ func (s *Store) saveTwitterWhitelist() error { return saveMap(s.twitterWhitelistPath, s.twitterWhitelisted) } -func (s *Store) AddTwitterParty(party *TwitterParty) error { - found, exists := s.twitterParties[party.TwitterID] - if exists { - return fmt.Errorf("the Twitter `%v` already registered for the campagna. Discount code is %v", - found.TwitterName, found.DiscountCode) - } - +func (s *Store) SaveTwitterParty(party *TwitterParty) error { s.twitterParties[party.TwitterID] = party return s.saveTwitterParties() diff --git a/twitter_api/mock.go b/twitter_api/mock.go index 7d1a8a03..12a4b4eb 100644 --- a/twitter_api/mock.go +++ b/twitter_api/mock.go @@ -40,18 +40,18 @@ func (m *MockIClient) EXPECT() *MockIClientMockRecorder { } // RetweetSearch mocks base method. -func (m *MockIClient) RetweetSearch(ctx context.Context, discordName, twitterName string) (*TweetInfo, error) { +func (m *MockIClient) RetweetSearch(ctx context.Context, discordID, twitterName string) (*TweetInfo, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "RetweetSearch", ctx, discordName, twitterName) + ret := m.ctrl.Call(m, "RetweetSearch", ctx, discordID, twitterName) ret0, _ := ret[0].(*TweetInfo) ret1, _ := ret[1].(error) return ret0, ret1 } // RetweetSearch indicates an expected call of RetweetSearch. -func (mr *MockIClientMockRecorder) RetweetSearch(ctx, discordName, twitterName any) *gomock.Call { +func (mr *MockIClientMockRecorder) RetweetSearch(ctx, discordID, twitterName any) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RetweetSearch", reflect.TypeOf((*MockIClient)(nil).RetweetSearch), ctx, discordName, twitterName) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RetweetSearch", reflect.TypeOf((*MockIClient)(nil).RetweetSearch), ctx, discordID, twitterName) } // UserInfo mocks base method. diff --git a/twitter_api/twitter.go b/twitter_api/twitter.go index c3a1f523..0610b4eb 100644 --- a/twitter_api/twitter.go +++ b/twitter_api/twitter.go @@ -94,7 +94,7 @@ func (c *Client) RetweetSearch(ctx context.Context, discordID string, twitterNam UserFields: []twitter.UserField{twitter.UserFieldName}, TweetFields: []twitter.TweetField{twitter.TweetFieldCreatedAt}, } - query := fmt.Sprintf("%v (#Pactus or #PactusBoosterProgram) from:%v is:quote", discordID, twitterName) + query := fmt.Sprintf("%v (#Pactus OR #PactusBoosterProgram) from:%v is:quote", discordID, twitterName) logger.Debug("search query", "query", query) res, err := c.client.TweetRecentSearch(ctx, query, opts)