mirror of
https://github.com/kaspanet/kaspad.git
synced 2025-07-05 20:32: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