diff --git a/cmd/app.go b/cmd/app.go index 3ed0d5f7..57f6b5d9 100644 --- a/cmd/app.go +++ b/cmd/app.go @@ -35,8 +35,15 @@ func main() { return } + // load list of validators already received faucet + rs, err := discord.LoadReferralData(cfg) + if err != nil { + log.Println(err) + return + } + ///start discord bot - bot, err := discord.Start(cfg, w, ss) + bot, err := discord.Start(cfg, w, ss, rs) if err != nil { log.Printf("could not start discord bot: %v\n", err) return diff --git a/config/config.go b/config/config.go index 2949c99f..3c7ef450 100644 --- a/config/config.go +++ b/config/config.go @@ -8,13 +8,15 @@ import ( ) type Config struct { - DiscordToken string `json:"discord_token"` - WalletPath string `json:"wallet_path"` - WalletPassword string `json:"wallet_password"` - Servers []string `json:"servers"` - FaucetAddress string `json:"faucet_address"` - FaucetAmount float64 `json:"faucet_amount"` - ValidatorDataPath string `json:"validator_data_path"` + DiscordToken string `json:"discord_token"` + WalletPath string `json:"wallet_path"` + WalletPassword string `json:"wallet_password"` + Servers []string `json:"servers"` + FaucetAddress string `json:"faucet_address"` + FaucetAmount float64 `json:"faucet_amount"` + ReferralerStakeAmount float64 `json:"referraler_stake_amount"` // who get faucet + ValidatorDataPath string `json:"validator_data_path"` + ReferralDataPath string `json:"referral_data_path"` } func Load(path string) (*Config, error) { diff --git a/discord/discord.go b/discord/discord.go index 98609540..4d9b4d5d 100644 --- a/discord/discord.go +++ b/discord/discord.go @@ -11,6 +11,7 @@ import ( "github.com/kehiy/RoboPac/config" "github.com/kehiy/RoboPac/wallet" "github.com/libp2p/go-libp2p/core/peer" + gonanoid "github.com/matoous/go-nanoid/v2" "github.com/pactus-project/pactus/crypto" "github.com/pactus-project/pactus/util" pactus "github.com/pactus-project/pactus/www/grpc/gen/go" @@ -23,13 +24,14 @@ type Bot struct { faucetWallet *wallet.Wallet cfg *config.Config store *SafeStore + referralStore *ReferralStore cm *client.Mgr } // guildID: "795592769300987944" -func Start(cfg *config.Config, w *wallet.Wallet, ss *SafeStore) (*Bot, error) { +func Start(cfg *config.Config, w *wallet.Wallet, ss *SafeStore, rs *ReferralStore) (*Bot, error) { cm := client.NewClientMgr() for _, s := range cfg.Servers { @@ -171,6 +173,97 @@ func (b *Bot) messageHandler(s *discordgo.Session, m *discordgo.MessageCreate) { return } + if strings.ToLower(m.Content) == "my-referral" { + referrals := b.referralStore.GetAllReferrals() + for _, r := range referrals { + if r.DiscordID == m.Author.ID { + msg := fmt.Sprintf("Your referral data:\nPoints: %v\nCode: ```%v```\n", r.Points, r.ReferralCode) + _, _ = s.ChannelMessageSendReply(m.ChannelID, msg, m.Reference()) + return + } + } + + referralCode, err := gonanoid.New(10) + if err != nil { + msg := "can't generate referral code, please try again later." + _, _ = s.ChannelMessageSendReply(m.ChannelID, msg, m.Reference()) + return + } + + err = b.referralStore.NewReferral(m.Author.ID, m.Author.Username, referralCode) + if err != nil { + msg := "can't generate referral code, please try again later." + _, _ = s.ChannelMessageSendReply(m.ChannelID, msg, m.Reference()) + return + } + + msg := fmt.Sprintf("Your referral data:\nPoints: %v\nCode: ```%v```\n", 0, referralCode) + _, _ = s.ChannelMessageSendReply(m.ChannelID, msg, m.Reference()) + return + } + + if strings.Contains(strings.ToLower(m.Content), "faucet-referral") { + trimmedPrefix := strings.TrimPrefix(strings.ToLower(m.Content), "faucet-referral") + + Params := strings.Split(trimmedPrefix, " ") + if len(Params) != 2 { + msg := p.Sprintf("*Invalid* parameters!") + _, _ = s.ChannelMessageSendReply(m.ChannelID, msg, m.Reference()) + return + } + + address := strings.Trim(Params[0], " ") + referralCode := strings.Trim(Params[1], " ") + + peerID, pubKey, isValid, msg := b.validateInfo(address, m.Author.ID) + + msg = fmt.Sprintf("%v\ndiscord: %v\naddress: %v", + msg, m.Author.Username, address) + + if !isValid { + _, _ = s.ChannelMessageSendReply(m.ChannelID, msg, m.Reference()) + return + } + + // validate referral. + _, found := b.referralStore.GetData(referralCode) + if !found { + msg := p.Sprintf("*Invalid* referral!") + _, _ = s.ChannelMessageSendReply(m.ChannelID, msg, m.Reference()) + return + } + + if pubKey != "" { + // check available balance + balance := b.faucetWallet.GetBalance() + if balance.Available < b.cfg.FaucetAmount { + _, _ = s.ChannelMessageSendReply(m.ChannelID, "Insufficient faucet balance. Try again later.", m.Reference()) + return + } + + amount := b.cfg.ReferralerStakeAmount + ok := b.referralStore.AddPoint(referralCode) + if !ok { + _, _ = s.ChannelMessageSendReply(m.ChannelID, "Can't update referral data. please try again later.", m.Reference()) + return + } + + // send faucet + txHashFaucet := b.faucetWallet.BondTransaction(pubKey, address, amount) + + if txHashFaucet != "" { + err := b.store.SetData(peerID, address, m.Author.Username, m.Author.ID, amount) + if err != nil { + log.Printf("error saving faucet information: %v\n", err) + } + + msg := p.Sprintf("%v %.4f test PACs is staked to %v successfully!", + m.Author.Username, amount, address) + _, _ = s.ChannelMessageSendReply(m.ChannelID, msg, m.Reference()) + } + } + } + if strings.Contains(strings.ToLower(m.Content), "faucet") { trimmedPrefix := strings.TrimPrefix(strings.ToLower(m.Content), "faucet") // faucet message must contain address/public-key @@ -240,8 +333,11 @@ func help(s *discordgo.Session, m *discordgo.MessageCreate) { "To see the faucet account balance, simply type: `balance`\n" + "To see the faucet address, simply type: `address`\n" + "To get network information, simply type: `network`\n" + + "To get network health status, simply type: `health`\n" + "To get peer information, simply type: `peer-info [validator address]`\n" + - "To request faucet for test network: simply post `faucet [validator address]`.", + "To get your referral information, simply type: `my-referral`\n" + + "To request faucet for test network *with referral code*: simply type `faucet-referral [validator address] [referral code]`\n" + + "To request faucet for test network: simply type `faucet [validator address]`.", Fields: []*discordgo.MessageEmbedField{ { Name: "Example of requesting `faucet` ", diff --git a/discord/referral.go b/discord/referral.go new file mode 100644 index 00000000..2bfd36be --- /dev/null +++ b/discord/referral.go @@ -0,0 +1,125 @@ +package discord + +import ( + "fmt" + "log" + "os" + "sync" + + "github.com/kehiy/RoboPac/config" +) + +type Referral struct { + ReferralCode string `json:"referral_code"` + Points int `json:"points"` + DiscordName string `json:"discord_name"` + DiscordID string `json:"discord_id"` +} + +// SafeStore is a thread-safe cache. +type ReferralStore struct { + syncMap *sync.Map + cfg *config.Config +} + +func LoadReferralData(cfg *config.Config) (*ReferralStore, error) { + file, err := os.ReadFile(cfg.ReferralDataPath) + if err != nil { + log.Printf("error loading validator data: %v", err) + return nil, fmt.Errorf("error loading data file: %w", err) + } + if len(file) == 0 { + rs := &ReferralStore{ + syncMap: &sync.Map{}, + cfg: cfg, + } + return rs, nil + } + + data, err := unmarshalJSON(file) + if err != nil { + log.Printf("error unmarshalling validator data: %v", err) + return nil, fmt.Errorf("error unmarshalling validator data: %w", err) + } + rs := &ReferralStore{ + syncMap: data, + cfg: cfg, + } + return rs, nil +} + +// SetData Set a given value to the data storage. +func (rs *ReferralStore) NewReferral(discordId, discordName, referralCode string) error { + rs.syncMap.Store(referralCode, &Referral{ + Points: 0, + DiscordName: discordName, + ReferralCode: referralCode, + DiscordID: discordId, + }) + // save record + data, err := marshalJSON(rs.syncMap) + if err != nil { + log.Printf("error marshalling validator data file: %v", err) + return fmt.Errorf("error marshalling validator data file: %w", err) + } + if err := os.WriteFile(rs.cfg.ReferralDataPath, data, 0o600); err != nil { + log.Printf("failed to write to %s: %v", rs.cfg.ReferralDataPath, err) + return fmt.Errorf("failed to write to %s: %w", rs.cfg.ReferralDataPath, err) + } + return nil +} + +// GetData retrieves the given key from the storage. +func (rs *ReferralStore) GetData(code string) (*Referral, bool) { + entry, found := rs.syncMap.Load(code) + if !found { + return nil, false + } + referral := entry.(*Referral) + return referral, true +} + +// GetAllReferrals retrieves all referrals in store. +func (rs *ReferralStore) GetAllReferrals() []*Referral { + result := []*Referral{} + + rs.syncMap.Range(func(key, value any) bool { + referral, ok := value.(*Referral) + if !ok { + return true + } + result = append(result, referral) + return true + }) + + return result +} + +// AddPoint add one point for a referral. +func (rs *ReferralStore) AddPoint(code string) bool { + entry, found := rs.syncMap.Load(code) + if !found { + return false + } + + if found { + referral := entry.(*Referral) + referral.Points++ + rs.syncMap.Store(referral.ReferralCode, referral) + return true + } + + // save record + data, err := marshalJSON(rs.syncMap) + if err != nil { + log.Printf("error marshalling validator data file: %v", err) + return false + } + + if err := os.WriteFile(rs.cfg.ReferralDataPath, data, 0o600); err != nil { + log.Printf("failed to write to %s: %v", rs.cfg.ReferralDataPath, err) + return false + } + + return false +} diff --git a/go.mod b/go.mod index 16d4e9df..8c6a4b82 100644 --- a/go.mod +++ b/go.mod @@ -8,6 +8,7 @@ require ( github.com/bwmarrin/discordgo v0.27.1 github.com/k0kubun/pp v3.0.1+incompatible github.com/libp2p/go-libp2p v0.31.0 + github.com/matoous/go-nanoid/v2 v2.0.0 github.com/pactus-project/pactus v0.17.0 github.com/yudai/pp v2.0.1+incompatible golang.org/x/text v0.13.0 diff --git a/go.sum b/go.sum index 6cc70c0b..a9063180 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,6 @@ github.com/bwmarrin/discordgo v0.27.1 h1:ib9AIc/dom1E/fSIulrBwnez0CToJE113ZGt4HoliGY= github.com/bwmarrin/discordgo v0.27.1/go.mod h1:NJZpH+1AfhIcyQsPeuBKsUtYrRnjkyu0kIVMCHkZtRY= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/decred/dcrd/crypto/blake256 v1.0.1 h1:7PltbUIQB7u/FfZ39+DGa/ShuMyJ5ilcvdfma9wOH6Y= @@ -37,6 +38,9 @@ github.com/libp2p/go-buffer-pool v0.1.0 h1:oK4mSFcQz7cTQIfqbe4MIj9gLW+mnanjyFtc6 github.com/libp2p/go-buffer-pool v0.1.0/go.mod h1:N+vh8gMqimBzdKkSMVuydVDq+UV5QTWy5HSiZacSbPg= github.com/libp2p/go-libp2p v0.31.0 h1:LFShhP8F6xthWiBBq3euxbKjZsoRajVEyBS9snfHxYg= github.com/libp2p/go-libp2p v0.31.0/go.mod h1:W/FEK1c/t04PbRH3fA9i5oucu5YcgrG0JVoBWT1B7Eg= +github.com/matoous/go-nanoid v1.5.0/go.mod h1:zyD2a71IubI24efhpvkJz+ZwfwagzgSO6UNiFsZKN7U= +github.com/matoous/go-nanoid/v2 v2.0.0 h1:d19kur2QuLeHmJBkvYkFdhFBzLoo1XVm2GgTpL+9Tj0= +github.com/matoous/go-nanoid/v2 v2.0.0/go.mod h1:FtS4aGPVfEkxKxhdWPAspZpZSh1cOjtM7Ej/So3hR0g= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= @@ -66,6 +70,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI= github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/tyler-smith/go-bip39 v1.1.0 h1:5eUemwrMargf3BSLRRCalXT93Ns6pQJIjYQN2nyfOP8= @@ -113,6 +119,8 @@ google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp0 google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= lukechampine.com/blake3 v1.2.1 h1:YuqqRuaqsGV71BV/nm9xlI0MKUv4QC54jQnBChWbGnI= diff --git a/wallet/wallet.go b/wallet/wallet.go index 9eb86d93..c860d21d 100644 --- a/wallet/wallet.go +++ b/wallet/wallet.go @@ -43,7 +43,7 @@ func Open(cfg *config.Config) *Wallet { func (w *Wallet) BondTransaction(pubKey, toAddress string, amount float64) string { opts := []pwallet.TxOption{ pwallet.OptionFee(util.CoinToChange(0)), - pwallet.OptionMemo("faucet from PactusBot"), + pwallet.OptionMemo("Faucet from PactusBot"), } tx, err := w.wallet.MakeBondTx(w.address, toAddress, pubKey, util.CoinToChange(amount), opts...)