eccfaucet/main.go

476 lines
13 KiB
Go

package main
import (
"bytes"
"crypto/tls"
"encoding/base64"
"encoding/gob"
"encoding/json"
"errors"
"flag"
"fmt"
"html/template"
"log"
"net/http"
"os"
"strconv"
"time"
"github.com/gobuffalo/packr"
"github.com/kelseyhightower/envconfig"
_ "github.com/lib/pq"
"github.com/muesli/cache2go"
"github.com/ybbus/jsonrpc"
)
const tapAmount = 1.0
const tapWaitMinutes = 2
const opStatusWaitSeconds = 120
type TapRequest struct {
NetworkAddress string
WalletAddress string
RequestedAt time.Time
}
type ECCfaucetConfig struct {
ListenPort string
ListenAddress string
RPCUser string
RPCPassword string
RPCHost string
RPCPort string
FundingAddress string
TLSCertFile string
TLSKeyFile string
}
func (c *ECCfaucetConfig) checkConfig() error {
if c.ListenPort == "" {
c.ListenPort = "3000"
}
if c.ListenAddress == "" {
c.ListenPort = "127.0.0.1"
}
if c.RPCHost == "" {
c.ListenPort = "localhost"
}
if c.ListenPort == "" {
c.ListenPort = "3000"
}
if c.FundingAddress == "" {
return fmt.Errorf("ECCFAUCET_FUNDINGADDRESS is required")
}
if (c.TLSCertFile == "" && c.TLSKeyFile != "") ||
(c.TLSCertFile != "" && c.TLSKeyFile == "") {
return fmt.Errorf("ECCFAUCET_TLSCERTFILE and ECCFAUCET_TLSKEYFILE are both required")
}
return nil
}
// ECCFaucet holds a zfaucet configuration
type ECCFaucet struct {
RPCConnetion jsonrpc.RPCClient
CurrentHeight int
UpdatedChainInfo time.Time
UpdatedWallet time.Time
Operations map[string]OperationStatus
ZcashdVersion string
ZcashNetwork string
FundingAddress string
TapRequests []*TapRequest
TapCache *cache2go.CacheTable
HomeHTML string
}
type SendAmount struct {
Address string `json:"address"`
Amount float32 `json:"amount"`
}
// TODO tag facet transactions, zaddr targets only
type SendAmountMemo struct {
SendAmount
Memo string
}
func (z *ECCFaucet) ClearCache() {
for {
now := time.Now()
fmt.Printf("Clearing cache: %d\n", len(z.TapRequests))
for _, t := range z.TapRequests {
fmt.Printf("Checking RemoteAddress: '%#v' - '%#v'\n", t.NetworkAddress, t.RequestedAt)
diff := now.Sub(t.RequestedAt)
if diff.Minutes() > tapWaitMinutes {
fmt.Printf("Old entry! : %#v\n", t)
}
}
time.Sleep(time.Second * 60 * tapWaitMinutes)
}
}
func (z *ECCFaucet) UpdateZcashInfo() {
for {
z.UpdatedChainInfo = time.Now()
var blockChainInfo *GetBlockchainInfo
if err := z.RPCConnetion.CallFor(&blockChainInfo, "getblockchaininfo"); err != nil {
fmt.Printf("Failed to get blockchaininfo: %s\n", err)
} else {
z.CurrentHeight = blockChainInfo.Blocks
z.ZcashNetwork = blockChainInfo.Chain
}
var info *GetBlockInfo
if err := z.RPCConnetion.CallFor(&info, "getinfo"); err != nil {
fmt.Printf("Failed to get getinfo: %s\n", err)
} else {
z.ZcashdVersion = strconv.Itoa(info.Version)
}
fmt.Println("Updated Zcashd Info")
time.Sleep(time.Second * 30)
}
}
func (z *ECCFaucet) WaitForOperation(opid string) (os OperationStatus, err error) {
var opStatus []struct {
CreationTime int `json:"creation_time"`
ID string `json:"id"`
Method string `json:"method"`
Result struct {
TxID string `json:"txid"`
}
Status string `json:"status"`
}
var parentList [][]string
var opList []string
opList = append(opList, opid)
parentList = append(parentList, opList)
fmt.Printf("opList: %s\n", opList)
fmt.Printf("parentList: %s\n", parentList)
// Wait for a few seconds for the operational status to become available
for i := 0; i < opStatusWaitSeconds; i++ {
if err := z.RPCConnetion.CallFor(
&opStatus,
"z_getoperationresult",
parentList,
); err != nil {
return os, fmt.Errorf("failed to call z_getoperationresult: %s", err)
} else {
fmt.Printf("op: %s, i: %d, status: %#v\n", opid, i, opStatus)
if len(opStatus) > 0 {
fmt.Printf("opStatus: %#v\n", opStatus[0])
//z.Operations[opid] = OperationStatus{
os = OperationStatus{
UpdatedAt: time.Now(),
TxID: opStatus[0].Result.TxID,
Status: opStatus[0].Status,
}
z.Operations[opid] = os
return os, nil
}
}
time.Sleep(time.Second * 1)
}
return os, errors.New("Timeout waiting for operations status")
}
func (z *ECCFaucet) ValidateFundingAddress() (bool, error) {
if z.FundingAddress == "" {
return false, errors.New("FundingAddressis required")
}
return true, nil
}
func (z *ECCFaucet) ZSendManyFaucet(remoteAddr string, remoteWallet string) (opStatus OperationStatus, err error) {
var op *string
amountEntry := SendAmount{
Address: remoteWallet,
Amount: tapAmount,
}
fmt.Printf("ZSendManyFaucet sending: %#v\n", amountEntry)
fmt.Printf("ZSendManyFaucet from funding address: %s\n", z.FundingAddress)
// if err != nil {
// return opStatus, err
// }
// Call z_sendmany with a single entry entry list
if err := z.RPCConnetion.CallFor(
&op,
"z_sendmany",
z.FundingAddress,
[]SendAmount{amountEntry},
); err != nil {
return opStatus, err
}
fmt.Printf("ZSendManyFaucet sent to %s: Address: %s %s\n", remoteWallet, remoteAddr, *op)
opStatus, err = z.WaitForOperation(*op)
if err != nil {
return opStatus, err
}
if opStatus.Status != "success" {
return opStatus, fmt.Errorf("Failed to send funds: %s", err)
}
tapRequest := &TapRequest{
NetworkAddress: remoteAddr,
WalletAddress: remoteWallet,
RequestedAt: time.Now(),
}
z.TapCache.Add(remoteAddr, tapWaitMinutes*60*time.Second, tapRequest)
z.TapRequests = append(z.TapRequests, tapRequest)
return opStatus, err
}
type GetBlockInfo struct {
Version int
}
func getBlockchainInfo(rpcClient jsonrpc.RPCClient) (blockChainInfo *GetBlockchainInfo, err error) {
if err := rpcClient.CallFor(&blockChainInfo, "getblockchaininfo"); err != nil {
return nil, err
}
return
}
func getInfo(rpcClient jsonrpc.RPCClient) (info *GetBlockInfo, err error) {
if err := rpcClient.CallFor(&info, "getinfo"); err != nil {
return nil, err
}
return info, nil
}
func main() {
versionFlag := flag.Bool("version", false, "print version information")
flag.Parse()
if *versionFlag {
fmt.Printf("(version=%s, branch=%s, gitcommit=%s)\n", Version, Branch, GitCommit)
fmt.Printf("(go=%s, user=%s, date=%s)\n", GoVersion, BuildUser, BuildDate)
os.Exit(0)
}
var zConfig ECCfaucetConfig
err := envconfig.Process("eccfaucet", &zConfig)
if err != nil {
log.Fatal(err.Error())
}
if err = zConfig.checkConfig(); err != nil {
log.Fatalf("Config error: %s", err)
}
fmt.Printf("zfaucet: %#v\n", zConfig)
basicAuth := base64.StdEncoding.EncodeToString([]byte(zConfig.RPCUser + ":" + zConfig.RPCPassword))
var z ECCFaucet
z.TapCache = cache2go.Cache("tapRequests")
z.FundingAddress = zConfig.FundingAddress
z.Operations = make(map[string]OperationStatus)
z.RPCConnetion = jsonrpc.NewClientWithOpts("http://"+zConfig.RPCHost+":"+zConfig.RPCPort,
&jsonrpc.RPCClientOpts{
CustomHeaders: map[string]string{
"Authorization": "Basic " + basicAuth,
}})
go z.ClearCache()
go z.UpdateZcashInfo()
box := packr.NewBox("./templates")
z.HomeHTML, err = box.FindString("eccfaucet.html")
if err != nil {
log.Fatal(err)
}
homeHandler := http.HandlerFunc(z.home)
balanceHandler := http.HandlerFunc(z.balance)
opsStatusHandler := http.HandlerFunc(z.opsStatus)
addressHandler := http.HandlerFunc(z.addresses)
mux := http.NewServeMux()
mux.Handle("/", homeHandler)
mux.Handle("/balance", z.OKMiddleware(balanceHandler))
mux.Handle("/addresses", z.OKMiddleware(addressHandler))
mux.Handle("/ops/status", z.OKMiddleware(opsStatusHandler))
log.Printf("Listening on :%s...\n", zConfig.ListenPort)
if zConfig.TLSCertFile != "" && zConfig.TLSKeyFile != "" {
// https://gist.github.com/denji/12b3a568f092ab951456
cfg := &tls.Config{
MinVersion: tls.VersionTLS12,
CurvePreferences: []tls.CurveID{tls.CurveP521, tls.CurveP384, tls.CurveP256},
PreferServerCipherSuites: true,
CipherSuites: []uint16{
tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA,
tls.TLS_RSA_WITH_AES_256_GCM_SHA384,
tls.TLS_RSA_WITH_AES_256_CBC_SHA,
},
}
srv := &http.Server{
Addr: zConfig.ListenAddress + ":" + zConfig.ListenPort,
Handler: mux,
TLSConfig: cfg,
TLSNextProto: make(map[string]func(*http.Server, *tls.Conn, http.Handler), 0),
}
err = srv.ListenAndServeTLS(zConfig.TLSCertFile, zConfig.TLSKeyFile)
} else {
err = http.ListenAndServe(zConfig.ListenAddress+":"+zConfig.ListenPort, mux)
}
log.Fatal(err)
}
// OperationStatus describes an rpc response
type OperationStatus struct {
UpdatedAt time.Time
Status string
TxID string
result interface{}
}
// home is the default request handler
func (z *ECCFaucet) home(w http.ResponseWriter, r *http.Request) {
// tData is the html template data
tData := struct {
Z *ECCFaucet
Msg string
}{
z,
"",
}
switch r.Method {
case http.MethodPost:
res, err := z.TapCache.Value(r.RemoteAddr)
if err == nil {
fmt.Println("Found value in cache:", res.Data().(*TapRequest).NetworkAddress)
tData.Msg = fmt.Sprintf("You may only tap the faucet every %d minutes\nPlease try again later\n", tapWaitMinutes)
break
} else {
fmt.Println("Error retrieving value from cache:", err)
}
if err := checkFaucetAddress(r.FormValue("address")); err != nil {
tData.Msg = fmt.Sprintf("Invalid address: %s", err)
break
}
opStatus, err := z.ZSendManyFaucet(r.RemoteAddr, r.FormValue("address"))
if err != nil {
tData.Msg = fmt.Sprintf("Failed to send funds: %s", err)
break
}
tData.Msg = fmt.Sprintf("Successfully submitted operation, transaction: %s", opStatus.TxID)
}
w.Header().Set("Content-Type", "text/html")
tmpl, err := template.New("name").Parse(z.HomeHTML)
if err != nil {
http.Error(w, err.Error(), 500)
}
tmpl.Execute(w, tData)
}
// OKMiddleware determines if a request is allowed before execution
func (z *ECCFaucet) OKMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Our middleware logic goes here...
next.ServeHTTP(w, r)
})
}
// Balance
func (z *ECCFaucet) balance(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
var totalBalance *z_gettotalbalance
if err := z.RPCConnetion.CallFor(&totalBalance, "z_gettotalbalance"); err != nil {
http.Error(w, err.Error(), 500)
return
}
out, err := json.Marshal(totalBalance)
if err != nil {
http.Error(w, err.Error(), 500)
return
}
fmt.Fprintf(w, string(out))
}
// opsStatus
func (z *ECCFaucet) opsStatus(w http.ResponseWriter, r *http.Request) {
// tData is the html template data
tData := struct {
Z *ECCFaucet
Ops *[]string
Type string
}{
z,
nil,
"opsStatus",
}
if err := z.RPCConnetion.CallFor(&tData.Ops, "z_listoperationids"); err != nil {
http.Error(w, err.Error(), 500)
return
}
w.Header().Set("Content-Type", "text/html")
tmpl, err := template.New("name").Parse(z.HomeHTML)
if err != nil {
http.Error(w, err.Error(), 500)
}
tmpl.Execute(w, tData)
}
// addresses
func (z *ECCFaucet) addresses(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
var addresses []WalletAddress
var zlist *[]string
var taddrs []interface{}
// Z addresses
if err := z.RPCConnetion.CallFor(&zlist, "z_listaddresses"); err != nil {
http.Error(w, err.Error(), 500)
return
}
for _, zaddr := range *zlist {
entry := WalletAddress{
Address: zaddr,
}
entry.Notes = append(entry.Notes, "z address")
addresses = append(addresses, entry)
}
// T addresses
if err := z.RPCConnetion.CallFor(&taddrs, "listaddressgroupings"); err != nil {
http.Error(w, fmt.Sprintf("Problem calling listaddressgroupings: %s", err.Error()), 500)
return
}
fmt.Printf("T addresses:\n%#v\n", taddrs)
// TODO: fix this mess
for _, a := range taddrs {
switch aResult := a.(type) {
case []interface{}:
for _, b := range aResult {
switch bResult := b.(type) {
case []interface{}:
for _, x := range bResult {
switch x.(type) {
case string:
taddr := fmt.Sprintf("%v", x)
fmt.Printf("Adding T Address: %s\n", taddr)
entry := WalletAddress{
Address: taddr,
}
entry.Notes = append(entry.Notes, "t address")
addresses = append(addresses, entry)
}
}
}
}
}
}
out, err := json.Marshal(addresses)
if err != nil {
http.Error(w, err.Error(), 500)
return
}
fmt.Fprintf(w, string(out))
}
// GetBytes returns a byte slice from an interface
func GetBytes(key interface{}) ([]byte, error) {
var buf bytes.Buffer
enc := gob.NewEncoder(&buf)
err := enc.Encode(key)
if err != nil {
return nil, err
}
return buf.Bytes(), nil
}