From 770ce4e21dfcf9cc72d727dabc9a997275b72e4a Mon Sep 17 00:00:00 2001 From: Nils Jakobi Date: Tue, 12 Jan 2021 22:37:37 +0100 Subject: [PATCH] first working version --- .gitignore | 3 + Dockerfile | 3 + README.md | 44 ++++++++++++ go.mod | 5 ++ go.sum | 2 + main.go | 206 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 263 insertions(+) create mode 100644 .gitignore create mode 100644 Dockerfile create mode 100755 README.md create mode 100644 go.mod create mode 100644 go.sum create mode 100644 main.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..80a2757 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +# binaries +addressWatcher +addressWatcher.exe diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..365dd63 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,3 @@ +FROM scratch +ADD addressWatcher /opt/addressWatcher/addressWatcher +ENTRYPOINT ["/opt/addressWatcher/addressWatcher"] \ No newline at end of file diff --git a/README.md b/README.md new file mode 100755 index 0000000..78e627e --- /dev/null +++ b/README.md @@ -0,0 +1,44 @@ +# addressWatcher + +## How to build +You can build this program using the following commandline +``` +CGO_ENABLED=0 go build -ldflags '-s -w' -trimpath . +``` + +## How to use +Fill all needed parameters and execute the binary +``` +./addressWatcher \ +-address YOUR_ADDRESS \ +-apikey YOUR_APIKEY_FOR_chainz.cryptoid.info \ +-coin SHORT_FOR_YOUR_COIN \ +-chatid TELEGRAM_CHATID \ +-token TELEGRAM_BOT_TOKEN \ +-name NICKNAME_OF_ADDRESS +``` + +## Prerequisits +1. Check if your Blockchain is available at https://chainz.cryptoid.info/ +1. If so, take note of the short handle in the url for your coin (e.g. `btc` for bitcoin, as taken from `https://btc.cryptoid.info/btc/`) +1. You need to create a Telegram bot first by talking to `@botfather` in Telegram and following the instructions +1. Create a seperate group for you (,some others) and your bot in Telegram +1. Invite `@RawDataBot` into your channel and read out the chatid (including the `-` sign at the front) e.g. `-123456` +1. Remove `@RawDataBot` from your channel +1. Request an API key from https://chainz.cryptoid.info/api.key.dws +1. Start this program somewhere it can run forever, e.g. a server + +## Use cases + +### Monitoring a Masternode +This program can be used to track rewards for a masternode. If you put in the address of the masternode this program will alert you of any changes made to the wallet's amount + +### Monitoring a donation address +If you have an address where people can donate cryptocurrency to, you can leave this program running to be alerted of any incoming donations + + +## Known issues + +### API goes down +From time to time the API does not respond, or goes into maintenance mode. If this happens, the API responds with an amount of 0. My program will pick this up and think of it as a withdrawal of all funds. +I need to find a way to check the API's readyness first. \ No newline at end of file diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..3bfcc6a --- /dev/null +++ b/go.mod @@ -0,0 +1,5 @@ +module git.nils.zone/nils/addressWatcher + +go 1.14 + +require github.com/shopspring/decimal v1.2.0 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..7042e94 --- /dev/null +++ b/go.sum @@ -0,0 +1,2 @@ +github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ= +github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= diff --git a/main.go b/main.go new file mode 100644 index 0000000..90384a2 --- /dev/null +++ b/main.go @@ -0,0 +1,206 @@ +package main + +import ( + "flag" + "fmt" + "io/ioutil" + "log" + "net/http" + "os" + "os/signal" + "strconv" + "strings" + "syscall" + "time" + + "github.com/shopspring/decimal" +) + +// Define global variables + +type config struct { + address string + apiKey string + coin string + chatID string + token string + name string +} + +func main() { + // flags + address := flag.String("address", "", "Address to monitor for changes") + apiKey := flag.String("apikey", "", "APIKEY from https://chainz.cryptoid.info") + coin := flag.String("coin", "", "Coin of address. You can lookup all supported coins at: https://chainz.cryptoid.info/ (use abbreviated form as you see in the url, e.g. Litecoin = ltc)") + chatID := flag.String("chatid", "", "Look up the ChatID of your channel by inviting @RawDataBot to your channel (and removing it afterwards)") + token := flag.String("token", "", "Telegram token for your bot, you need to create a bot by talking to @botfather first") + name := flag.String("name", "", "(Nick)name of address, will appear in Telegram message") + flag.Parse() + + c := config{ + address: *address, + apiKey: *apiKey, + coin: *coin, + chatID: *chatID, + token: *token, + name: *name, + } + + switch { + case c.address == "": + log.Fatalln("Please specify an address using -address") + case c.apiKey == "": + log.Fatalln("Please specify an API key using -apikey") + case c.chatID == "": + log.Fatalln("Please specify a chat ID using -chatid") + case c.coin == "": + log.Fatalln("Please specify a coin using -coin") + case c.token == "": + log.Fatalln("Please specify a token using -token") + case c.name == "": + log.Fatalln("Please specify a name using -name") + } + + // all checks completed starting script + log.Println("Passed all checks, starting watcher now!") + + // setup scheduler + // new stuff + // tick := time.NewTicker(time.Minute) + tick := time.NewTicker(time.Second * 10) + go scheduler(tick, c) + sigs := make(chan os.Signal, 1) + signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM) + <-sigs + tick.Stop() +} + +func scheduler(tick *time.Ticker, c config) error { + // variable to hold amound of last call + var previousAmount float64 + + // run once (directly on start) + previousAmount = task(c, previousAmount) + + // loop every time interval + for range tick.C { + previousAmount = task(c, previousAmount) + } + return nil +} + +func task(c config, previousAmount float64) float64 { + amount, err := parseAmount(c) + if err != nil { + log.Fatalf("can't parse amount, error is: %s\n", err) + } + + previousAmount, err = compareAmount(previousAmount, amount, c) + if err != nil { + log.Fatalf("can't compare amount, error is: %s\n", err) + } + return previousAmount +} + +func parseAmount(c config) (float64, error) { + // Define Variables + var url string + var amount float64 + + // assemble url + url = "https://chainz.cryptoid.info/" + c.coin + "/api.dws?q=getbalance&a=" + c.address + "&key=" + c.apiKey + + // form url + request, err := http.NewRequest(http.MethodGet, url, nil) + if err != nil { + return 0, fmt.Errorf("can't request %s, error is: %s", url, err) + } + + // add headers + request.Header.Set("Content-Type", "application/json") + + // request response + client := &http.Client{} + response, err := client.Do(request) + if err != nil { + return 0, fmt.Errorf("HTTPS request to url %s failed with error %s", url, err) + } + // read response into data variable + data, err := ioutil.ReadAll(response.Body) + if err != nil { + return 0, fmt.Errorf("can't read body %s, error is %s", response.Body, err) + } + + // convert []byte to int + amount, err = strconv.ParseFloat(string(data), 64) + if err != nil { + // can't parse string to number (this happens when website is in maintenance mode) + return 0, nil + } + + // output amount to console + log.Printf("Current amount of %s: %f\n", strings.ToUpper(c.coin), amount) + + return amount, nil +} + +// compareAmount compares the current amount a with the previous amount p +func compareAmount(p float64, a float64, c config) (f float64, err error) { + if a == p { + // current amount is equal to previous + } else if a > p { + // current amount is more than previous + err = sendToTelegram(decimal.NewFromFloat(a), decimal.NewFromFloat(p), false, c) + } else { + // current amount is less than previous + err = sendToTelegram(decimal.NewFromFloat(a), decimal.NewFromFloat(p), true, c) + } + if err != nil { + return 0, err + } + return a, nil +} + +// sendToTelegram sends amount a, and previous amount p, and bool less to telegram +func sendToTelegram(a decimal.Decimal, p decimal.Decimal, isLess bool, c config) error { + // Variables + url := "https://api.telegram.org/bot" + c.token + "/sendMessage?chat_id=" + c.chatID + "&text=" + var text string + + // make coin uppercase + c.coin = strings.ToUpper(c.coin) + + // see if less bool is true or false, to send out messages for added or withdrawn coins + if isLess { + // the current amount is less than previous + diff := p.Sub(a) + text = fmt.Sprintf("%s %s have been withdrawn from %s, totalling now %s %s", diff.String(), c.coin, c.name, a.String(), c.coin) + } else { + // amount is more than previous amount + diff := a.Sub(p) + text = fmt.Sprintf("%s %s have been deposited to %s, totalling now %s %s", diff.String(), c.coin, c.name, a.String(), c.coin) + } + + // construct url to be called + url = url + text + + // call url + request, err := http.NewRequest(http.MethodGet, url, nil) + if err != nil { + return fmt.Errorf("can't request %s, error is: %s", url, err) + } + + // add headers + request.Header.Set("Content-Type", "application/json") + + // request response + client := &http.Client{} + response, err := client.Do(request) + if err != nil { + return fmt.Errorf("HTTPS request to url %s failed with error %s", url, err) + } + if response.StatusCode != 200 { + return fmt.Errorf("unexpected status code from telegram API, error is %d", response.StatusCode) + } + return nil +}