kaspad/faucet/faucet.go
Ori Newman c88fa1492e [NOD-375] Move to pkg/errors (#447)
* [NOD-375] Move to pkg/errors

* [NOD-375] Fix tests

* [NOD-375] Make AreErrorsEqual a shared function
2019-11-04 11:24:12 +02:00

332 lines
10 KiB
Go

package main
import (
"bytes"
"encoding/hex"
"encoding/json"
"fmt"
"github.com/daglabs/btcd/apiserver/apimodels"
"github.com/daglabs/btcd/blockdag"
"github.com/daglabs/btcd/faucet/config"
"github.com/daglabs/btcd/httpserverutils"
"github.com/daglabs/btcd/txscript"
"github.com/daglabs/btcd/util"
"github.com/daglabs/btcd/util/daghash"
"github.com/daglabs/btcd/wire"
"github.com/pkg/errors"
"io/ioutil"
"net/http"
"net/url"
"path"
)
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.APIServerURL)
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
}