mirror of
https://github.com/kaspanet/kaspad.git
synced 2025-07-06 12:52:31 +00:00
[NOD-495] Remove faucet to separate repository
This commit is contained in:
parent
4a0b7ad268
commit
6479820fb4
@ -1,121 +0,0 @@
|
|||||||
package config
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/jessevdk/go-flags"
|
|
||||||
"github.com/kaspanet/kaspad/dagconfig"
|
|
||||||
"github.com/kaspanet/kaspad/kasparov/logger"
|
|
||||||
"github.com/kaspanet/kaspad/util"
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
"path/filepath"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
defaultLogFilename = "faucet.log"
|
|
||||||
defaultErrLogFilename = "faucet_err.log"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
// Default configuration options
|
|
||||||
defaultLogDir = util.AppDataDir("faucet", false)
|
|
||||||
defaultDBAddress = "localhost:3306"
|
|
||||||
defaultHTTPListen = "0.0.0.0:8081"
|
|
||||||
|
|
||||||
// activeNetParams are the currently active net params
|
|
||||||
activeNetParams *dagconfig.Params
|
|
||||||
)
|
|
||||||
|
|
||||||
// Config defines the configuration options for the API server.
|
|
||||||
type Config struct {
|
|
||||||
LogDir string `long:"logdir" description:"Directory to log output."`
|
|
||||||
HTTPListen string `long:"listen" description:"HTTP address to listen on (default: 0.0.0.0:8081)"`
|
|
||||||
KasparovdURL string `long:"kasparovd-url" description:"The API server url to connect to"`
|
|
||||||
PrivateKey string `long:"private-key" description:"Faucet Private key"`
|
|
||||||
DBAddress string `long:"dbaddress" description:"Database address"`
|
|
||||||
DBUser string `long:"dbuser" description:"Database user" required:"true"`
|
|
||||||
DBPassword string `long:"dbpass" description:"Database password" required:"true"`
|
|
||||||
DBName string `long:"dbname" description:"Database name" required:"true"`
|
|
||||||
Migrate bool `long:"migrate" description:"Migrate the database to the latest version. The server will not start when using this flag."`
|
|
||||||
FeeRate float64 `long:"fee-rate" description:"Coins per gram fee rate"`
|
|
||||||
TestNet bool `long:"testnet" description:"Connect to testnet"`
|
|
||||||
SimNet bool `long:"simnet" description:"Connect to the simulation test network"`
|
|
||||||
DevNet bool `long:"devnet" description:"Connect to the development test network"`
|
|
||||||
}
|
|
||||||
|
|
||||||
var cfg *Config
|
|
||||||
|
|
||||||
// Parse parses the CLI arguments and returns a config struct.
|
|
||||||
func Parse() error {
|
|
||||||
cfg = &Config{
|
|
||||||
LogDir: defaultLogDir,
|
|
||||||
DBAddress: defaultDBAddress,
|
|
||||||
HTTPListen: defaultHTTPListen,
|
|
||||||
}
|
|
||||||
parser := flags.NewParser(cfg, flags.PrintErrors|flags.HelpFlag)
|
|
||||||
_, err := parser.Parse()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if !cfg.Migrate {
|
|
||||||
if cfg.KasparovdURL == "" {
|
|
||||||
return errors.New("api-server-url argument is required when --migrate flag is not raised")
|
|
||||||
}
|
|
||||||
if cfg.PrivateKey == "" {
|
|
||||||
return errors.New("private-key argument is required when --migrate flag is not raised")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
err = resolveNetwork(cfg)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
logFile := filepath.Join(cfg.LogDir, defaultLogFilename)
|
|
||||||
errLogFile := filepath.Join(cfg.LogDir, defaultErrLogFilename)
|
|
||||||
logger.InitLog(logFile, errLogFile)
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func resolveNetwork(cfg *Config) error {
|
|
||||||
// Multiple networks can't be selected simultaneously.
|
|
||||||
numNets := 0
|
|
||||||
if cfg.TestNet {
|
|
||||||
numNets++
|
|
||||||
}
|
|
||||||
if cfg.SimNet {
|
|
||||||
numNets++
|
|
||||||
}
|
|
||||||
if cfg.DevNet {
|
|
||||||
numNets++
|
|
||||||
}
|
|
||||||
if numNets > 1 {
|
|
||||||
return errors.New("multiple net params (testnet, simnet, devnet, etc.) can't be used " +
|
|
||||||
"together -- choose one of them")
|
|
||||||
}
|
|
||||||
|
|
||||||
activeNetParams = &dagconfig.MainNetParams
|
|
||||||
switch {
|
|
||||||
case cfg.TestNet:
|
|
||||||
activeNetParams = &dagconfig.TestNetParams
|
|
||||||
case cfg.SimNet:
|
|
||||||
activeNetParams = &dagconfig.SimNetParams
|
|
||||||
case cfg.DevNet:
|
|
||||||
activeNetParams = &dagconfig.DevNetParams
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// MainConfig is a getter to the main config
|
|
||||||
func MainConfig() (*Config, error) {
|
|
||||||
if cfg == nil {
|
|
||||||
return nil, errors.New("No configuration was set for the faucet")
|
|
||||||
}
|
|
||||||
return cfg, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ActiveNetParams returns the currently active net params
|
|
||||||
func ActiveNetParams() *dagconfig.Params {
|
|
||||||
return activeNetParams
|
|
||||||
}
|
|
@ -1,151 +0,0 @@
|
|||||||
package database
|
|
||||||
|
|
||||||
import (
|
|
||||||
nativeerrors "errors"
|
|
||||||
"fmt"
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
"os"
|
|
||||||
|
|
||||||
"github.com/golang-migrate/migrate/v4/source"
|
|
||||||
"github.com/jinzhu/gorm"
|
|
||||||
"github.com/kaspanet/kaspad/faucet/config"
|
|
||||||
|
|
||||||
"github.com/golang-migrate/migrate/v4"
|
|
||||||
)
|
|
||||||
|
|
||||||
// db is the API server database.
|
|
||||||
var db *gorm.DB
|
|
||||||
|
|
||||||
// DB returns a reference to the database connection
|
|
||||||
func DB() (*gorm.DB, error) {
|
|
||||||
if db == nil {
|
|
||||||
return nil, errors.New("Database is not connected")
|
|
||||||
}
|
|
||||||
return db, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type gormLogger struct{}
|
|
||||||
|
|
||||||
func (l gormLogger) Print(v ...interface{}) {
|
|
||||||
str := fmt.Sprint(v...)
|
|
||||||
log.Errorf(str)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Connect connects to the database mentioned in
|
|
||||||
// config variable.
|
|
||||||
func Connect() error {
|
|
||||||
connectionString, err := buildConnectionString()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
migrator, driver, err := openMigrator(connectionString)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
isCurrent, version, err := isCurrent(migrator, driver)
|
|
||||||
if err != nil {
|
|
||||||
return errors.Errorf("Error checking whether the database is current: %s", err)
|
|
||||||
}
|
|
||||||
if !isCurrent {
|
|
||||||
return errors.Errorf("Database is not current (version %d). Please migrate"+
|
|
||||||
" the database by running the faucet with --migrate flag and then run it again.", version)
|
|
||||||
}
|
|
||||||
|
|
||||||
db, err = gorm.Open("mysql", connectionString)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
db.SetLogger(gormLogger{})
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close closes the connection to the database
|
|
||||||
func Close() error {
|
|
||||||
if db == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
err := db.Close()
|
|
||||||
db = nil
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func buildConnectionString() (string, error) {
|
|
||||||
cfg, err := config.MainConfig()
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
return fmt.Sprintf("%s:%s@tcp(%s)/%s?charset=utf8&parseTime=True",
|
|
||||||
cfg.DBUser, cfg.DBPassword, cfg.DBAddress, cfg.DBName), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// isCurrent resolves whether the database is on the latest
|
|
||||||
// version of the schema.
|
|
||||||
func isCurrent(migrator *migrate.Migrate, driver source.Driver) (bool, uint, error) {
|
|
||||||
// Get the current version
|
|
||||||
version, isDirty, err := migrator.Version()
|
|
||||||
if nativeerrors.Is(err, migrate.ErrNilVersion) {
|
|
||||||
return false, 0, nil
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return false, 0, err
|
|
||||||
}
|
|
||||||
if isDirty {
|
|
||||||
return false, 0, errors.Errorf("Database is dirty")
|
|
||||||
}
|
|
||||||
|
|
||||||
// The database is current if Next returns ErrNotExist
|
|
||||||
_, err = driver.Next(version)
|
|
||||||
if pathErr, ok := err.(*os.PathError); ok {
|
|
||||||
if pathErr.Err == os.ErrNotExist {
|
|
||||||
return true, version, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false, version, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func openMigrator(connectionString string) (*migrate.Migrate, source.Driver, error) {
|
|
||||||
driver, err := source.Open("file://migrations")
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
migrator, err := migrate.NewWithSourceInstance(
|
|
||||||
"migrations", driver, "mysql://"+connectionString)
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
return migrator, driver, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Migrate database to the latest version.
|
|
||||||
func Migrate() error {
|
|
||||||
connectionString, err := buildConnectionString()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
migrator, driver, err := openMigrator(connectionString)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
isCurrent, version, err := isCurrent(migrator, driver)
|
|
||||||
if err != nil {
|
|
||||||
return errors.Errorf("Error checking whether the database is current: %s", err)
|
|
||||||
}
|
|
||||||
if isCurrent {
|
|
||||||
log.Infof("Database is already up-to-date (version %d)", version)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
err = migrator.Up()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
version, isDirty, err := migrator.Version()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if isDirty {
|
|
||||||
return errors.Errorf("error migrating database: database is dirty")
|
|
||||||
}
|
|
||||||
log.Infof("Migrated database to the latest version (version %d)", version)
|
|
||||||
return nil
|
|
||||||
}
|
|
@ -1,9 +0,0 @@
|
|||||||
package database
|
|
||||||
|
|
||||||
import "github.com/kaspanet/kaspad/util/panics"
|
|
||||||
import "github.com/kaspanet/kaspad/kasparov/logger"
|
|
||||||
|
|
||||||
var (
|
|
||||||
log = logger.BackendLog.Logger("DTBS")
|
|
||||||
spawn = panics.GoroutineWrapperFunc(log)
|
|
||||||
)
|
|
@ -1,28 +0,0 @@
|
|||||||
# -- multistage docker build: stage #1: build stage
|
|
||||||
FROM golang:1.13-alpine AS build
|
|
||||||
|
|
||||||
RUN mkdir -p /go/src/github.com/kaspanet/kaspad
|
|
||||||
|
|
||||||
WORKDIR /go/src/github.com/kaspanet/kaspad
|
|
||||||
|
|
||||||
RUN apk add --no-cache curl git
|
|
||||||
|
|
||||||
COPY go.mod .
|
|
||||||
COPY go.sum .
|
|
||||||
|
|
||||||
RUN go mod download
|
|
||||||
|
|
||||||
COPY . .
|
|
||||||
|
|
||||||
RUN cd faucet && CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o faucet .
|
|
||||||
|
|
||||||
# --- multistage docker build: stage #2: runtime image
|
|
||||||
FROM alpine
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
RUN apk add --no-cache tini
|
|
||||||
|
|
||||||
COPY --from=build /go/src/github.com/kaspanet/kaspad/faucet /app/
|
|
||||||
|
|
||||||
ENTRYPOINT ["/sbin/tini", "--"]
|
|
||||||
CMD ["/app/faucet"]
|
|
332
faucet/faucet.go
332
faucet/faucet.go
@ -1,332 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"encoding/hex"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"io/ioutil"
|
|
||||||
"net/http"
|
|
||||||
"net/url"
|
|
||||||
"path"
|
|
||||||
|
|
||||||
"github.com/kaspanet/kaspad/blockdag"
|
|
||||||
"github.com/kaspanet/kaspad/faucet/config"
|
|
||||||
"github.com/kaspanet/kaspad/httpserverutils"
|
|
||||||
"github.com/kaspanet/kaspad/kasparov/kasparovd/apimodels"
|
|
||||||
"github.com/kaspanet/kaspad/txscript"
|
|
||||||
"github.com/kaspanet/kaspad/util"
|
|
||||||
"github.com/kaspanet/kaspad/util/daghash"
|
|
||||||
"github.com/kaspanet/kaspad/wire"
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
sendAmount = 10000
|
|
||||||
// Value 8 bytes + serialized varint size for the length of ScriptPubKey +
|
|
||||||
// ScriptPubKey bytes.
|
|
||||||
outputSize uint64 = 8 + 1 + 25
|
|
||||||
minTxFee uint64 = 3000
|
|
||||||
|
|
||||||
requiredConfirmations = 10
|
|
||||||
)
|
|
||||||
|
|
||||||
type utxoSet map[wire.Outpoint]*blockdag.UTXOEntry
|
|
||||||
|
|
||||||
// apiURL returns a full concatenated URL from the base
|
|
||||||
// API server URL and the given path.
|
|
||||||
func apiURL(requestPath string) (string, error) {
|
|
||||||
cfg, err := config.MainConfig()
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
u, err := url.Parse(cfg.KasparovdURL)
|
|
||||||
if err != nil {
|
|
||||||
return "", errors.WithStack(err)
|
|
||||||
}
|
|
||||||
u.Path = path.Join(u.Path, requestPath)
|
|
||||||
return u.String(), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// getFromAPIServer makes an HTTP GET request to the API server
|
|
||||||
// to the given request path, and returns the response body.
|
|
||||||
func getFromAPIServer(requestPath string) ([]byte, error) {
|
|
||||||
getAPIURL, err := apiURL(requestPath)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
resp, err := http.Get(getAPIURL)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.WithStack(err)
|
|
||||||
}
|
|
||||||
defer func() {
|
|
||||||
err := resp.Body.Close()
|
|
||||||
if err != nil {
|
|
||||||
panic(errors.WithStack(err))
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
body, err := ioutil.ReadAll(resp.Body)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.WithStack(err)
|
|
||||||
}
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
|
||||||
clientError := &httpserverutils.ClientError{}
|
|
||||||
err := json.Unmarshal(body, &clientError)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.WithStack(err)
|
|
||||||
}
|
|
||||||
return nil, errors.WithStack(clientError)
|
|
||||||
}
|
|
||||||
return body, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// getFromAPIServer makes an HTTP POST request to the API server
|
|
||||||
// to the given request path. It converts the given data to JSON,
|
|
||||||
// and post it as the POST data.
|
|
||||||
func postToAPIServer(requestPath string, data interface{}) error {
|
|
||||||
dataBytes, err := json.Marshal(data)
|
|
||||||
if err != nil {
|
|
||||||
return errors.WithStack(err)
|
|
||||||
}
|
|
||||||
r := bytes.NewReader(dataBytes)
|
|
||||||
postAPIURL, err := apiURL(requestPath)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
resp, err := http.Post(postAPIURL, "application/json", r)
|
|
||||||
if err != nil {
|
|
||||||
return errors.WithStack(err)
|
|
||||||
}
|
|
||||||
defer func() {
|
|
||||||
err := resp.Body.Close()
|
|
||||||
if err != nil {
|
|
||||||
panic(errors.WithStack(err))
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
|
||||||
body, err := ioutil.ReadAll(resp.Body)
|
|
||||||
if err != nil {
|
|
||||||
return errors.WithStack(err)
|
|
||||||
}
|
|
||||||
clientError := &httpserverutils.ClientError{}
|
|
||||||
err = json.Unmarshal(body, &clientError)
|
|
||||||
if err != nil {
|
|
||||||
return errors.WithStack(err)
|
|
||||||
}
|
|
||||||
return errors.WithStack(clientError)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func isUTXOMatured(entry *blockdag.UTXOEntry, confirmations uint64) bool {
|
|
||||||
if entry.IsCoinbase() {
|
|
||||||
return confirmations >= config.ActiveNetParams().BlockCoinbaseMaturity
|
|
||||||
}
|
|
||||||
return confirmations >= requiredConfirmations
|
|
||||||
}
|
|
||||||
|
|
||||||
func getWalletUTXOSet() (utxoSet, error) {
|
|
||||||
body, err := getFromAPIServer(fmt.Sprintf("utxos/address/%s", faucetAddress.EncodeAddress()))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
utxoResponses := []*apimodels.TransactionOutputResponse{}
|
|
||||||
err = json.Unmarshal(body, &utxoResponses)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
walletUTXOSet := make(utxoSet)
|
|
||||||
for _, utxoResponse := range utxoResponses {
|
|
||||||
scriptPubKey, err := hex.DecodeString(utxoResponse.ScriptPubKey)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
txOut := &wire.TxOut{
|
|
||||||
Value: utxoResponse.Value,
|
|
||||||
ScriptPubKey: scriptPubKey,
|
|
||||||
}
|
|
||||||
txID, err := daghash.NewTxIDFromStr(utxoResponse.TransactionID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
outpoint := wire.NewOutpoint(txID, utxoResponse.Index)
|
|
||||||
utxoEntry := blockdag.NewUTXOEntry(txOut, *utxoResponse.IsCoinbase, utxoResponse.AcceptingBlockBlueScore)
|
|
||||||
if !isUTXOMatured(utxoEntry, *utxoResponse.Confirmations) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
walletUTXOSet[*outpoint] = utxoEntry
|
|
||||||
}
|
|
||||||
return walletUTXOSet, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func sendToAddress(address util.Address) (*wire.MsgTx, error) {
|
|
||||||
tx, err := createTx(address)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
buf := bytes.NewBuffer(make([]byte, 0, tx.SerializeSize()))
|
|
||||||
if err := tx.Serialize(buf); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
rawTx := &apimodels.RawTransaction{RawTransaction: hex.EncodeToString(buf.Bytes())}
|
|
||||||
return tx, postToAPIServer("transaction", rawTx)
|
|
||||||
}
|
|
||||||
|
|
||||||
func createTx(address util.Address) (*wire.MsgTx, error) {
|
|
||||||
walletUTXOSet, err := getWalletUTXOSet()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
tx, err := createUnsignedTx(walletUTXOSet, address)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
err = signTx(walletUTXOSet, tx)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return tx, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func createUnsignedTx(walletUTXOSet utxoSet, address util.Address) (*wire.MsgTx, error) {
|
|
||||||
tx := wire.NewNativeMsgTx(wire.TxVersion, nil, nil)
|
|
||||||
netAmount, isChangeOutputRequired, err := fundTx(walletUTXOSet, tx, sendAmount)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if isChangeOutputRequired {
|
|
||||||
tx.AddTxOut(&wire.TxOut{
|
|
||||||
Value: sendAmount,
|
|
||||||
ScriptPubKey: address.ScriptAddress(),
|
|
||||||
})
|
|
||||||
tx.AddTxOut(&wire.TxOut{
|
|
||||||
Value: netAmount - sendAmount,
|
|
||||||
ScriptPubKey: faucetScriptPubKey,
|
|
||||||
})
|
|
||||||
return tx, nil
|
|
||||||
}
|
|
||||||
tx.AddTxOut(&wire.TxOut{
|
|
||||||
Value: netAmount,
|
|
||||||
ScriptPubKey: address.ScriptAddress(),
|
|
||||||
})
|
|
||||||
return tx, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// signTx signs a transaction
|
|
||||||
func signTx(walletUTXOSet utxoSet, tx *wire.MsgTx) error {
|
|
||||||
for i, txIn := range tx.TxIn {
|
|
||||||
outpoint := txIn.PreviousOutpoint
|
|
||||||
|
|
||||||
sigScript, err := txscript.SignatureScript(tx, i, walletUTXOSet[outpoint].ScriptPubKey(),
|
|
||||||
txscript.SigHashAll, faucetPrivateKey, true)
|
|
||||||
if err != nil {
|
|
||||||
return errors.Errorf("Failed to sign transaction: %s", err)
|
|
||||||
}
|
|
||||||
txIn.SignatureScript = sigScript
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func fundTx(walletUTXOSet utxoSet, tx *wire.MsgTx, amount uint64) (netAmount uint64, isChangeOutputRequired bool, err error) {
|
|
||||||
amountSelected := uint64(0)
|
|
||||||
isTxFunded := false
|
|
||||||
for outpoint, entry := range walletUTXOSet {
|
|
||||||
amountSelected += entry.Amount()
|
|
||||||
|
|
||||||
// Add the selected output to the transaction
|
|
||||||
tx.AddTxIn(wire.NewTxIn(&outpoint, nil))
|
|
||||||
|
|
||||||
// Check if transaction has enough funds. If we don't have enough
|
|
||||||
// coins from the current amount selected to pay the fee continue
|
|
||||||
// to grab more coins.
|
|
||||||
isTxFunded, isChangeOutputRequired, netAmount, err = isFundedAndIsChangeOutputRequired(tx, amountSelected, amount, walletUTXOSet)
|
|
||||||
if err != nil {
|
|
||||||
return 0, false, err
|
|
||||||
}
|
|
||||||
if isTxFunded {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !isTxFunded {
|
|
||||||
return 0, false, errors.Errorf("not enough funds for coin selection")
|
|
||||||
}
|
|
||||||
|
|
||||||
return netAmount, isChangeOutputRequired, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// isFundedAndIsChangeOutputRequired returns three values and an error:
|
|
||||||
// * isTxFunded is whether the transaction inputs cover the target amount + the required fee.
|
|
||||||
// * isChangeOutputRequired is whether it is profitable to add an additional change
|
|
||||||
// output to the transaction.
|
|
||||||
// * netAmount is the amount of coins that will be eventually sent to the recipient. If no
|
|
||||||
// change output is needed, the netAmount will be usually a little bit higher than the
|
|
||||||
// targetAmount. Otherwise, it'll be the same as the targetAmount.
|
|
||||||
func isFundedAndIsChangeOutputRequired(tx *wire.MsgTx, amountSelected uint64, targetAmount uint64, walletUTXOSet utxoSet) (isTxFunded, isChangeOutputRequired bool, netAmount uint64, err error) {
|
|
||||||
// First check if it can be funded with one output and the required fee for it.
|
|
||||||
isFundedWithOneOutput, oneOutputFee, err := isFundedWithNumberOfOutputs(tx, 1, amountSelected, targetAmount, walletUTXOSet)
|
|
||||||
if err != nil {
|
|
||||||
return false, false, 0, err
|
|
||||||
}
|
|
||||||
if !isFundedWithOneOutput {
|
|
||||||
return false, false, 0, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Now check if it can be funded with two outputs and the required fee for it.
|
|
||||||
isFundedWithTwoOutputs, twoOutputsFee, err := isFundedWithNumberOfOutputs(tx, 2, amountSelected, targetAmount, walletUTXOSet)
|
|
||||||
if err != nil {
|
|
||||||
return false, false, 0, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// If it can be funded with two outputs, check if adding a change output worth it: i.e. check if
|
|
||||||
// the amount you save by not sending the recipient the whole inputs amount (minus fees) is greater
|
|
||||||
// than the additional fee that is required by adding a change output. If this is the case, return
|
|
||||||
// isChangeOutputRequired as true.
|
|
||||||
if isFundedWithTwoOutputs && twoOutputsFee-oneOutputFee < targetAmount-amountSelected {
|
|
||||||
return true, true, amountSelected - twoOutputsFee, nil
|
|
||||||
}
|
|
||||||
return true, false, amountSelected - oneOutputFee, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// isFundedWithNumberOfOutputs returns whether the transaction inputs cover
|
|
||||||
// the target amount + the required fee with the assumed number of outputs.
|
|
||||||
func isFundedWithNumberOfOutputs(tx *wire.MsgTx, numberOfOutputs uint64, amountSelected uint64, targetAmount uint64, walletUTXOSet utxoSet) (isTxFunded bool, fee uint64, err error) {
|
|
||||||
reqFee, err := calcFee(tx, numberOfOutputs, walletUTXOSet)
|
|
||||||
if err != nil {
|
|
||||||
return false, 0, err
|
|
||||||
}
|
|
||||||
return amountSelected > reqFee && amountSelected-reqFee >= targetAmount, reqFee, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func calcFee(msgTx *wire.MsgTx, numberOfOutputs uint64, walletUTXOSet utxoSet) (uint64, error) {
|
|
||||||
txMass := calcTxMass(msgTx, walletUTXOSet)
|
|
||||||
txMassWithOutputs := txMass + outputsTotalSize(numberOfOutputs)*blockdag.MassPerTxByte
|
|
||||||
cfg, err := config.MainConfig()
|
|
||||||
if err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
reqFee := uint64(float64(txMassWithOutputs) * cfg.FeeRate)
|
|
||||||
if reqFee < minTxFee {
|
|
||||||
return minTxFee, nil
|
|
||||||
}
|
|
||||||
return reqFee, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func outputsTotalSize(numberOfOutputs uint64) uint64 {
|
|
||||||
return numberOfOutputs*outputSize + uint64(wire.VarIntSerializeSize(numberOfOutputs))
|
|
||||||
}
|
|
||||||
|
|
||||||
func calcTxMass(msgTx *wire.MsgTx, walletUTXOSet utxoSet) uint64 {
|
|
||||||
previousScriptPubKeys := getPreviousScriptPubKeys(msgTx, walletUTXOSet)
|
|
||||||
return blockdag.CalcTxMass(util.NewTx(msgTx), previousScriptPubKeys)
|
|
||||||
}
|
|
||||||
|
|
||||||
func getPreviousScriptPubKeys(msgTx *wire.MsgTx, walletUTXOSet utxoSet) [][]byte {
|
|
||||||
previousScriptPubKeys := make([][]byte, len(msgTx.TxIn))
|
|
||||||
for i, txIn := range msgTx.TxIn {
|
|
||||||
outpoint := txIn.PreviousOutpoint
|
|
||||||
previousScriptPubKeys[i] = walletUTXOSet[outpoint].ScriptPubKey()
|
|
||||||
}
|
|
||||||
return previousScriptPubKeys
|
|
||||||
}
|
|
@ -1,66 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/kaspanet/kaspad/faucet/database"
|
|
||||||
"github.com/kaspanet/kaspad/httpserverutils"
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
"net"
|
|
||||||
"net/http"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
const minRequestInterval = time.Hour * 24
|
|
||||||
|
|
||||||
type ipUse struct {
|
|
||||||
IP string
|
|
||||||
LastUse time.Time
|
|
||||||
}
|
|
||||||
|
|
||||||
func ipFromRequest(r *http.Request) (string, error) {
|
|
||||||
ip, _, err := net.SplitHostPort(r.RemoteAddr)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
return ip, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func validateIPUsage(r *http.Request) error {
|
|
||||||
db, err := database.DB()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
now := time.Now()
|
|
||||||
timeBeforeMinRequestInterval := now.Add(-minRequestInterval)
|
|
||||||
var count int
|
|
||||||
ip, err := ipFromRequest(r)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
dbResult := db.Model(&ipUse{}).Where(&ipUse{IP: ip}).Where("last_use BETWEEN ? AND ?", timeBeforeMinRequestInterval, now).Count(&count)
|
|
||||||
dbErrors := dbResult.GetErrors()
|
|
||||||
if httpserverutils.HasDBError(dbErrors) {
|
|
||||||
return httpserverutils.NewErrorFromDBErrors("Some errors were encountered when checking the last use of an IP:", dbResult.GetErrors())
|
|
||||||
}
|
|
||||||
if count != 0 {
|
|
||||||
return httpserverutils.NewHandlerError(http.StatusForbidden, errors.New("A user is allowed to to have one request from the faucet every 24 hours"))
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func updateIPUsage(r *http.Request) error {
|
|
||||||
db, err := database.DB()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
ip, err := ipFromRequest(r)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
dbResult := db.Where(&ipUse{IP: ip}).Assign(&ipUse{LastUse: time.Now()}).FirstOrCreate(&ipUse{})
|
|
||||||
dbErrors := dbResult.GetErrors()
|
|
||||||
if httpserverutils.HasDBError(dbErrors) {
|
|
||||||
return httpserverutils.NewErrorFromDBErrors("Some errors were encountered when upserting the IP to the new date:", dbResult.GetErrors())
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
@ -1,11 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/kaspanet/kaspad/logger"
|
|
||||||
"github.com/kaspanet/kaspad/util/panics"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
log = logger.BackendLog.Logger("FAUC")
|
|
||||||
spawn = panics.GoroutineWrapperFunc(log)
|
|
||||||
)
|
|
@ -1,88 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"github.com/kaspanet/kaspad/dagconfig"
|
|
||||||
"github.com/kaspanet/kaspad/ecc"
|
|
||||||
"github.com/kaspanet/kaspad/faucet/config"
|
|
||||||
"github.com/kaspanet/kaspad/faucet/database"
|
|
||||||
"github.com/kaspanet/kaspad/txscript"
|
|
||||||
"github.com/kaspanet/kaspad/util"
|
|
||||||
"github.com/kaspanet/kaspad/util/base58"
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
"os"
|
|
||||||
|
|
||||||
_ "github.com/golang-migrate/migrate/v4/database/mysql"
|
|
||||||
_ "github.com/golang-migrate/migrate/v4/source/file"
|
|
||||||
_ "github.com/jinzhu/gorm/dialects/mysql"
|
|
||||||
"github.com/kaspanet/kaspad/signal"
|
|
||||||
"github.com/kaspanet/kaspad/util/panics"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
faucetAddress util.Address
|
|
||||||
faucetPrivateKey *ecc.PrivateKey
|
|
||||||
faucetScriptPubKey []byte
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
defer panics.HandlePanic(log, nil, nil)
|
|
||||||
|
|
||||||
err := config.Parse()
|
|
||||||
if err != nil {
|
|
||||||
err := errors.Wrap(err, "Error parsing command-line arguments")
|
|
||||||
_, err = fmt.Fprintf(os.Stderr, err.Error())
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
cfg, err := config.MainConfig()
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if cfg.Migrate {
|
|
||||||
err := database.Migrate()
|
|
||||||
if err != nil {
|
|
||||||
panic(errors.Errorf("Error migrating database: %s", err))
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
err = database.Connect()
|
|
||||||
if err != nil {
|
|
||||||
panic(errors.Errorf("Error connecting to database: %s", err))
|
|
||||||
}
|
|
||||||
defer func() {
|
|
||||||
err := database.Close()
|
|
||||||
if err != nil {
|
|
||||||
panic(errors.Errorf("Error closing the database: %s", err))
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
privateKeyBytes := base58.Decode(cfg.PrivateKey)
|
|
||||||
faucetPrivateKey, _ = ecc.PrivKeyFromBytes(ecc.S256(), privateKeyBytes)
|
|
||||||
|
|
||||||
faucetAddress, err = privateKeyToP2PKHAddress(faucetPrivateKey, config.ActiveNetParams())
|
|
||||||
if err != nil {
|
|
||||||
panic(errors.Errorf("Failed to get P2PKH address from private key: %s", err))
|
|
||||||
}
|
|
||||||
|
|
||||||
faucetScriptPubKey, err = txscript.PayToAddrScript(faucetAddress)
|
|
||||||
if err != nil {
|
|
||||||
panic(errors.Errorf("failed to generate faucetScriptPubKey to address: %s", err))
|
|
||||||
}
|
|
||||||
|
|
||||||
shutdownServer := startHTTPServer(cfg.HTTPListen)
|
|
||||||
defer shutdownServer()
|
|
||||||
|
|
||||||
interrupt := signal.InterruptListener()
|
|
||||||
<-interrupt
|
|
||||||
}
|
|
||||||
|
|
||||||
// privateKeyToP2PKHAddress generates p2pkh address from private key.
|
|
||||||
func privateKeyToP2PKHAddress(key *ecc.PrivateKey, net *dagconfig.Params) (util.Address, error) {
|
|
||||||
return util.NewAddressPubKeyHashFromPublicKey(key.PubKey().SerializeCompressed(), net.Prefix)
|
|
||||||
}
|
|
@ -1 +0,0 @@
|
|||||||
DROP TABLE `ip_uses`;
|
|
@ -1,6 +0,0 @@
|
|||||||
CREATE TABLE `ip_uses`
|
|
||||||
(
|
|
||||||
`ip` VARCHAR(39) NOT NULL,
|
|
||||||
`last_use` DATETIME NOT NULL,
|
|
||||||
PRIMARY KEY (`ip`)
|
|
||||||
);
|
|
@ -1,81 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"encoding/json"
|
|
||||||
"github.com/kaspanet/kaspad/faucet/config"
|
|
||||||
"github.com/kaspanet/kaspad/httpserverutils"
|
|
||||||
"github.com/kaspanet/kaspad/util"
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
"net/http"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/gorilla/handlers"
|
|
||||||
"github.com/gorilla/mux"
|
|
||||||
)
|
|
||||||
|
|
||||||
const gracefulShutdownTimeout = 30 * time.Second
|
|
||||||
|
|
||||||
// startHTTPServer starts the HTTP REST server and returns a
|
|
||||||
// function to gracefully shutdown it.
|
|
||||||
func startHTTPServer(listenAddr string) func() {
|
|
||||||
router := mux.NewRouter()
|
|
||||||
router.Use(httpserverutils.AddRequestMetadataMiddleware)
|
|
||||||
router.Use(httpserverutils.RecoveryMiddleware)
|
|
||||||
router.Use(httpserverutils.LoggingMiddleware)
|
|
||||||
router.Use(httpserverutils.SetJSONMiddleware)
|
|
||||||
router.HandleFunc(
|
|
||||||
"/request_money",
|
|
||||||
httpserverutils.MakeHandler(requestMoneyHandler)).
|
|
||||||
Methods("POST")
|
|
||||||
httpServer := &http.Server{
|
|
||||||
Addr: listenAddr,
|
|
||||||
Handler: handlers.CORS()(router),
|
|
||||||
}
|
|
||||||
spawn(func() {
|
|
||||||
log.Errorf("%s", httpServer.ListenAndServe())
|
|
||||||
})
|
|
||||||
|
|
||||||
return func() {
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), gracefulShutdownTimeout)
|
|
||||||
defer cancel()
|
|
||||||
err := httpServer.Shutdown(ctx)
|
|
||||||
if err != nil {
|
|
||||||
log.Errorf("Error shutting down HTTP server: %s", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type requestMoneyData struct {
|
|
||||||
Address string `json:"address"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func requestMoneyHandler(_ *httpserverutils.ServerContext, r *http.Request, _ map[string]string, _ map[string]string,
|
|
||||||
requestBody []byte) (interface{}, error) {
|
|
||||||
hErr := validateIPUsage(r)
|
|
||||||
if hErr != nil {
|
|
||||||
return nil, hErr
|
|
||||||
}
|
|
||||||
requestData := &requestMoneyData{}
|
|
||||||
err := json.Unmarshal(requestBody, requestData)
|
|
||||||
if err != nil {
|
|
||||||
return nil, httpserverutils.NewHandlerErrorWithCustomClientMessage(http.StatusUnprocessableEntity,
|
|
||||||
errors.Wrap(err, "Error unmarshalling request body"),
|
|
||||||
"The request body is not json-formatted")
|
|
||||||
}
|
|
||||||
address, err := util.DecodeAddress(requestData.Address, config.ActiveNetParams().Prefix)
|
|
||||||
if err != nil {
|
|
||||||
return nil, httpserverutils.NewHandlerErrorWithCustomClientMessage(http.StatusUnprocessableEntity,
|
|
||||||
errors.Wrap(err, "Error decoding address"),
|
|
||||||
"Error decoding address")
|
|
||||||
}
|
|
||||||
tx, err := sendToAddress(address)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
hErr = updateIPUsage(r)
|
|
||||||
if hErr != nil {
|
|
||||||
return nil, hErr
|
|
||||||
}
|
|
||||||
return tx.TxID().String(), nil
|
|
||||||
}
|
|
Loading…
x
Reference in New Issue
Block a user