kaspad/cmd/wallet/send.go
2021-03-30 18:01:56 +03:00

205 lines
6.2 KiB
Go

package main
import (
"encoding/hex"
"fmt"
"github.com/kaspanet/go-secp256k1"
"github.com/kaspanet/kaspad/app/appmessage"
"github.com/kaspanet/kaspad/domain/consensus/model/externalapi"
"github.com/kaspanet/kaspad/domain/consensus/utils/consensushashing"
"github.com/kaspanet/kaspad/domain/consensus/utils/constants"
"github.com/kaspanet/kaspad/domain/consensus/utils/subnetworks"
"github.com/kaspanet/kaspad/domain/consensus/utils/transactionid"
"github.com/kaspanet/kaspad/domain/consensus/utils/txscript"
"github.com/kaspanet/kaspad/infrastructure/network/rpcclient"
"github.com/kaspanet/kaspad/util"
"github.com/pkg/errors"
)
const feeSompis uint64 = 1000
func send(conf *sendConfig) error {
toAddress, err := util.DecodeAddress(conf.ToAddress, conf.ActiveNetParams.Prefix)
if err != nil {
return err
}
keyPair, publicKey, err := parsePrivateKey(conf.PrivateKey)
if err != nil {
return err
}
serializedPublicKey, err := publicKey.Serialize()
if err != nil {
return err
}
fromAddress, err := util.NewAddressPubKeyHashFromPublicKey(serializedPublicKey[:], conf.ActiveNetParams.Prefix)
if err != nil {
return err
}
client, err := rpcclient.NewRPCClient(conf.RPCServer)
if err != nil {
return err
}
utxos, err := fetchSpendableUTXOs(conf, client, fromAddress.String())
if err != nil {
return err
}
sendAmountSompi := uint64(conf.SendAmount * util.SompiPerKaspa)
totalToSend := sendAmountSompi + feeSompis
selectedUTXOs, changeSompi, err := selectUTXOs(utxos, totalToSend)
if err != nil {
return err
}
rpcTransaction, err := generateTransaction(keyPair, selectedUTXOs, sendAmountSompi, changeSompi, toAddress, fromAddress)
if err != nil {
return err
}
transactionID, err := sendTransaction(client, rpcTransaction)
if err != nil {
return err
}
fmt.Println("Transaction was sent successfully")
fmt.Printf("Transaction ID: \t%s\n", transactionID)
return nil
}
func parsePrivateKey(privateKeyHex string) (*secp256k1.SchnorrKeyPair, *secp256k1.SchnorrPublicKey, error) {
privateKeyBytes, err := hex.DecodeString(privateKeyHex)
if err != nil {
return nil, nil, errors.Wrap(err, "Error parsing private key hex")
}
keyPair, err := secp256k1.DeserializeSchnorrPrivateKeyFromSlice(privateKeyBytes)
if err != nil {
return nil, nil, errors.Wrap(err, "Error deserializing private key")
}
publicKey, err := keyPair.SchnorrPublicKey()
if err != nil {
return nil, nil, errors.Wrap(err, "Error generating public key")
}
return keyPair, publicKey, nil
}
func fetchSpendableUTXOs(conf *sendConfig, client *rpcclient.RPCClient, address string) ([]*appmessage.UTXOsByAddressesEntry, error) {
getUTXOsByAddressesResponse, err := client.GetUTXOsByAddresses([]string{address})
if err != nil {
return nil, err
}
virtualSelectedParentBlueScoreResponse, err := client.GetVirtualSelectedParentBlueScore()
if err != nil {
return nil, err
}
virtualSelectedParentBlueScore := virtualSelectedParentBlueScoreResponse.BlueScore
spendableUTXOs := make([]*appmessage.UTXOsByAddressesEntry, 0)
for _, entry := range getUTXOsByAddressesResponse.Entries {
if !isUTXOSpendable(entry, virtualSelectedParentBlueScore, conf.ActiveNetParams.BlockCoinbaseMaturity) {
continue
}
spendableUTXOs = append(spendableUTXOs, entry)
}
return spendableUTXOs, nil
}
func selectUTXOs(utxos []*appmessage.UTXOsByAddressesEntry, totalToSpend uint64) (
selectedUTXOs []*appmessage.UTXOsByAddressesEntry, changeSompi uint64, err error) {
selectedUTXOs = []*appmessage.UTXOsByAddressesEntry{}
totalValue := uint64(0)
for _, utxo := range utxos {
selectedUTXOs = append(selectedUTXOs, utxo)
totalValue += utxo.UTXOEntry.Amount
if totalValue >= totalToSpend {
break
}
}
if totalValue < totalToSpend {
return nil, 0, errors.Errorf("Insufficient funds for send: %f required, while only %f available",
float64(totalToSpend)/util.SompiPerKaspa, float64(totalValue)/util.SompiPerKaspa)
}
return selectedUTXOs, totalValue - totalToSpend, nil
}
func generateTransaction(keyPair *secp256k1.SchnorrKeyPair, selectedUTXOs []*appmessage.UTXOsByAddressesEntry,
sompisToSend uint64, change uint64, toAddress util.Address,
fromAddress util.Address) (*appmessage.RPCTransaction, error) {
inputs := make([]*externalapi.DomainTransactionInput, len(selectedUTXOs))
for i, utxo := range selectedUTXOs {
outpointTransactionIDBytes, err := hex.DecodeString(utxo.Outpoint.TransactionID)
if err != nil {
return nil, err
}
outpointTransactionID, err := transactionid.FromBytes(outpointTransactionIDBytes)
if err != nil {
return nil, err
}
outpoint := externalapi.DomainOutpoint{
TransactionID: *outpointTransactionID,
Index: utxo.Outpoint.Index,
}
inputs[i] = &externalapi.DomainTransactionInput{PreviousOutpoint: outpoint}
}
toScript, err := txscript.PayToAddrScript(toAddress)
if err != nil {
return nil, err
}
mainOutput := &externalapi.DomainTransactionOutput{
Value: sompisToSend,
ScriptPublicKey: toScript,
}
fromScript, err := txscript.PayToAddrScript(fromAddress)
if err != nil {
return nil, err
}
changeOutput := &externalapi.DomainTransactionOutput{
Value: change,
ScriptPublicKey: fromScript,
}
outputs := []*externalapi.DomainTransactionOutput{mainOutput, changeOutput}
domainTransaction := &externalapi.DomainTransaction{
Version: constants.MaxTransactionVersion,
Inputs: inputs,
Outputs: outputs,
LockTime: 0,
SubnetworkID: subnetworks.SubnetworkIDNative,
Gas: 0,
Payload: nil,
}
sighashReusedValues := &consensushashing.SighashReusedValues{}
for i, input := range domainTransaction.Inputs {
signatureScript, err := txscript.SignatureScript(
domainTransaction, i, consensushashing.SigHashAll, keyPair, sighashReusedValues)
if err != nil {
return nil, err
}
input.SignatureScript = signatureScript
}
rpcTransaction := appmessage.DomainTransactionToRPCTransaction(domainTransaction)
return rpcTransaction, nil
}
func sendTransaction(client *rpcclient.RPCClient, rpcTransaction *appmessage.RPCTransaction) (string, error) {
submitTransactionResponse, err := client.SubmitTransaction(rpcTransaction)
if err != nil {
return "", errors.Wrapf(err, "error submitting transaction")
}
return submitTransactionResponse.TransactionID, nil
}