From 3fb6a34162742460c81e6315e41c4ef3966962e5 Mon Sep 17 00:00:00 2001 From: piux2 <> Date: Tue, 25 Oct 2022 23:47:58 -0700 Subject: [PATCH 1/3] init release --- .gitignore | 19 ++ Makefile | 12 + README.md | 124 +++++++++ cmd/gnodiscord/discord.go | 230 +++++++++++++++++ cmd/gnodiscord/faucet.go | 284 +++++++++++++++++++++ cmd/gnodiscord/main.go | 54 ++++ examples/gno.land/r/faucet/admin.gno | 100 ++++++++ examples/gno.land/r/faucet/faucet.gno | 97 +++++++ examples/gno.land/r/faucet/faucet_test.gno | 103 ++++++++ faucet.sh | 61 +++++ gnodiscord/discord.go | 230 +++++++++++++++++ gnodiscord/faucet.go | 284 +++++++++++++++++++++ gnodiscord/main.go | 54 ++++ go.mod | 51 ++++ go.sum | 283 ++++++++++++++++++++ provision.sh | 12 + send.sh | 7 + startbot.sh | 12 + 18 files changed, 2017 insertions(+) create mode 100644 .gitignore create mode 100644 Makefile create mode 100644 cmd/gnodiscord/discord.go create mode 100644 cmd/gnodiscord/faucet.go create mode 100644 cmd/gnodiscord/main.go create mode 100644 examples/gno.land/r/faucet/admin.gno create mode 100644 examples/gno.land/r/faucet/faucet.gno create mode 100644 examples/gno.land/r/faucet/faucet_test.gno create mode 100755 faucet.sh create mode 100644 gnodiscord/discord.go create mode 100644 gnodiscord/faucet.go create mode 100644 gnodiscord/main.go create mode 100644 go.mod create mode 100644 go.sum create mode 100755 provision.sh create mode 100755 send.sh create mode 100755 startbot.sh diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ef1e272 --- /dev/null +++ b/.gitignore @@ -0,0 +1,19 @@ +misc/ +*.sw[pon] +cmd/foo.go +unsorted.* +.DS_Store +data/* +proto/* +testdir +pkgs/sdk/vm/_testdata +build/* +*.tx +*.log.* +*.log +*.gno.gen.go +*.gno.gen_test.go +.vscode +.idea +*.pb.go +pbbindings.go diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..61eb7c5 --- /dev/null +++ b/Makefile @@ -0,0 +1,12 @@ + + +.PHONY: build +all: build + +build: + @echo "build" + go build -o build/gnobot ./cmd/gnodiscord + +build_linux: + @echo "build" + GOOS=linux GOARCH=amd64 go build -o build/gnobot_linux ./cmd/gnodiscord diff --git a/README.md b/README.md index b3bc34d..c09d424 100644 --- a/README.md +++ b/README.md @@ -2,3 +2,127 @@ the community bot(s) interacting with r/faucet See https://github.com/gnolang/gno/issues/364. + +## GNO Discord Faucet + +Community members can request testing tokens from the discord channel. + +## Problem to solve + +A centralized faucet endpoint could easily be an abusive target. +In addition, it adds operational complexity and cost to operating an endpoint. + +### Details + +There are three issues we try to solve in the web-based faucet. + +1) It is centralized and can be an abuse target. + +2) First-time users need to understand the fee structure to register and interact with the board and require multiple requests to get in fees + +3) There need to be more tokens allocated to the faucet wallet to support many user registrations and board creation. + + + +## Solution + +A decentralized discord bot can be a faucet to any discord server and channels. + +Discord provides sophisticated verification to prevent abuse. We can leverage it to distribute test tokens in the community group. + +It is friendly to community members. The moderator can easily manage and prevent people from abusing the faucet. + +### Details + +A discord faucet bot can solve 1, 2, and partially 3 + +1) A discord bot can run on any computer and be configured to use any wallet to support the faucet. There is no direct attacking point as a service. Instead, the testing token request is funneled through the discord app, which the admin moderates. Multiple bots can stand by and respond to the same channel as back to each other. + +2) In the gno land test net, users must request 200gnot to register and cost 100gnot to interact with the board contract if not registered. + +How do we allow people quickly get started and prevent people keep accumulating testing tokens? + +We can set a limit for each, say 400gnot per account. We give max 400gnot for first-time users. So they can get started the right way and have enough tokens to try everything. For non-first-time users, we provide a regular amount of 1gnot to cover most of the gas fee usage. + +3) A faucet can drain out with regular usage. We can run a backup bot with another faucet wallet monitor on a different channel as a backup. + +## Other aspects + +4) We can also recycle the tokens from the user and broad contract back to the faucet manually or automatically. + +5) 200 gnot user registration fee is to prevent people from spamming the user board. We can add a limited number of users can register per day and lower the fee. It slows down the pace that the faucets drain out. + +## Features + +- Each account is limited to 350gnot max. +- Instead of incrementally issuing a small amount to users, we give the first-time user the max amount. It costs 200gnot to register and 100gnot to create a board in the current test net. No need to request multiple times to be able to register users and create boards. + +## Instruction + +#### 0) Install the gnokey + + git clone https://github.com/gnolang/gno + cd gno + make install_gnokey + +make sure you include $GOPATH/bin in $PATH + +#### 0.1) create admin and controller accounts + + gnokey add admin + + gnokey add controller1 + + gnokey add controller2 + + +#### 1) build gnobot + + make + +#### 2) Deploy faucet contract and assign controller the faucet + + please modify the the address in the script. + + ./provison.sh + +#### 3) start the bot + +check out + + ./startbot.sh + +The following flags are required when you run the gnobot. We do not recommend storing the discord token on your local machine, not even in the env file. + +--chain-id + +--token + +--channel + +--bot-name + +--guild + +--limit // default 350000000ugnot + +--send // default 5000000ugnot + +DISCORDBOT_TOKEN: + +Your discord application bot token. (NOT OAuth2 token ) + +DISCORD_CHANNELID: + +The id of a channel that you add the bot to + +DISCORD_GUILD: + +The id of the discord server that you add the bot to + +DISCORD_BOTNAME: + +The name of your bot + + + ./build/gnodiscord faucet test1 --chain-id test3 --token DISCORDBOT_TOKEN --channel DISCORD_CHANNELID -bot-name DISCORD_BOTNAME --guild $DISCORD_GUILD --remote test3.gno.land:36657 diff --git a/cmd/gnodiscord/discord.go b/cmd/gnodiscord/discord.go new file mode 100644 index 0000000..0bccb05 --- /dev/null +++ b/cmd/gnodiscord/discord.go @@ -0,0 +1,230 @@ +package main + +import ( + "fmt" + "strings" + "unicode" + + "github.com/bwmarrin/discordgo" + + "github.com/gnolang/gno/pkgs/amino" + + "github.com/gnolang/gno/pkgs/crypto" + + "github.com/gnolang/gno/pkgs/crypto/keys/client" + "github.com/gnolang/gno/pkgs/errors" + "github.com/gnolang/gno/pkgs/std" +) + +// Start a discord session +func (df *DiscordFaucet) Start() error { + err := df.discordFaucet() + if err != nil { + return err + } + + // Open a websocket connection to Discord and begin listening. + + return df.session.Open() +} + +// Close discord session smoothly +func (df *DiscordFaucet) Close() { + df.session.Close() +} + +// it takes discord server API token and rpc client. +// We use rpc client to validate addresses and check abuses. +// and we cap the balance holding to 400 GNOT before issue new tokens from faucet + +func (df *DiscordFaucet) discordFaucet() error { + // Create a new Discord session using the provided bot token. + dg, err := discordgo.New("Bot " + df.opts.BotToken) + if err != nil { + fmt.Println("failed to create discord bot session.", err) + return err + } + + df.session = dg + + // we only care about receiving message events. + dg.Identify.Intents = discordgo.IntentsGuildMessages + + // Register the messageCreate func as a callback for MessageCreate events. + dg.AddHandler(func(s *discordgo.Session, m *discordgo.MessageCreate) { + // Ignore all messages created by the bot itself + // This is a good practice. + if m.Author.ID == s.State.User.ID { + return + } + // ignore message from other channels + if m.ChannelID != df.opts.Channel { + return + } + + // ignore message from other discord guild/server + + if m.GuildID != df.opts.Guild { + return + } + + // ignore message does not mention anyone + if len(m.Mentions) == 0 { + return + } + + user := m.Mentions[0] + + // ingore messages that doe not mentiont this bot + + if user.Bot != true || user.Username != df.opts.BotName { + return + } + + var res string + // retrive toAddress from received disocrd message + toAddr, bal, err := df.process(m) + if err != nil { + + res = fmt.Sprintf("%s", err) + dg.ChannelMessageSend(m.ChannelID, "<@"+m.Author.ID+"> "+res) + + fmt.Printf("channel %s:<@%s> %s", m.ChannelID, m.Author.Username, res) + + return + + } + var send std.Coins + // if the account has no balance we give it upto the full. + if bal.IsZero() { + send = std.NewCoins(perAccountLimit) + } else { + send, _ = std.ParseCoins(df.opts.Send) + } + + err = df.sendAmountTo(toAddr, send) + if err != nil { + + dg.ChannelMessageSend(m.ChannelID, "<@"+m.Author.ID+"> "+"faucet failed") + + fmt.Printf("channel %s:<@%s>%v", m.ChannelID, m.Author.Username, err) + + return + + } + + var amount string + for _, v := range send { + amount += v.String() + " " + } + + res = fmt.Sprintf("Cha-Ching! %s +%s", toAddr, amount) + + dg.ChannelMessageSend(m.ChannelID, "<@"+m.Author.ID+"> "+res) + }) + + return nil +} + +// This function will be called every time a new +// message is created on any channel that the authenticated bot has access to. +// It returns true and valid response if the message from discord is valid. +// Other we returns false with an empty string,which we should ingore. +func (df *DiscordFaucet) process(m *discordgo.MessageCreate) (string, std.Coin, error) { + validAddr, err := retrieveAddr(m.Content) + + zero := std.Coin{} + + if err != nil { + return "", zero, errors.New("No valid addresse: %s", err) + } + + bal, err := df.checkBalance(validAddr) + if err != nil { + return "", zero, errors.New("It seems our faucet is not working properly %s", err) + } + + if bal.IsGTE(perAccountLimit) { + return "", zero, errors.New("Your account %s still has %d%s, no need to accumulate more testing tokens", validAddr, bal.Amount, bal.Denom) + } + + return validAddr, bal, nil +} + +// retrive the first string with valid addresse length +func retrieveAddr(message string) (string, error) { + var addr string + words := strings.Fields(message) + + for _, w := range words { + + s := strings.TrimFunc(w, func(r rune) bool { + return !unicode.IsLetter(r) && !unicode.IsNumber(r) + }) + + if len(s) == addressStringLength { + addr = s + break + } + } + + ok, err := isValid(addr) + + if !ok { + return "", err + } + + return addr, nil +} + +// A valid address format + +func isValid(addr string) (bool, error) { + // validate prefix + + if strings.HasPrefix(addr, crypto.Bech32AddrPrefix) == false { + return false, errors.New("The address does not have correct prefix %s", crypto.Bech32AddrPrefix) + } + + if addressStringLength != len(addr) { + return false, errors.New("The address does not have correct length %d", addressStringLength) + } + + _, err := crypto.AddressFromBech32(addr) + if err != nil { + return false, errors.Wrap(err, "parsing address") + } + return true, nil +} + +// return true if the account balance is within limit +// return amount of token from +func (df *DiscordFaucet) checkBalance(addr string) (std.Coin, error) { + qopts := client.QueryOptions{ + Path: fmt.Sprintf("auth/accounts/%s", addr), + } + qopts.Remote = df.opts.Remote + qres, err := client.QueryHandler(qopts) + if err != nil { + return std.Coin{}, errors.Wrap(err, "query account") + } + var acc struct{ BaseAccount std.BaseAccount } + err = amino.UnmarshalJSON(qres.Response.Data, &acc) + if err != nil { + return std.Coin{}, err + } + + balances := acc.BaseAccount.GetCoins() + + bal := std.Coin{Denom: "ugnot", Amount: 0} + + if len(balances) > 0 { + for i, v := range balances { + if v.Denom == "ugnot" { + bal = balances[i] + } + } + } + + return bal, nil +} diff --git a/cmd/gnodiscord/faucet.go b/cmd/gnodiscord/faucet.go new file mode 100644 index 0000000..6af25c1 --- /dev/null +++ b/cmd/gnodiscord/faucet.go @@ -0,0 +1,284 @@ +package main + +import ( + "fmt" + "os" + "os/signal" + "strconv" + "syscall" + + "github.com/bwmarrin/discordgo" + "github.com/gnolang/gno/pkgs/amino" + "github.com/gnolang/gno/pkgs/command" + + "github.com/gnolang/gno/pkgs/crypto/keys" + "github.com/gnolang/gno/pkgs/crypto/keys/client" + "github.com/gnolang/gno/pkgs/errors" + + "github.com/gnolang/gno/pkgs/sdk/vm" + "github.com/gnolang/gno/pkgs/std" +) + +var perAccountLimit = std.NewCoin("ugnot", 350000000) // 350 gnot + +// A valid gno address string length is 40 +const addressStringLength = 40 + +type faucetOptions struct { + client.BaseOptions // home, ... + ChainID string `flag:"chain-id" help:"chain id must provide"` + + GasWanted int64 `flag:"gas-wanted" help:"gas requested for tx"` + GasFee string `flag:"gas-fee" help:"gas payment fee"` + Send string `flag:"send" help:"send coins per request"` + Memo string `flag:"memo" help:"any descriptive text"` + PkgPath string `flag:"pkgpath" help:"pacakge holding the faucet fund"` + Limit string `flag:"limit" help:"per transfer and per user account limit"` + BotToken string `flag:"token" help:"discord bot token - must provide"` + BotName string `flag:"bot-name" help:"discord bot name - must provide"` + Channel string `flag:"channel" help:"discord bot channel id - must provide"` + Guild string `flag:"guild" help:"discord bot guild/server id - must provide"` +} + +var DefaultFaucetOptions = faucetOptions{ + BaseOptions: client.DefaultBaseOptions, + ChainID: "", // must override + + GasWanted: 800000, + GasFee: "1000000ugnot", + Send: "5000000ugnot", + Memo: "disccord faucet", + PkgPath: "gno.land/r/faucet", + Limit: "350000000ugnot", + + BotToken: "", // must override + BotName: "", // must override + Channel: "", // must override + Guild: "", // must override + +} + +// DiscordFaucet access local keybase, remote chain endpoint discord session +type DiscordFaucet struct { + // local wallet key store + keybase keys.Keybase + + keyinfo keys.Info + + keyname string + + keypass string + + sequence uint64 + + // discord session + session *discordgo.Session + + // other faucetOptions + opts faucetOptions +} + +// NewDiscordFaucet construct an instance +func NewDiscordFaucet(name, pass string, opts faucetOptions) (*DiscordFaucet, error) { + kb, err := keys.NewKeyBaseFromDir(opts.Home) + if err != nil { + return nil, err + } + info, err := kb.GetByName(name) + if err != nil { + return nil, err + } + + // sign a dummy value to validate key pass + const dummy = "test" + _, _, err = kb.Sign(name, pass, []byte(dummy)) + if err != nil { + return nil, err + } + + return &DiscordFaucet{ + keybase: kb, + keyinfo: info, + keyname: name, + keypass: pass, + + opts: opts, + }, nil +} + +func faucetApp(cmd *command.Command, args []string, iopts interface{}) error { + opts := iopts.(faucetOptions) + + if len(args) != 1 { + cmd.ErrPrintfln("Usage: facuet ") + return errors.New("invalid args") + } + + if opts.ChainID == "" { + return errors.New("chain-id not specified") + } + if opts.BotToken == "" { + return errors.New("discord bot token not specified") + } + + if opts.Channel == "" { + return errors.New("discord bot channel id not specified") + } + + if opts.BotName == "" { + return errors.New("discord bot name not specified") + } + + if opts.Guild == "" { + return errors.New("discord bot guild/server id not specified") + } + + remote := opts.Remote + if remote == "" || remote == "y" { + return errors.New("missing remote url") + } + + var err error + perAccountLimit, err = std.ParseCoin(opts.Limit) + if err != nil { + panic(err) + } + + // XXX XXX + // Read supply account pubkey. + + name := args[0] + var pass string + if opts.Quiet { + pass, err = cmd.GetPassword("", false) + } else { + pass, err = cmd.GetPassword("Enter password.", false) + } + if err != nil { + return err + } + + // validate password + + // start a discord session + + df, err := NewDiscordFaucet(name, pass, opts) + if err != nil { + return err + } + // Start the bot + + err = df.Start() + if err != nil { + return err + } + + // Cleanly close down the Discord session. + defer df.Close() + + // Wait here until CTRL-C or other term signal is received. + fmt.Println("Bot is now running. Press CTRL-C to exit.") + sc := make(chan os.Signal, 1) + signal.Notify(sc, syscall.SIGINT, syscall.SIGTERM, os.Interrupt, os.Kill) + <-sc + + return nil +} + +// call faucet contract to send tokens to requester +func (df *DiscordFaucet) sendAmountTo(to string, send std.Coins) error { + function := "Transfer" + // Read faucet account pubkey. + + faucet := df.keyinfo.GetAddress() + + // parse gas wanted & fee. + gaswanted := df.opts.GasWanted + + gasfee, err := std.ParseCoin(df.opts.GasFee) + if err != nil { + panic(err) + } + sendAmount := strconv.FormatInt(send.AmountOf("ugnot"), 10) + // construct msg & tx and marshal. + msg := vm.MsgCall{ + Caller: faucet, + PkgPath: df.opts.PkgPath, + Func: function, + Args: []string{to, sendAmount}, + } + tx := std.Tx{ + Msgs: []std.Msg{msg}, + Fee: std.NewFee(gaswanted, gasfee), + Signatures: nil, + Memo: df.opts.Memo, + } + + return df.signAndBroadcast(tx) +} + +func (df *DiscordFaucet) signAndBroadcast(tx std.Tx) error { + // query account to get account number and sequece + + accountAddr := df.keyinfo.GetAddress().String() + + qopts := client.QueryOptions{ + Path: fmt.Sprintf("auth/accounts/%s", accountAddr), + } + qopts.Remote = df.opts.Remote + qres, err := client.QueryHandler(qopts) + if err != nil { + return errors.Wrap(err, "query account") + } + var qret struct{ BaseAccount std.BaseAccount } + err = amino.UnmarshalJSON(qres.Response.Data, &qret) + if err != nil { + return err + } + + // sign tx + accountNumber := qret.BaseAccount.AccountNumber + sequence := qret.BaseAccount.Sequence + sopts := client.SignOptions{ + Sequence: &sequence, + AccountNumber: &accountNumber, + ChainID: df.opts.ChainID, + NameOrBech32: df.keyname, + TxJson: amino.MustMarshalJSON(tx), + } + sopts.Home = df.opts.Home + sopts.Pass = df.keypass + + signedTx, err := client.SignHandler(sopts) + if err != nil { + return errors.Wrap(err, "sign tx") + } + + // broadcast tx bytes. + + // broadcast signed tx + bopts := client.BroadcastOptions{ + Tx: signedTx, + } + bopts.Remote = df.opts.Remote + bres, err := client.BroadcastHandler(bopts) + if err != nil { + return errors.Wrap(err, "broadcast tx") + } + if bres.CheckTx.IsErr() { + return errors.Wrap(bres.CheckTx.Error, "check transaction failed: log:%s", bres.CheckTx.Log) + } + if bres.DeliverTx.IsErr() { + return errors.Wrap(bres.DeliverTx.Error, "deliver transaction failed: log:%s", bres.DeliverTx.Log) + } + + fmt.Println("Message Delivered!") + fmt.Println("GAS WANTED:", bres.DeliverTx.GasWanted) + fmt.Println("GAS USED: ", bres.DeliverTx.GasUsed) + + if string(bres.DeliverTx.Data) != "(\"\" string)" { + return errors.New(string(bres.DeliverTx.Data)) + } + + return nil +} diff --git a/cmd/gnodiscord/main.go b/cmd/gnodiscord/main.go new file mode 100644 index 0000000..2d236db --- /dev/null +++ b/cmd/gnodiscord/main.go @@ -0,0 +1,54 @@ +package main + +import ( + "os" + + "github.com/gnolang/gno/pkgs/command" + "github.com/gnolang/gno/pkgs/errors" +) + +type ( + // AppItem is an app mapped to a subcommand + AppItem = command.AppItem + + // AppList holds a list of subcommands apps + AppList = command.AppList +) + +var mainApps AppList = []AppItem{ + {App: faucetApp, Name: "faucet", Desc: "discord faucet", Defaults: DefaultFaucetOptions}, +} + +func main() { + cmd := command.NewStdCommand() + exec := os.Args[0] + args := os.Args[1:] + err := runMain(cmd, exec, args) + if err != nil { + cmd.ErrPrintfln("%s", err.Error()) + cmd.ErrPrintfln("%#v", err) + return // exit + } +} + +func runMain(cmd *command.Command, exec string, args []string) error { + // show help message. + if len(args) == 0 || args[0] == "help" || args[0] == "--help" { + cmd.Println("available subcommands:") + for _, appItem := range mainApps { + cmd.Printf(" %s - %s\n", appItem.Name, appItem.Desc) + } + return nil + } + + // switch on first argument. + for _, appItem := range mainApps { + if appItem.Name == args[0] { + err := cmd.Run(appItem.App, args[1:], appItem.Defaults) + return err // done + } + } + + // unknown app command! + return errors.New("unknown command " + args[0]) +} diff --git a/examples/gno.land/r/faucet/admin.gno b/examples/gno.land/r/faucet/admin.gno new file mode 100644 index 0000000..4be92b7 --- /dev/null +++ b/examples/gno.land/r/faucet/admin.gno @@ -0,0 +1,100 @@ +package faucet + +import ( + "errors" + "std" +) + +func AdminSetInPause(inPause bool) string { + if err := assertIsAdmin(); err != nil { + return err.Error() + } + gInPause = inPause + return "" +} + +func AdminSetMessage(message string) string { + if err := assertIsAdmin(); err != nil { + return err.Error() + } + gMessage = message + return "" +} + +func AdminSetTransferLimit(amount int64, denom string) string { + if err := assertIsAdmin(); err != nil { + return err.Error() + } + limit = std.Coins{std.Coin{Denom: denom, Amount: amount}} + return "" +} + +func AdminSetAdminAddr(addr std.Address) string { + if err := assertIsAdmin(); err != nil { + return err.Error() + } + gAdminAddr = addr + return "" +} + +func AdminAddController(addr std.Address) string { + if err := assertIsAdmin(); err != nil { + return err.Error() + } + + k := -1 + for _, v := range gControllers { + + if v == addr { + + return addr.String() + " exists, no need to add." + } + + } + for i, v := range gControllers { + + if v == "" { + + k = i + break + } + } + if k < 0 { + return "can not add more controllers than allowed" + } + + gControllers[k] = addr + + return "" +} + +func AdminRemoveController(addr std.Address) string { + if err := assertIsAdmin(); err != nil { + return err.Error() + } + + removed := false + for i, v := range gControllers { + + if v == addr { + + gControllers[i] = "" + removed = true + } + + } + + if removed == false { + + return addr.String() + " is not on the controller list" + } + return "" +} + +func assertIsAdmin() error { + caller := std.GetOrigCaller() + if caller != gAdminAddr { + return errors.New("restricted for admin") + } + return nil +} diff --git a/examples/gno.land/r/faucet/faucet.gno b/examples/gno.land/r/faucet/faucet.gno new file mode 100644 index 0000000..0b3fd06 --- /dev/null +++ b/examples/gno.land/r/faucet/faucet.gno @@ -0,0 +1,97 @@ +package faucet + +import ( + "errors" + "std" + "strings" + + "gno.land/p/ufmt" +) + +var ( + // configurable by admin. + gAdminAddr std.Address = "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5" + gControllers [3]std.Address = [3]std.Address{} // limit it to 3 + + gInPause = false + gMessage = "# Community Faucet.\n\n" + + // internal vars, for stats. + gTotalTransferred std.Coins + gTotalTransfers = uint(0) + + // per account request limit 350 gnot + limit std.Coins = std.Coins{std.Coin{"ugnot", 350000000}} +) + +func Transfer(to std.Address, send int64) string { + if err := assertIsController(); err != nil { + return err.Error() + } + + if gInPause { + return errors.New("faucet in pause").Error() + } + + var sendCoins std.Coins + + if send <= limit.AmountOf("ugnot") { + + sendCoins = std.Coins{std.Coin{Denom: "ugnot", Amount: send}} + + } else { + + return errors.New("Per request limit " + limit.String() + " exceed").Error() + } + + gTotalTransferred = gTotalTransferred.Add(sendCoins) + gTotalTransfers++ + + banker := std.GetBanker(std.BankerTypeRealmSend) + pkgaddr := std.GetOrigPkgAddr() + banker.SendCoins(pkgaddr, to, sendCoins) + return "" +} +func GetPerTransferLimit() int64 { + + return limit.AmountOf("ugnot") + +} +func Render(path string) string { + banker := std.GetBanker(std.BankerTypeRealmSend) + balance := banker.GetCoins(std.GetOrigPkgAddr()) + + output := path + gMessage + if gInPause { + output += "Status: inactive.\n" + } else { + output += "Status: active.\n\n" + } + output += ufmt.Sprintf("Balance: %s.\n", balance.String()) + output += ufmt.Sprintf("Total transfer: %s (in %d times).\n\n", gTotalTransferred.String(), gTotalTransfers) + + output += "Package address: " + std.GetOrigPkgAddr().String() + "\n\n" + output += ufmt.Sprintf("Admin: %s\n\n ", gAdminAddr.String()) + output += ufmt.Sprintf("Controller:\n\n ") + + for _, v := range gControllers { + + output += ufmt.Sprintf("%s ", v.String()) + + } + output += ufmt.Sprintf("\n\n") + output += ufmt.Sprintf("Per request limit: %s\n\n", limit.String()) + + return output +} + +func assertIsController() error { + caller := std.GetOrigCaller() + + for _, v := range gControllers { + if caller == v { + return nil + } + } + return errors.New(caller.String() + " is not on the controller list") +} diff --git a/examples/gno.land/r/faucet/faucet_test.gno b/examples/gno.land/r/faucet/faucet_test.gno new file mode 100644 index 0000000..f061329 --- /dev/null +++ b/examples/gno.land/r/faucet/faucet_test.gno @@ -0,0 +1,103 @@ +package faucet + +import ( + "fmt" + "std" + "testing" + + "gno.land/p/testutils" + "gno.land/r/faucet" +) + +func TestPackage(t *testing.T) { + var ( + adminaddr = std.Address("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") + faucetaddr = std.TestDerivePkgAddr("gno.land/r/faucet") + controlleraddr1 = testutils.TestAddress("controller1") + controlleraddr2 = testutils.TestAddress("controller2") + controlleraddr3 = testutils.TestAddress("controller3") + controlleraddr4 = testutils.TestAddress("controller4") + + test1addr = testutils.TestAddress("test1") + ) + // deposit 1000gnot to faucet contract + + std.TestIssueCoins(faucetaddr, std.Coins{{"ugnot", 1000000000}}) + std.TestSetOrigPkgAddr(faucetaddr) + assertBalance(t, faucetaddr, 1000000000) + + // by default, balance is empty, and as a user I cannot call Transfer, or Admin commands. + + assertBalance(t, test1addr, 0) + assertErr(t, faucet.Transfer(test1addr, 1000000)) + assertErr(t, faucet.AdminAddController(controlleraddr1)) + std.TestSetOrigCaller(controlleraddr1) + assertErr(t, faucet.Transfer(test1addr, 1000000)) + + // as an admin, add the controller to contract and deposit more 2000gnot to contract + std.TestSetOrigCaller(adminaddr) + assertNoErr(t, faucet.AdminAddController(controlleraddr1)) + assertBalance(t, faucetaddr, 1000000000) + + // now, send some tokens as controller. + std.TestSetOrigCaller(controlleraddr1) + assertNoErr(t, faucet.Transfer(test1addr, 1000000)) + assertBalance(t, test1addr, 1000000) + assertNoErr(t, faucet.Transfer(test1addr, 1000000)) + assertBalance(t, test1addr, 2000000) + assertBalance(t, faucetaddr, 998000000) + + // remove controller + // as an admin, remove controller + std.TestSetOrigCaller(adminaddr) + assertNoErr(t, faucet.AdminRemoveController(controlleraddr1)) + std.TestSetOrigCaller(controlleraddr1) + assertErr(t, faucet.Transfer(test1addr, 1000000)) + + // add more than 3 controllers + + std.TestSetOrigCaller(adminaddr) + assertNoErr(t, faucet.AdminAddController(controlleraddr1)) + assertErr(t, faucet.AdminAddController(controlleraddr1)) + assertNoErr(t, faucet.AdminAddController(controlleraddr2)) + assertNoErr(t, faucet.AdminAddController(controlleraddr3)) + assertErr(t, faucet.AdminAddController(controlleraddr4)) + + // send more than per transfer limit + std.TestSetOrigCaller(adminaddr) + faucet.AdminSetTransferLimit(300000000, "ugnot") + std.TestSetOrigCaller(controlleraddr1) + assertErr(t, faucet.Transfer(test1addr, 301000000)) + + // block transefer from the address not on the controllers list. + std.TestSetOrigCaller(controlleraddr4) + assertErr(t, faucet.Transfer(test1addr, 1000000)) + +} + +func assertErr(t *testing.T, err string) { + t.Helper() + if err == "" { + t.Logf("info: got err: %v", err) + t.Errorf("expected an error, got nil.") + } +} + +func assertNoErr(t *testing.T, err string) { + t.Helper() + if err != "" { + t.Errorf("got err: %v.", err) + } +} + +func assertBalance(t *testing.T, addr std.Address, expectedBal int64) { + t.Helper() + + banker := std.GetBanker(std.BankerTypeReadonly) + coins := banker.GetCoins(addr) + got := coins.AmountOf("ugnot") + + if expectedBal != got { + t.Errorf("invalid balance: expected %d, got %d.", expectedBal, got) + } +} diff --git a/faucet.sh b/faucet.sh new file mode 100755 index 0000000..93855a0 --- /dev/null +++ b/faucet.sh @@ -0,0 +1,61 @@ +#!/bin/bash + +# a pacakge deployed to the realm, with package the prefixe gno.land/r/ is a smart contract code with capability to persist state on chain +# --deposit flag: a minumn fee of 100gnot is needed to deploy the smart contract in the package path gno.land/r/ + +default=' test1 --gas-fee 10000000ugnot --gas-wanted 1000000 --broadcast true --remote test3.gno.land:36657 --chainid test3' +package=' --pkgdir examples/gno.land/r/faucet --pkgpath gno.land/r/faucet ' + +#faucet admin contract holds the faucet fund. let's deposit 200K gnot first +deposit=' --deposit 200000000000ugnot' + +# call set faucet account +callAddController=' --pkgpath gno.land/r/faucet --func AdminAddController --args ' +addController="$default $callAddController $3" + + +# call transfer +callTransfer=' --pkgpath gno.land/r/faucet --func Transfer --args ' +transfer="$default $callTransfer $3 --args $4" + + +gnotx='gnokey maketx' + +if [ "$1" = "call" ] +then + if [ "$2" = "controller" ] + then + if [ "$3" = "" ] + then + echo 'need the third parameter: controller_address' + + else + echo $gnotx call $addController + $gnotx call $addController + fi + + elif [ "$2" = "transfer" ] + then + if [ "$3" = "" ] + then + echo 'need the third parameter: to_address' + + else + echo $gnotx call $transfer + $gnotx call $transfer + + fi + else + echo 'the second parameter must be controller or transfer' + fi + +elif [ "$1" = "deploy" ] +then + # deploy contracts + echo "$gnotx addpkg $defaut $package" + $gnotx addpkg $default $package $deposit + +else + echo 'the fist parameter must be either call or deploy' + echo 'usage: deploy | call transfer [toaddress] | call controller [address]' +fi diff --git a/gnodiscord/discord.go b/gnodiscord/discord.go new file mode 100644 index 0000000..0bccb05 --- /dev/null +++ b/gnodiscord/discord.go @@ -0,0 +1,230 @@ +package main + +import ( + "fmt" + "strings" + "unicode" + + "github.com/bwmarrin/discordgo" + + "github.com/gnolang/gno/pkgs/amino" + + "github.com/gnolang/gno/pkgs/crypto" + + "github.com/gnolang/gno/pkgs/crypto/keys/client" + "github.com/gnolang/gno/pkgs/errors" + "github.com/gnolang/gno/pkgs/std" +) + +// Start a discord session +func (df *DiscordFaucet) Start() error { + err := df.discordFaucet() + if err != nil { + return err + } + + // Open a websocket connection to Discord and begin listening. + + return df.session.Open() +} + +// Close discord session smoothly +func (df *DiscordFaucet) Close() { + df.session.Close() +} + +// it takes discord server API token and rpc client. +// We use rpc client to validate addresses and check abuses. +// and we cap the balance holding to 400 GNOT before issue new tokens from faucet + +func (df *DiscordFaucet) discordFaucet() error { + // Create a new Discord session using the provided bot token. + dg, err := discordgo.New("Bot " + df.opts.BotToken) + if err != nil { + fmt.Println("failed to create discord bot session.", err) + return err + } + + df.session = dg + + // we only care about receiving message events. + dg.Identify.Intents = discordgo.IntentsGuildMessages + + // Register the messageCreate func as a callback for MessageCreate events. + dg.AddHandler(func(s *discordgo.Session, m *discordgo.MessageCreate) { + // Ignore all messages created by the bot itself + // This is a good practice. + if m.Author.ID == s.State.User.ID { + return + } + // ignore message from other channels + if m.ChannelID != df.opts.Channel { + return + } + + // ignore message from other discord guild/server + + if m.GuildID != df.opts.Guild { + return + } + + // ignore message does not mention anyone + if len(m.Mentions) == 0 { + return + } + + user := m.Mentions[0] + + // ingore messages that doe not mentiont this bot + + if user.Bot != true || user.Username != df.opts.BotName { + return + } + + var res string + // retrive toAddress from received disocrd message + toAddr, bal, err := df.process(m) + if err != nil { + + res = fmt.Sprintf("%s", err) + dg.ChannelMessageSend(m.ChannelID, "<@"+m.Author.ID+"> "+res) + + fmt.Printf("channel %s:<@%s> %s", m.ChannelID, m.Author.Username, res) + + return + + } + var send std.Coins + // if the account has no balance we give it upto the full. + if bal.IsZero() { + send = std.NewCoins(perAccountLimit) + } else { + send, _ = std.ParseCoins(df.opts.Send) + } + + err = df.sendAmountTo(toAddr, send) + if err != nil { + + dg.ChannelMessageSend(m.ChannelID, "<@"+m.Author.ID+"> "+"faucet failed") + + fmt.Printf("channel %s:<@%s>%v", m.ChannelID, m.Author.Username, err) + + return + + } + + var amount string + for _, v := range send { + amount += v.String() + " " + } + + res = fmt.Sprintf("Cha-Ching! %s +%s", toAddr, amount) + + dg.ChannelMessageSend(m.ChannelID, "<@"+m.Author.ID+"> "+res) + }) + + return nil +} + +// This function will be called every time a new +// message is created on any channel that the authenticated bot has access to. +// It returns true and valid response if the message from discord is valid. +// Other we returns false with an empty string,which we should ingore. +func (df *DiscordFaucet) process(m *discordgo.MessageCreate) (string, std.Coin, error) { + validAddr, err := retrieveAddr(m.Content) + + zero := std.Coin{} + + if err != nil { + return "", zero, errors.New("No valid addresse: %s", err) + } + + bal, err := df.checkBalance(validAddr) + if err != nil { + return "", zero, errors.New("It seems our faucet is not working properly %s", err) + } + + if bal.IsGTE(perAccountLimit) { + return "", zero, errors.New("Your account %s still has %d%s, no need to accumulate more testing tokens", validAddr, bal.Amount, bal.Denom) + } + + return validAddr, bal, nil +} + +// retrive the first string with valid addresse length +func retrieveAddr(message string) (string, error) { + var addr string + words := strings.Fields(message) + + for _, w := range words { + + s := strings.TrimFunc(w, func(r rune) bool { + return !unicode.IsLetter(r) && !unicode.IsNumber(r) + }) + + if len(s) == addressStringLength { + addr = s + break + } + } + + ok, err := isValid(addr) + + if !ok { + return "", err + } + + return addr, nil +} + +// A valid address format + +func isValid(addr string) (bool, error) { + // validate prefix + + if strings.HasPrefix(addr, crypto.Bech32AddrPrefix) == false { + return false, errors.New("The address does not have correct prefix %s", crypto.Bech32AddrPrefix) + } + + if addressStringLength != len(addr) { + return false, errors.New("The address does not have correct length %d", addressStringLength) + } + + _, err := crypto.AddressFromBech32(addr) + if err != nil { + return false, errors.Wrap(err, "parsing address") + } + return true, nil +} + +// return true if the account balance is within limit +// return amount of token from +func (df *DiscordFaucet) checkBalance(addr string) (std.Coin, error) { + qopts := client.QueryOptions{ + Path: fmt.Sprintf("auth/accounts/%s", addr), + } + qopts.Remote = df.opts.Remote + qres, err := client.QueryHandler(qopts) + if err != nil { + return std.Coin{}, errors.Wrap(err, "query account") + } + var acc struct{ BaseAccount std.BaseAccount } + err = amino.UnmarshalJSON(qres.Response.Data, &acc) + if err != nil { + return std.Coin{}, err + } + + balances := acc.BaseAccount.GetCoins() + + bal := std.Coin{Denom: "ugnot", Amount: 0} + + if len(balances) > 0 { + for i, v := range balances { + if v.Denom == "ugnot" { + bal = balances[i] + } + } + } + + return bal, nil +} diff --git a/gnodiscord/faucet.go b/gnodiscord/faucet.go new file mode 100644 index 0000000..6af25c1 --- /dev/null +++ b/gnodiscord/faucet.go @@ -0,0 +1,284 @@ +package main + +import ( + "fmt" + "os" + "os/signal" + "strconv" + "syscall" + + "github.com/bwmarrin/discordgo" + "github.com/gnolang/gno/pkgs/amino" + "github.com/gnolang/gno/pkgs/command" + + "github.com/gnolang/gno/pkgs/crypto/keys" + "github.com/gnolang/gno/pkgs/crypto/keys/client" + "github.com/gnolang/gno/pkgs/errors" + + "github.com/gnolang/gno/pkgs/sdk/vm" + "github.com/gnolang/gno/pkgs/std" +) + +var perAccountLimit = std.NewCoin("ugnot", 350000000) // 350 gnot + +// A valid gno address string length is 40 +const addressStringLength = 40 + +type faucetOptions struct { + client.BaseOptions // home, ... + ChainID string `flag:"chain-id" help:"chain id must provide"` + + GasWanted int64 `flag:"gas-wanted" help:"gas requested for tx"` + GasFee string `flag:"gas-fee" help:"gas payment fee"` + Send string `flag:"send" help:"send coins per request"` + Memo string `flag:"memo" help:"any descriptive text"` + PkgPath string `flag:"pkgpath" help:"pacakge holding the faucet fund"` + Limit string `flag:"limit" help:"per transfer and per user account limit"` + BotToken string `flag:"token" help:"discord bot token - must provide"` + BotName string `flag:"bot-name" help:"discord bot name - must provide"` + Channel string `flag:"channel" help:"discord bot channel id - must provide"` + Guild string `flag:"guild" help:"discord bot guild/server id - must provide"` +} + +var DefaultFaucetOptions = faucetOptions{ + BaseOptions: client.DefaultBaseOptions, + ChainID: "", // must override + + GasWanted: 800000, + GasFee: "1000000ugnot", + Send: "5000000ugnot", + Memo: "disccord faucet", + PkgPath: "gno.land/r/faucet", + Limit: "350000000ugnot", + + BotToken: "", // must override + BotName: "", // must override + Channel: "", // must override + Guild: "", // must override + +} + +// DiscordFaucet access local keybase, remote chain endpoint discord session +type DiscordFaucet struct { + // local wallet key store + keybase keys.Keybase + + keyinfo keys.Info + + keyname string + + keypass string + + sequence uint64 + + // discord session + session *discordgo.Session + + // other faucetOptions + opts faucetOptions +} + +// NewDiscordFaucet construct an instance +func NewDiscordFaucet(name, pass string, opts faucetOptions) (*DiscordFaucet, error) { + kb, err := keys.NewKeyBaseFromDir(opts.Home) + if err != nil { + return nil, err + } + info, err := kb.GetByName(name) + if err != nil { + return nil, err + } + + // sign a dummy value to validate key pass + const dummy = "test" + _, _, err = kb.Sign(name, pass, []byte(dummy)) + if err != nil { + return nil, err + } + + return &DiscordFaucet{ + keybase: kb, + keyinfo: info, + keyname: name, + keypass: pass, + + opts: opts, + }, nil +} + +func faucetApp(cmd *command.Command, args []string, iopts interface{}) error { + opts := iopts.(faucetOptions) + + if len(args) != 1 { + cmd.ErrPrintfln("Usage: facuet ") + return errors.New("invalid args") + } + + if opts.ChainID == "" { + return errors.New("chain-id not specified") + } + if opts.BotToken == "" { + return errors.New("discord bot token not specified") + } + + if opts.Channel == "" { + return errors.New("discord bot channel id not specified") + } + + if opts.BotName == "" { + return errors.New("discord bot name not specified") + } + + if opts.Guild == "" { + return errors.New("discord bot guild/server id not specified") + } + + remote := opts.Remote + if remote == "" || remote == "y" { + return errors.New("missing remote url") + } + + var err error + perAccountLimit, err = std.ParseCoin(opts.Limit) + if err != nil { + panic(err) + } + + // XXX XXX + // Read supply account pubkey. + + name := args[0] + var pass string + if opts.Quiet { + pass, err = cmd.GetPassword("", false) + } else { + pass, err = cmd.GetPassword("Enter password.", false) + } + if err != nil { + return err + } + + // validate password + + // start a discord session + + df, err := NewDiscordFaucet(name, pass, opts) + if err != nil { + return err + } + // Start the bot + + err = df.Start() + if err != nil { + return err + } + + // Cleanly close down the Discord session. + defer df.Close() + + // Wait here until CTRL-C or other term signal is received. + fmt.Println("Bot is now running. Press CTRL-C to exit.") + sc := make(chan os.Signal, 1) + signal.Notify(sc, syscall.SIGINT, syscall.SIGTERM, os.Interrupt, os.Kill) + <-sc + + return nil +} + +// call faucet contract to send tokens to requester +func (df *DiscordFaucet) sendAmountTo(to string, send std.Coins) error { + function := "Transfer" + // Read faucet account pubkey. + + faucet := df.keyinfo.GetAddress() + + // parse gas wanted & fee. + gaswanted := df.opts.GasWanted + + gasfee, err := std.ParseCoin(df.opts.GasFee) + if err != nil { + panic(err) + } + sendAmount := strconv.FormatInt(send.AmountOf("ugnot"), 10) + // construct msg & tx and marshal. + msg := vm.MsgCall{ + Caller: faucet, + PkgPath: df.opts.PkgPath, + Func: function, + Args: []string{to, sendAmount}, + } + tx := std.Tx{ + Msgs: []std.Msg{msg}, + Fee: std.NewFee(gaswanted, gasfee), + Signatures: nil, + Memo: df.opts.Memo, + } + + return df.signAndBroadcast(tx) +} + +func (df *DiscordFaucet) signAndBroadcast(tx std.Tx) error { + // query account to get account number and sequece + + accountAddr := df.keyinfo.GetAddress().String() + + qopts := client.QueryOptions{ + Path: fmt.Sprintf("auth/accounts/%s", accountAddr), + } + qopts.Remote = df.opts.Remote + qres, err := client.QueryHandler(qopts) + if err != nil { + return errors.Wrap(err, "query account") + } + var qret struct{ BaseAccount std.BaseAccount } + err = amino.UnmarshalJSON(qres.Response.Data, &qret) + if err != nil { + return err + } + + // sign tx + accountNumber := qret.BaseAccount.AccountNumber + sequence := qret.BaseAccount.Sequence + sopts := client.SignOptions{ + Sequence: &sequence, + AccountNumber: &accountNumber, + ChainID: df.opts.ChainID, + NameOrBech32: df.keyname, + TxJson: amino.MustMarshalJSON(tx), + } + sopts.Home = df.opts.Home + sopts.Pass = df.keypass + + signedTx, err := client.SignHandler(sopts) + if err != nil { + return errors.Wrap(err, "sign tx") + } + + // broadcast tx bytes. + + // broadcast signed tx + bopts := client.BroadcastOptions{ + Tx: signedTx, + } + bopts.Remote = df.opts.Remote + bres, err := client.BroadcastHandler(bopts) + if err != nil { + return errors.Wrap(err, "broadcast tx") + } + if bres.CheckTx.IsErr() { + return errors.Wrap(bres.CheckTx.Error, "check transaction failed: log:%s", bres.CheckTx.Log) + } + if bres.DeliverTx.IsErr() { + return errors.Wrap(bres.DeliverTx.Error, "deliver transaction failed: log:%s", bres.DeliverTx.Log) + } + + fmt.Println("Message Delivered!") + fmt.Println("GAS WANTED:", bres.DeliverTx.GasWanted) + fmt.Println("GAS USED: ", bres.DeliverTx.GasUsed) + + if string(bres.DeliverTx.Data) != "(\"\" string)" { + return errors.New(string(bres.DeliverTx.Data)) + } + + return nil +} diff --git a/gnodiscord/main.go b/gnodiscord/main.go new file mode 100644 index 0000000..2d236db --- /dev/null +++ b/gnodiscord/main.go @@ -0,0 +1,54 @@ +package main + +import ( + "os" + + "github.com/gnolang/gno/pkgs/command" + "github.com/gnolang/gno/pkgs/errors" +) + +type ( + // AppItem is an app mapped to a subcommand + AppItem = command.AppItem + + // AppList holds a list of subcommands apps + AppList = command.AppList +) + +var mainApps AppList = []AppItem{ + {App: faucetApp, Name: "faucet", Desc: "discord faucet", Defaults: DefaultFaucetOptions}, +} + +func main() { + cmd := command.NewStdCommand() + exec := os.Args[0] + args := os.Args[1:] + err := runMain(cmd, exec, args) + if err != nil { + cmd.ErrPrintfln("%s", err.Error()) + cmd.ErrPrintfln("%#v", err) + return // exit + } +} + +func runMain(cmd *command.Command, exec string, args []string) error { + // show help message. + if len(args) == 0 || args[0] == "help" || args[0] == "--help" { + cmd.Println("available subcommands:") + for _, appItem := range mainApps { + cmd.Printf(" %s - %s\n", appItem.Name, appItem.Desc) + } + return nil + } + + // switch on first argument. + for _, appItem := range mainApps { + if appItem.Name == args[0] { + err := cmd.Run(appItem.App, args[1:], appItem.Defaults) + return err // done + } + } + + // unknown app command! + return errors.New("unknown command " + args[0]) +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..43d1a56 --- /dev/null +++ b/go.mod @@ -0,0 +1,51 @@ +module github.com/gnolang/community-faucet-bot + +go 1.18 + +require ( + github.com/bwmarrin/discordgo v0.26.1 + github.com/gnolang/gno v0.0.0-20221023190340-45bad87c8d41 +) + +require ( + github.com/btcsuite/btcd v0.22.0-beta.0.20220111032746-97732e52810c // indirect + github.com/btcsuite/btcd/btcutil v1.1.1 // indirect + github.com/cespare/xxhash v1.1.0 // indirect + github.com/cespare/xxhash/v2 v2.1.1 // indirect + github.com/cockroachdb/apd v1.1.0 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/dgraph-io/badger/v3 v3.2103.2 // indirect + github.com/dgraph-io/ristretto v0.1.0 // indirect + github.com/dustin/go-humanize v1.0.0 // indirect + github.com/gnolang/cors v1.8.1 // indirect + github.com/gnolang/overflow v0.0.0-20170615021017-4d914c927216 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b // indirect + github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6 // indirect + github.com/golang/protobuf v1.5.0 // indirect + github.com/golang/snappy v0.0.3 // indirect + github.com/google/flatbuffers v1.12.1 // indirect + github.com/gorilla/websocket v1.4.2 // indirect + github.com/jmhodges/levigo v1.0.0 // indirect + github.com/klauspost/compress v1.12.3 // indirect + github.com/libp2p/go-buffer-pool v0.0.2 // indirect + github.com/linxGnu/grocksdb v1.7.2 // indirect + github.com/pelletier/go-toml v1.9.3 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/stretchr/testify v1.7.1 // indirect + github.com/syndtr/goleveldb v1.0.0 // indirect + github.com/tecbot/gorocksdb v0.0.0-20191217155057-f0fad39f321c // indirect + go.etcd.io/bbolt v1.3.6 // indirect + go.opencensus.io v0.22.5 // indirect + go.uber.org/atomic v1.7.0 // indirect + go.uber.org/multierr v1.8.0 // indirect + golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b // indirect + golang.org/x/mod v0.4.2 // indirect + golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985 // indirect + golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1 // indirect + golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b // indirect + golang.org/x/tools v0.1.0 // indirect + google.golang.org/protobuf v1.27.1 // indirect + gopkg.in/yaml.v3 v3.0.0-20220512140231-539c8e751b99 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..724d424 --- /dev/null +++ b/go.sum @@ -0,0 +1,283 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/OneOfOne/xxhash v1.2.2 h1:KMrpdQIwFcEqXDklaen+P1axHaj9BSKzvpUUfnHldSE= +github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= +github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII= +github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= +github.com/btcsuite/btcd v0.20.1-beta/go.mod h1:wVuoA8VJLEcwgqHBwHmzLRazpKxTv13Px/pDuV7OomQ= +github.com/btcsuite/btcd v0.22.0-beta.0.20220111032746-97732e52810c h1:lnAMg3ra/Gw4AkRMxrxYs8nrprWsHowg8H9zaYsJOo4= +github.com/btcsuite/btcd v0.22.0-beta.0.20220111032746-97732e52810c/go.mod h1:tjmYdS6MLJ5/s0Fj4DbLgSbDHbEqLJrtnHecBFkdz5M= +github.com/btcsuite/btcd/btcec/v2 v2.1.1/go.mod h1:ctjw4H1kknNJmRN4iP1R7bTQ+v3GJkZBd6mui8ZsAZE= +github.com/btcsuite/btcd/btcutil v1.0.0/go.mod h1:Uoxwv0pqYWhD//tfTiipkxNfdhG9UrLwaeswfjfdF0A= +github.com/btcsuite/btcd/btcutil v1.1.1 h1:hDcDaXiP0uEzR8Biqo2weECKqEw0uHDZ9ixIWevVQqY= +github.com/btcsuite/btcd/btcutil v1.1.1/go.mod h1:nbKlBMNm9FGsdvKvu0essceubPiAcI57pYBNnsLAa34= +github.com/btcsuite/btcd/chaincfg/chainhash v1.0.0/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc= +github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f/go.mod h1:TdznJufoqS23FtqVCzL0ZqgP5MqXbb4fg/WgDys70nA= +github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d/go.mod h1:+5NJ2+qvTyV9exUAL/rxXi3DcLg2Ts+ymUAY5y4NvMg= +github.com/btcsuite/btcutil v1.0.2 h1:9iZ1Terx9fMIOtq1VrwdqfsATL9MC2l8ZrUY6YZ2uts= +github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd/go.mod h1:HHNXQzUsZCxOoE+CPiyCTO6x34Zs86zZUiwtpXoGdtg= +github.com/btcsuite/goleveldb v0.0.0-20160330041536-7834afc9e8cd/go.mod h1:F+uVaaLLH7j4eDXPRvw78tMflu7Ie2bzYOH4Y8rRKBY= +github.com/btcsuite/goleveldb v1.0.0/go.mod h1:QiK9vBlgftBg6rWQIj6wFzbPfRjiykIEhBH4obrXJ/I= +github.com/btcsuite/snappy-go v0.0.0-20151229074030-0bdef8d06723/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc= +github.com/btcsuite/snappy-go v1.0.0/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc= +github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtEyQwv5/p4Mg4C0fgbePVuGr935/5ddU9Z3TmDRY= +github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46fmI40EZs= +github.com/bwmarrin/discordgo v0.26.1 h1:AIrM+g3cl+iYBr4yBxCBp9tD9jR3K7upEjl0d89FRkE= +github.com/bwmarrin/discordgo v0.26.1/go.mod h1:NJZpH+1AfhIcyQsPeuBKsUtYrRnjkyu0kIVMCHkZtRY= +github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko= +github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= +github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY= +github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I= +github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= +github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= +github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= +github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= +github.com/davecgh/go-spew v0.0.0-20171005155431-ecdeabc65495/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +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.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeCxkaw7y45JueMRL4DIyJDKs= +github.com/decred/dcrd/lru v1.0.0/go.mod h1:mxKOwFd7lFjN2GZYsiz/ecgqR6kkYAl+0pz0tEMk218= +github.com/dgraph-io/badger/v3 v3.2103.2 h1:dpyM5eCJAtQCBcMCZcT4UBZchuTJgCywerHHgmxfxM8= +github.com/dgraph-io/badger/v3 v3.2103.2/go.mod h1:RHo4/GmYcKKh5Lxu63wLEMHJ70Pac2JqZRYGhlyAo2M= +github.com/dgraph-io/ristretto v0.1.0 h1:Jv3CGQHp9OjuMBSne1485aDpUkTKEcUqF+jm/LuerPI= +github.com/dgraph-io/ristretto v0.1.0/go.mod h1:fux0lOrBhrVCJd3lcTHsIJhq1T2rokOu6v9Vcb3Q9ug= +github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2 h1:tdlZCpZ/P9DhczCTSixgIKmwPv6+wP5DGjqLYw5SUiA= +github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= +github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo= +github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/facebookgo/ensure v0.0.0-20200202191622-63f1cf65ac4c h1:8ISkoahWXwZR41ois5lSJBSVw4D0OV19Ht/JSTzvSv0= +github.com/facebookgo/stack v0.0.0-20160209184415-751773369052 h1:JWuenKqqX8nojtoVVWjGfOF9635RETekkoH6Cc9SX0A= +github.com/facebookgo/subset v0.0.0-20200203212716-c811ad88dec4 h1:7HZCaLC5+BZpmbhCOZJ293Lz68O7PYrF2EzeiFMwCLk= +github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= +github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +github.com/gnolang/cors v1.8.1 h1:D3y1DMoWcgGpCefHwD4UHjy1w1163sfczZyy7b5wH8o= +github.com/gnolang/cors v1.8.1/go.mod h1:g7HJhHH+N1r+oRrb7ckR2J6xp5es4EizpAP0JpfgVgU= +github.com/gnolang/gno v0.0.0-20221023190340-45bad87c8d41 h1:8+r2hFuGtaufW9LDAA42lPC8/SBDxk+vjEsbyKeTojE= +github.com/gnolang/gno v0.0.0-20221023190340-45bad87c8d41/go.mod h1:hDQVHl8teTSs4lUy4R9kKbGxvmH1P5q5HYIqZy1t3Oc= +github.com/gnolang/overflow v0.0.0-20170615021017-4d914c927216 h1:GKvsK3oLWG9B1GL7WP/VqwM6C92j5tIvB844oggL9Lk= +github.com/gnolang/overflow v0.0.0-20170615021017-4d914c927216/go.mod h1:xJhtEL7ahjM1WJipt89gel8tHzfIl/LyMY+lCYh38d8= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6 h1:ZgQEtGgCBiWRM39fZuwSd1LwSqqSW0hOdXCYYDX0R3I= +github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.0 h1:LUVKkCeviFUMKqHa4tXIIij/lbhnMbP7Fn5wKdKkRh4= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/golang/snappy v0.0.3 h1:fHPg5GQYlCeLIPB9BZqMVR5nR9A+IM5zcgeTdjMYmLA= +github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/flatbuffers v1.12.1 h1:MVlul7pQNoDzWRLTw5imwYsl+usrS1TXG2H4jg6ImGw= +github.com/google/flatbuffers v1.12.1/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/gofuzz v1.0.0 h1:A8PeW59pxE9IoFRqBp37U+mSNaQoZ46F1f0f863XSXw= +github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= +github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/jaekwon/testify v1.6.1 h1:4AtAJcR9GzXN5W4DdY7ie74iCPiJV1JJUJL90t2ZUyw= +github.com/jessevdk/go-flags v0.0.0-20141203071132-1679536dcc89/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= +github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= +github.com/jmhodges/levigo v1.0.0 h1:q5EC36kV79HWeTBWsod3mG11EgStG3qArTKcvlksN1U= +github.com/jmhodges/levigo v1.0.0/go.mod h1:Q6Qx+uH3RAqyK4rFQroq9RL7mdkABMcfhEI+nNuzMJQ= +github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlTRt3OuAQ= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4= +github.com/klauspost/compress v1.12.3 h1:G5AfA94pHPysR56qqrkO2pxEexdDzrpFJ6yt/VqWxVU= +github.com/klauspost/compress v1.12.3/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg= +github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/lib/pq v1.10.7 h1:p7ZhMD+KsSRozJr34udlUrhboJwWAgCg34+/ZZNvZZw= +github.com/libp2p/go-buffer-pool v0.0.2 h1:QNK2iAFa8gjAe1SPz6mHSMuCcjs+X1wlHzeOSqcmlfs= +github.com/libp2p/go-buffer-pool v0.0.2/go.mod h1:MvaB6xw5vOrDl8rYZGLFdKAuk/hRoRZd1Vi32+RXyFM= +github.com/linxGnu/grocksdb v1.7.2 h1:5SVU++WAyOzk4jG0pXrI9plvZEPRCRbh4UrDhAxrktM= +github.com/linxGnu/grocksdb v1.7.2/go.mod h1:3tKVazPI6orwnNtFAOSRZwhi6bMOkc3+kREb8qpKaSE= +github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/nxadm/tail v1.4.4 h1:DQuhQpB1tVlglWS2hLQ5OV6B5r8aGxSrPc5Qo6uTN78= +github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= +github.com/onsi/ginkgo v1.14.0 h1:2mOpI4JVVPBN+WQRa0WKH2eXR+Ey+uK4n7Zj0aYpIQA= +github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= +github.com/onsi/gomega v1.4.1/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= +github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= +github.com/onsi/gomega v1.10.1 h1:o0+MgICZLuZ7xjH7Vx6zS/zcu93/BEp1VwkIW1mEXCE= +github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= +github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= +github.com/pelletier/go-toml v1.9.3 h1:zeC5b1GviRUyKYd6OJPvBU/mcVDVoL1OhT17FCt5dSQ= +github.com/pelletier/go-toml v1.9.3/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= +github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI= +github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= +github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= +github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= +github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/syndtr/goleveldb v1.0.0 h1:fBdIW9lB4Iz0n9khmH8w27SJ3QEJ7+IgjPEwGSZiFdE= +github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ= +github.com/tecbot/gorocksdb v0.0.0-20191217155057-f0fad39f321c h1:g+WoO5jjkqGAzHWCjJB1zZfXPIAaDpzXIEJ0eS6B5Ok= +github.com/tecbot/gorocksdb v0.0.0-20191217155057-f0fad39f321c/go.mod h1:ahpPrc7HpcfEWDQRZEmnXMzHY03mLDYMCxeDzy46i+8= +github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= +github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +go.etcd.io/bbolt v1.3.6 h1:/ecaJf0sk1l4l6V4awd65v2C3ILy7MSj+s/x1ADCIMU= +go.etcd.io/bbolt v1.3.6/go.mod h1:qXsaaIqmgQH0T+OPdb99Bf+PKfBBQVAdyD6TY9G8XM4= +go.opencensus.io v0.22.5 h1:dntmOdLpSpHlVqbW5Eay97DelsZHe+55D+xC6i0dDS0= +go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= +go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw= +go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/multierr v1.8.0 h1:dg6GjLku4EH+249NNmoIciG9N/jURbDG+pFlTkhzIC8= +go.uber.org/multierr v1.8.0/go.mod h1:7EAYxJLBy9rStEaz58O2t4Uvip6FSURkq8/ppBp95ak= +golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b h1:7mWr3k41Qtv8XlltBkDkl8LoP3mpSgBW8BUoxtEdbXg= +golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.2 h1:Gz96sIWK3OalVv/I/qNygP42zyoKp3xptRVCWRFEBvo= +golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20180719180050-a680a1efc54d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200813134508-3edf25e44fcc/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985 h1:4CSI6oo7cOjJKajidEljs9h+uP0rRZBPPPhcCbj5mw8= +golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200814200057-3d37ad5750ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200923182605-d9f96fdee20d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1 h1:SrN+KX8Art/Sf4HNj6Zcz06G7VEz+7w9tdXTPOZ7+l4= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b h1:9zKuko04nR4gjZ4+DNjHqRlAJqbJETHwiNKDqTfOjfE= +golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.0 h1:po9/4sTYwZU9lPhi1tOrb4hCv3qrhiQ77LZfGa2OjwY= +golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.27.1 h1:SnqbnDw1V7RiZcXPx5MEeqPv2s79L9i7BJUlG/+RurQ= +google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20220512140231-539c8e751b99 h1:dbuHpmKjkDzSOMKAWl10QNlgaZUd3V1q99xc81tt2Kc= +gopkg.in/yaml.v3 v3.0.0-20220512140231-539c8e751b99/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/provision.sh b/provision.sh new file mode 100755 index 0000000..08e9c94 --- /dev/null +++ b/provision.sh @@ -0,0 +1,12 @@ +#!/bin/bash + +echo "deploy contract..." +./faucet.sh deploy + +echo "fund admin account..." +./send.sh test1 g14ykc8d4n2sr9lmlv80cgp2qu74emujyujvhe5w 80000000ugnot +echo "fund controller1 account..." +./send.sh test1 g1q0pjk6dd6lehd5q3gcpghvcf3rd6mqy7tge4va 80000000ugnot + +echo "add controller1" +./faucet.sh call controller g1q0pjk6dd6lehd5q3gcpghvcf3rd6mqy7tge4va diff --git a/send.sh b/send.sh new file mode 100755 index 0000000..8073e8a --- /dev/null +++ b/send.sh @@ -0,0 +1,7 @@ +#!/bin/bash +echo send - from to amount +gnokey maketx send $1 --to $2 --send $3 \ +--gas-wanted 50000 --gas-fee 100000ugnot \ +--remote test3.gno.land:36657 \ +--chainid test3 \ +--broadcast true diff --git a/startbot.sh b/startbot.sh new file mode 100755 index 0000000..7288417 --- /dev/null +++ b/startbot.sh @@ -0,0 +1,12 @@ +#!/bin/bash + +./build/gnobot faucet $1 \ +--pkgpath gno.land/r/faucet \ +--token 'ABC' \ +--bot-name "faucet" \ +--channel "1029999999999999999" \ +--guild "1019999999999999996" \ +--chain-id test3 \ +--remote test3.gno.land:36657 \ +--limit 300000000ugnot \ +--send 5000000ugnot From f39bfc2f9dba195c36b5ee21fd41663562e2f31a Mon Sep 17 00:00:00 2001 From: piux2 <> Date: Wed, 26 Oct 2022 11:06:38 -0700 Subject: [PATCH 2/3] remove strings --- examples/gno.land/r/faucet/faucet.gno | 1 - 1 file changed, 1 deletion(-) diff --git a/examples/gno.land/r/faucet/faucet.gno b/examples/gno.land/r/faucet/faucet.gno index 0b3fd06..d9b89b4 100644 --- a/examples/gno.land/r/faucet/faucet.gno +++ b/examples/gno.land/r/faucet/faucet.gno @@ -3,7 +3,6 @@ package faucet import ( "errors" "std" - "strings" "gno.land/p/ufmt" ) From f754bf0da4f5cd7ddb596983bcb40c05d20cfdf8 Mon Sep 17 00:00:00 2001 From: piux2 <> Date: Thu, 27 Oct 2022 01:14:32 -0700 Subject: [PATCH 3/3] remove duplicated directory --- gnodiscord/discord.go | 230 ---------------------------------- gnodiscord/faucet.go | 284 ------------------------------------------ gnodiscord/main.go | 54 -------- 3 files changed, 568 deletions(-) delete mode 100644 gnodiscord/discord.go delete mode 100644 gnodiscord/faucet.go delete mode 100644 gnodiscord/main.go diff --git a/gnodiscord/discord.go b/gnodiscord/discord.go deleted file mode 100644 index 0bccb05..0000000 --- a/gnodiscord/discord.go +++ /dev/null @@ -1,230 +0,0 @@ -package main - -import ( - "fmt" - "strings" - "unicode" - - "github.com/bwmarrin/discordgo" - - "github.com/gnolang/gno/pkgs/amino" - - "github.com/gnolang/gno/pkgs/crypto" - - "github.com/gnolang/gno/pkgs/crypto/keys/client" - "github.com/gnolang/gno/pkgs/errors" - "github.com/gnolang/gno/pkgs/std" -) - -// Start a discord session -func (df *DiscordFaucet) Start() error { - err := df.discordFaucet() - if err != nil { - return err - } - - // Open a websocket connection to Discord and begin listening. - - return df.session.Open() -} - -// Close discord session smoothly -func (df *DiscordFaucet) Close() { - df.session.Close() -} - -// it takes discord server API token and rpc client. -// We use rpc client to validate addresses and check abuses. -// and we cap the balance holding to 400 GNOT before issue new tokens from faucet - -func (df *DiscordFaucet) discordFaucet() error { - // Create a new Discord session using the provided bot token. - dg, err := discordgo.New("Bot " + df.opts.BotToken) - if err != nil { - fmt.Println("failed to create discord bot session.", err) - return err - } - - df.session = dg - - // we only care about receiving message events. - dg.Identify.Intents = discordgo.IntentsGuildMessages - - // Register the messageCreate func as a callback for MessageCreate events. - dg.AddHandler(func(s *discordgo.Session, m *discordgo.MessageCreate) { - // Ignore all messages created by the bot itself - // This is a good practice. - if m.Author.ID == s.State.User.ID { - return - } - // ignore message from other channels - if m.ChannelID != df.opts.Channel { - return - } - - // ignore message from other discord guild/server - - if m.GuildID != df.opts.Guild { - return - } - - // ignore message does not mention anyone - if len(m.Mentions) == 0 { - return - } - - user := m.Mentions[0] - - // ingore messages that doe not mentiont this bot - - if user.Bot != true || user.Username != df.opts.BotName { - return - } - - var res string - // retrive toAddress from received disocrd message - toAddr, bal, err := df.process(m) - if err != nil { - - res = fmt.Sprintf("%s", err) - dg.ChannelMessageSend(m.ChannelID, "<@"+m.Author.ID+"> "+res) - - fmt.Printf("channel %s:<@%s> %s", m.ChannelID, m.Author.Username, res) - - return - - } - var send std.Coins - // if the account has no balance we give it upto the full. - if bal.IsZero() { - send = std.NewCoins(perAccountLimit) - } else { - send, _ = std.ParseCoins(df.opts.Send) - } - - err = df.sendAmountTo(toAddr, send) - if err != nil { - - dg.ChannelMessageSend(m.ChannelID, "<@"+m.Author.ID+"> "+"faucet failed") - - fmt.Printf("channel %s:<@%s>%v", m.ChannelID, m.Author.Username, err) - - return - - } - - var amount string - for _, v := range send { - amount += v.String() + " " - } - - res = fmt.Sprintf("Cha-Ching! %s +%s", toAddr, amount) - - dg.ChannelMessageSend(m.ChannelID, "<@"+m.Author.ID+"> "+res) - }) - - return nil -} - -// This function will be called every time a new -// message is created on any channel that the authenticated bot has access to. -// It returns true and valid response if the message from discord is valid. -// Other we returns false with an empty string,which we should ingore. -func (df *DiscordFaucet) process(m *discordgo.MessageCreate) (string, std.Coin, error) { - validAddr, err := retrieveAddr(m.Content) - - zero := std.Coin{} - - if err != nil { - return "", zero, errors.New("No valid addresse: %s", err) - } - - bal, err := df.checkBalance(validAddr) - if err != nil { - return "", zero, errors.New("It seems our faucet is not working properly %s", err) - } - - if bal.IsGTE(perAccountLimit) { - return "", zero, errors.New("Your account %s still has %d%s, no need to accumulate more testing tokens", validAddr, bal.Amount, bal.Denom) - } - - return validAddr, bal, nil -} - -// retrive the first string with valid addresse length -func retrieveAddr(message string) (string, error) { - var addr string - words := strings.Fields(message) - - for _, w := range words { - - s := strings.TrimFunc(w, func(r rune) bool { - return !unicode.IsLetter(r) && !unicode.IsNumber(r) - }) - - if len(s) == addressStringLength { - addr = s - break - } - } - - ok, err := isValid(addr) - - if !ok { - return "", err - } - - return addr, nil -} - -// A valid address format - -func isValid(addr string) (bool, error) { - // validate prefix - - if strings.HasPrefix(addr, crypto.Bech32AddrPrefix) == false { - return false, errors.New("The address does not have correct prefix %s", crypto.Bech32AddrPrefix) - } - - if addressStringLength != len(addr) { - return false, errors.New("The address does not have correct length %d", addressStringLength) - } - - _, err := crypto.AddressFromBech32(addr) - if err != nil { - return false, errors.Wrap(err, "parsing address") - } - return true, nil -} - -// return true if the account balance is within limit -// return amount of token from -func (df *DiscordFaucet) checkBalance(addr string) (std.Coin, error) { - qopts := client.QueryOptions{ - Path: fmt.Sprintf("auth/accounts/%s", addr), - } - qopts.Remote = df.opts.Remote - qres, err := client.QueryHandler(qopts) - if err != nil { - return std.Coin{}, errors.Wrap(err, "query account") - } - var acc struct{ BaseAccount std.BaseAccount } - err = amino.UnmarshalJSON(qres.Response.Data, &acc) - if err != nil { - return std.Coin{}, err - } - - balances := acc.BaseAccount.GetCoins() - - bal := std.Coin{Denom: "ugnot", Amount: 0} - - if len(balances) > 0 { - for i, v := range balances { - if v.Denom == "ugnot" { - bal = balances[i] - } - } - } - - return bal, nil -} diff --git a/gnodiscord/faucet.go b/gnodiscord/faucet.go deleted file mode 100644 index 6af25c1..0000000 --- a/gnodiscord/faucet.go +++ /dev/null @@ -1,284 +0,0 @@ -package main - -import ( - "fmt" - "os" - "os/signal" - "strconv" - "syscall" - - "github.com/bwmarrin/discordgo" - "github.com/gnolang/gno/pkgs/amino" - "github.com/gnolang/gno/pkgs/command" - - "github.com/gnolang/gno/pkgs/crypto/keys" - "github.com/gnolang/gno/pkgs/crypto/keys/client" - "github.com/gnolang/gno/pkgs/errors" - - "github.com/gnolang/gno/pkgs/sdk/vm" - "github.com/gnolang/gno/pkgs/std" -) - -var perAccountLimit = std.NewCoin("ugnot", 350000000) // 350 gnot - -// A valid gno address string length is 40 -const addressStringLength = 40 - -type faucetOptions struct { - client.BaseOptions // home, ... - ChainID string `flag:"chain-id" help:"chain id must provide"` - - GasWanted int64 `flag:"gas-wanted" help:"gas requested for tx"` - GasFee string `flag:"gas-fee" help:"gas payment fee"` - Send string `flag:"send" help:"send coins per request"` - Memo string `flag:"memo" help:"any descriptive text"` - PkgPath string `flag:"pkgpath" help:"pacakge holding the faucet fund"` - Limit string `flag:"limit" help:"per transfer and per user account limit"` - BotToken string `flag:"token" help:"discord bot token - must provide"` - BotName string `flag:"bot-name" help:"discord bot name - must provide"` - Channel string `flag:"channel" help:"discord bot channel id - must provide"` - Guild string `flag:"guild" help:"discord bot guild/server id - must provide"` -} - -var DefaultFaucetOptions = faucetOptions{ - BaseOptions: client.DefaultBaseOptions, - ChainID: "", // must override - - GasWanted: 800000, - GasFee: "1000000ugnot", - Send: "5000000ugnot", - Memo: "disccord faucet", - PkgPath: "gno.land/r/faucet", - Limit: "350000000ugnot", - - BotToken: "", // must override - BotName: "", // must override - Channel: "", // must override - Guild: "", // must override - -} - -// DiscordFaucet access local keybase, remote chain endpoint discord session -type DiscordFaucet struct { - // local wallet key store - keybase keys.Keybase - - keyinfo keys.Info - - keyname string - - keypass string - - sequence uint64 - - // discord session - session *discordgo.Session - - // other faucetOptions - opts faucetOptions -} - -// NewDiscordFaucet construct an instance -func NewDiscordFaucet(name, pass string, opts faucetOptions) (*DiscordFaucet, error) { - kb, err := keys.NewKeyBaseFromDir(opts.Home) - if err != nil { - return nil, err - } - info, err := kb.GetByName(name) - if err != nil { - return nil, err - } - - // sign a dummy value to validate key pass - const dummy = "test" - _, _, err = kb.Sign(name, pass, []byte(dummy)) - if err != nil { - return nil, err - } - - return &DiscordFaucet{ - keybase: kb, - keyinfo: info, - keyname: name, - keypass: pass, - - opts: opts, - }, nil -} - -func faucetApp(cmd *command.Command, args []string, iopts interface{}) error { - opts := iopts.(faucetOptions) - - if len(args) != 1 { - cmd.ErrPrintfln("Usage: facuet ") - return errors.New("invalid args") - } - - if opts.ChainID == "" { - return errors.New("chain-id not specified") - } - if opts.BotToken == "" { - return errors.New("discord bot token not specified") - } - - if opts.Channel == "" { - return errors.New("discord bot channel id not specified") - } - - if opts.BotName == "" { - return errors.New("discord bot name not specified") - } - - if opts.Guild == "" { - return errors.New("discord bot guild/server id not specified") - } - - remote := opts.Remote - if remote == "" || remote == "y" { - return errors.New("missing remote url") - } - - var err error - perAccountLimit, err = std.ParseCoin(opts.Limit) - if err != nil { - panic(err) - } - - // XXX XXX - // Read supply account pubkey. - - name := args[0] - var pass string - if opts.Quiet { - pass, err = cmd.GetPassword("", false) - } else { - pass, err = cmd.GetPassword("Enter password.", false) - } - if err != nil { - return err - } - - // validate password - - // start a discord session - - df, err := NewDiscordFaucet(name, pass, opts) - if err != nil { - return err - } - // Start the bot - - err = df.Start() - if err != nil { - return err - } - - // Cleanly close down the Discord session. - defer df.Close() - - // Wait here until CTRL-C or other term signal is received. - fmt.Println("Bot is now running. Press CTRL-C to exit.") - sc := make(chan os.Signal, 1) - signal.Notify(sc, syscall.SIGINT, syscall.SIGTERM, os.Interrupt, os.Kill) - <-sc - - return nil -} - -// call faucet contract to send tokens to requester -func (df *DiscordFaucet) sendAmountTo(to string, send std.Coins) error { - function := "Transfer" - // Read faucet account pubkey. - - faucet := df.keyinfo.GetAddress() - - // parse gas wanted & fee. - gaswanted := df.opts.GasWanted - - gasfee, err := std.ParseCoin(df.opts.GasFee) - if err != nil { - panic(err) - } - sendAmount := strconv.FormatInt(send.AmountOf("ugnot"), 10) - // construct msg & tx and marshal. - msg := vm.MsgCall{ - Caller: faucet, - PkgPath: df.opts.PkgPath, - Func: function, - Args: []string{to, sendAmount}, - } - tx := std.Tx{ - Msgs: []std.Msg{msg}, - Fee: std.NewFee(gaswanted, gasfee), - Signatures: nil, - Memo: df.opts.Memo, - } - - return df.signAndBroadcast(tx) -} - -func (df *DiscordFaucet) signAndBroadcast(tx std.Tx) error { - // query account to get account number and sequece - - accountAddr := df.keyinfo.GetAddress().String() - - qopts := client.QueryOptions{ - Path: fmt.Sprintf("auth/accounts/%s", accountAddr), - } - qopts.Remote = df.opts.Remote - qres, err := client.QueryHandler(qopts) - if err != nil { - return errors.Wrap(err, "query account") - } - var qret struct{ BaseAccount std.BaseAccount } - err = amino.UnmarshalJSON(qres.Response.Data, &qret) - if err != nil { - return err - } - - // sign tx - accountNumber := qret.BaseAccount.AccountNumber - sequence := qret.BaseAccount.Sequence - sopts := client.SignOptions{ - Sequence: &sequence, - AccountNumber: &accountNumber, - ChainID: df.opts.ChainID, - NameOrBech32: df.keyname, - TxJson: amino.MustMarshalJSON(tx), - } - sopts.Home = df.opts.Home - sopts.Pass = df.keypass - - signedTx, err := client.SignHandler(sopts) - if err != nil { - return errors.Wrap(err, "sign tx") - } - - // broadcast tx bytes. - - // broadcast signed tx - bopts := client.BroadcastOptions{ - Tx: signedTx, - } - bopts.Remote = df.opts.Remote - bres, err := client.BroadcastHandler(bopts) - if err != nil { - return errors.Wrap(err, "broadcast tx") - } - if bres.CheckTx.IsErr() { - return errors.Wrap(bres.CheckTx.Error, "check transaction failed: log:%s", bres.CheckTx.Log) - } - if bres.DeliverTx.IsErr() { - return errors.Wrap(bres.DeliverTx.Error, "deliver transaction failed: log:%s", bres.DeliverTx.Log) - } - - fmt.Println("Message Delivered!") - fmt.Println("GAS WANTED:", bres.DeliverTx.GasWanted) - fmt.Println("GAS USED: ", bres.DeliverTx.GasUsed) - - if string(bres.DeliverTx.Data) != "(\"\" string)" { - return errors.New(string(bres.DeliverTx.Data)) - } - - return nil -} diff --git a/gnodiscord/main.go b/gnodiscord/main.go deleted file mode 100644 index 2d236db..0000000 --- a/gnodiscord/main.go +++ /dev/null @@ -1,54 +0,0 @@ -package main - -import ( - "os" - - "github.com/gnolang/gno/pkgs/command" - "github.com/gnolang/gno/pkgs/errors" -) - -type ( - // AppItem is an app mapped to a subcommand - AppItem = command.AppItem - - // AppList holds a list of subcommands apps - AppList = command.AppList -) - -var mainApps AppList = []AppItem{ - {App: faucetApp, Name: "faucet", Desc: "discord faucet", Defaults: DefaultFaucetOptions}, -} - -func main() { - cmd := command.NewStdCommand() - exec := os.Args[0] - args := os.Args[1:] - err := runMain(cmd, exec, args) - if err != nil { - cmd.ErrPrintfln("%s", err.Error()) - cmd.ErrPrintfln("%#v", err) - return // exit - } -} - -func runMain(cmd *command.Command, exec string, args []string) error { - // show help message. - if len(args) == 0 || args[0] == "help" || args[0] == "--help" { - cmd.Println("available subcommands:") - for _, appItem := range mainApps { - cmd.Printf(" %s - %s\n", appItem.Name, appItem.Desc) - } - return nil - } - - // switch on first argument. - for _, appItem := range mainApps { - if appItem.Name == args[0] { - err := cmd.Run(appItem.App, args[1:], appItem.Defaults) - return err // done - } - } - - // unknown app command! - return errors.New("unknown command " + args[0]) -}