mirror of
https://github.com/kaspanet/kaspad.git
synced 2025-03-30 15:08:33 +00:00

* [NOD-375] Move to pkg/errors * [NOD-375] Fix tests * [NOD-375] Make AreErrorsEqual a shared function
332 lines
10 KiB
Go
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
|
|
}
|