Ori Newman 1e9ddc42d0
Add fee estimation to wallet (#2291)
* Add fee estimation to wallet

* Add fee rate to kaspawallet parse

* Update go version

* Get rid of golint

* Add RBF support to wallet

* Fix bump_fee UTXO lookup and fix wrong change address

* impl storage mass as per KIP9

* Use CalculateTransactionOverallMass where needed

* Some fixes

* Minor typos

* Fix test

* update version

* BroadcastRBF -> BroadcastReplacement

* rc3

* align proto files to only use camel case (fixed on RK as well)

* Rename to FeePolicy and add MaxFee option + todo

* apply max fee constrains

* increase minChangeTarget to 10kas

* fmt

* Some fixes

* fix description: maximum -> minimum

* put min feerate check in the correct location

* Fix calculateFeeLimits nil handling

* Add validations to CLI flags

* Change to rc6

* Add checkTransactionFeeRate

* Add failed broadcast transactions on send error`

* Fix estimateFee change value

* Estimate fee correctly for --send-all

* On estimateFee always assume that the recipient has ECDSA address

* remove patch version

---------

Co-authored-by: Michael Sutton <msutton@cs.huji.ac.il>
2024-10-22 12:34:54 +03:00

264 lines
8.2 KiB
Go

package libkaspawallet
import (
"github.com/kaspanet/kaspad/cmd/kaspawallet/libkaspawallet/bip32"
"github.com/kaspanet/kaspad/cmd/kaspawallet/libkaspawallet/serialization"
"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/txscript"
"github.com/kaspanet/kaspad/domain/consensus/utils/utxo"
"github.com/kaspanet/kaspad/util"
"github.com/pkg/errors"
)
// Payment contains a recipient payment details
type Payment struct {
Address util.Address
Amount uint64
}
// UTXO is a type that stores a UTXO and meta data
// that is needed in order to sign it and create
// transactions with it.
type UTXO struct {
Outpoint *externalapi.DomainOutpoint
UTXOEntry externalapi.UTXOEntry
DerivationPath string
}
// CreateUnsignedTransaction creates an unsigned transaction
func CreateUnsignedTransaction(
extendedPublicKeys []string,
minimumSignatures uint32,
payments []*Payment,
selectedUTXOs []*UTXO) (*serialization.PartiallySignedTransaction, error) {
sortPublicKeys(extendedPublicKeys)
return createUnsignedTransaction(extendedPublicKeys, minimumSignatures, payments, selectedUTXOs)
}
func multiSigRedeemScript(extendedPublicKeys []string, minimumSignatures uint32, path string, ecdsa bool) ([]byte, error) {
scriptBuilder := txscript.NewScriptBuilder()
scriptBuilder.AddInt64(int64(minimumSignatures))
for _, key := range extendedPublicKeys {
extendedKey, err := bip32.DeserializeExtendedKey(key)
if err != nil {
return nil, err
}
derivedKey, err := extendedKey.DeriveFromPath(path)
if err != nil {
return nil, err
}
publicKey, err := derivedKey.PublicKey()
if err != nil {
return nil, err
}
var serializedPublicKey []byte
if ecdsa {
serializedECDSAPublicKey, err := publicKey.Serialize()
if err != nil {
return nil, err
}
serializedPublicKey = serializedECDSAPublicKey[:]
} else {
schnorrPublicKey, err := publicKey.ToSchnorr()
if err != nil {
return nil, err
}
serializedSchnorrPublicKey, err := schnorrPublicKey.Serialize()
if err != nil {
return nil, err
}
serializedPublicKey = serializedSchnorrPublicKey[:]
}
scriptBuilder.AddData(serializedPublicKey)
}
scriptBuilder.AddInt64(int64(len(extendedPublicKeys)))
if ecdsa {
scriptBuilder.AddOp(txscript.OpCheckMultiSigECDSA)
} else {
scriptBuilder.AddOp(txscript.OpCheckMultiSig)
}
return scriptBuilder.Script()
}
func createUnsignedTransaction(
extendedPublicKeys []string,
minimumSignatures uint32,
payments []*Payment,
selectedUTXOs []*UTXO) (*serialization.PartiallySignedTransaction, error) {
inputs := make([]*externalapi.DomainTransactionInput, len(selectedUTXOs))
partiallySignedInputs := make([]*serialization.PartiallySignedInput, len(selectedUTXOs))
for i, utxo := range selectedUTXOs {
emptyPubKeySignaturePairs := make([]*serialization.PubKeySignaturePair, len(extendedPublicKeys))
for i, extendedPublicKey := range extendedPublicKeys {
extendedKey, err := bip32.DeserializeExtendedKey(extendedPublicKey)
if err != nil {
return nil, err
}
derivedKey, err := extendedKey.DeriveFromPath(utxo.DerivationPath)
if err != nil {
return nil, err
}
emptyPubKeySignaturePairs[i] = &serialization.PubKeySignaturePair{
ExtendedPublicKey: derivedKey.String(),
}
}
inputs[i] = &externalapi.DomainTransactionInput{PreviousOutpoint: *utxo.Outpoint}
partiallySignedInputs[i] = &serialization.PartiallySignedInput{
PrevOutput: &externalapi.DomainTransactionOutput{
Value: utxo.UTXOEntry.Amount(),
ScriptPublicKey: utxo.UTXOEntry.ScriptPublicKey(),
},
MinimumSignatures: minimumSignatures,
PubKeySignaturePairs: emptyPubKeySignaturePairs,
DerivationPath: utxo.DerivationPath,
}
}
outputs := make([]*externalapi.DomainTransactionOutput, len(payments))
for i, payment := range payments {
scriptPublicKey, err := txscript.PayToAddrScript(payment.Address)
if err != nil {
return nil, err
}
outputs[i] = &externalapi.DomainTransactionOutput{
Value: payment.Amount,
ScriptPublicKey: scriptPublicKey,
}
}
domainTransaction := &externalapi.DomainTransaction{
Version: constants.MaxTransactionVersion,
Inputs: inputs,
Outputs: outputs,
LockTime: 0,
SubnetworkID: subnetworks.SubnetworkIDNative,
Gas: 0,
Payload: nil,
}
return &serialization.PartiallySignedTransaction{
Tx: domainTransaction,
PartiallySignedInputs: partiallySignedInputs,
}, nil
}
// IsTransactionFullySigned returns whether the transaction is fully signed and ready to broadcast.
func IsTransactionFullySigned(partiallySignedTransactionBytes []byte) (bool, error) {
partiallySignedTransaction, err := serialization.DeserializePartiallySignedTransaction(partiallySignedTransactionBytes)
if err != nil {
return false, err
}
return isTransactionFullySigned(partiallySignedTransaction), nil
}
func isTransactionFullySigned(partiallySignedTransaction *serialization.PartiallySignedTransaction) bool {
for _, input := range partiallySignedTransaction.PartiallySignedInputs {
numSignatures := 0
for _, pair := range input.PubKeySignaturePairs {
if pair.Signature != nil {
numSignatures++
}
}
if uint32(numSignatures) < input.MinimumSignatures {
return false
}
}
return true
}
// ExtractTransaction extracts a domain transaction from partially signed transaction after all of the
// relevant parties have signed it.
func ExtractTransaction(partiallySignedTransactionBytes []byte, ecdsa bool) (*externalapi.DomainTransaction, error) {
partiallySignedTransaction, err := serialization.DeserializePartiallySignedTransaction(partiallySignedTransactionBytes)
if err != nil {
return nil, err
}
return ExtractTransactionDeserialized(partiallySignedTransaction, ecdsa)
}
// ExtractTransactionDeserialized does the same thing ExtractTransaction does, only receives the PartiallySignedTransaction
// in an already deserialized format
func ExtractTransactionDeserialized(partiallySignedTransaction *serialization.PartiallySignedTransaction, ecdsa bool) (
*externalapi.DomainTransaction, error) {
for i, input := range partiallySignedTransaction.PartiallySignedInputs {
isMultisig := len(input.PubKeySignaturePairs) > 1
scriptBuilder := txscript.NewScriptBuilder()
if isMultisig {
signatureCount := 0
for _, pair := range input.PubKeySignaturePairs {
if pair.Signature != nil {
scriptBuilder.AddData(pair.Signature)
signatureCount++
}
}
if uint32(signatureCount) < input.MinimumSignatures {
return nil, errors.Errorf("missing %d signatures", input.MinimumSignatures-uint32(signatureCount))
}
redeemScript, err := partiallySignedInputMultisigRedeemScript(input, ecdsa)
if err != nil {
return nil, err
}
scriptBuilder.AddData(redeemScript)
sigScript, err := scriptBuilder.Script()
if err != nil {
return nil, err
}
partiallySignedTransaction.Tx.Inputs[i].SignatureScript = sigScript
} else {
if len(input.PubKeySignaturePairs) > 1 {
return nil, errors.Errorf("Cannot sign on P2PK when len(input.PubKeySignaturePairs) > 1")
}
if input.PubKeySignaturePairs[0].Signature == nil {
return nil, errors.Errorf("missing signature")
}
sigScript, err := txscript.NewScriptBuilder().
AddData(input.PubKeySignaturePairs[0].Signature).
Script()
if err != nil {
return nil, err
}
partiallySignedTransaction.Tx.Inputs[i].SignatureScript = sigScript
}
partiallySignedTransaction.Tx.Inputs[i].UTXOEntry = utxo.NewUTXOEntry(
input.PrevOutput.Value,
input.PrevOutput.ScriptPublicKey,
false, // This is a fake value
0, // This is a fake value
)
}
return partiallySignedTransaction.Tx, nil
}
func partiallySignedInputMultisigRedeemScript(input *serialization.PartiallySignedInput, ecdsa bool) ([]byte, error) {
extendedPublicKeys := make([]string, len(input.PubKeySignaturePairs))
for i, pair := range input.PubKeySignaturePairs {
extendedPublicKeys[i] = pair.ExtendedPublicKey
}
return multiSigRedeemScript(extendedPublicKeys, input.MinimumSignatures, "m", ecdsa)
}