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

* Copy over the CLI wallet from Kasparov. * Fix trivial compilation errors. * Reimplement the balance command. * Extract isUTXOSpendable to a separate function. * Reimplement the send command. * Fix bad transaction ID parsing. * Add a missing newline in a log. * Don't use msgTx in send(). * Fix isUTXOSpendable not checking whether a UTXO is of a coinbase transaction. * Add --devnet, --testnet, etc. to command line flags. * In `create`, only print the public key of the active network. * Use coinbase maturity in isUTXOSpendable. * Add a readme. * Fix formatting in readme.
202 lines
6.1 KiB
Go
202 lines
6.1 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/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.DeserializePrivateKeyFromSlice(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.TransactionVersion,
|
|
Inputs: inputs,
|
|
Outputs: outputs,
|
|
LockTime: 0,
|
|
SubnetworkID: subnetworks.SubnetworkIDNative,
|
|
Gas: 0,
|
|
Payload: nil,
|
|
PayloadHash: externalapi.DomainHash{},
|
|
}
|
|
|
|
for i, input := range domainTransaction.Inputs {
|
|
signatureScript, err := txscript.SignatureScript(domainTransaction, i, fromScript, txscript.SigHashAll, keyPair)
|
|
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
|
|
}
|