mirror of
https://github.com/kaspanet/kaspad.git
synced 2025-03-30 15:08:33 +00:00
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>
This commit is contained in:
parent
48a142e12f
commit
1e9ddc42d0
12
.github/workflows/deploy.yaml
vendored
12
.github/workflows/deploy.yaml
vendored
@ -1,7 +1,7 @@
|
||||
name: Build and upload assets
|
||||
on:
|
||||
release:
|
||||
types: [ published ]
|
||||
types: [published]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
@ -9,7 +9,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ ubuntu-latest, windows-latest, macos-latest ]
|
||||
os: [ubuntu-latest, windows-latest, macos-latest]
|
||||
name: Building, ${{ matrix.os }}
|
||||
steps:
|
||||
- name: Fix CRLF on Windows
|
||||
@ -19,7 +19,6 @@ jobs:
|
||||
- name: Check out code into the Go module directory
|
||||
uses: actions/checkout@v2
|
||||
|
||||
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v2
|
||||
with:
|
||||
@ -31,7 +30,7 @@ jobs:
|
||||
# `-tags netgo,osusergo` means use pure go replacements for "os/user" and "net"
|
||||
# `-s -w` strips the binary to produce smaller size binaries
|
||||
run: |
|
||||
go build -v -ldflags="-s -w -extldflags=-static" -tags netgo,osusergo -o ./bin/ . ./cmd/...
|
||||
go build -v -ldflags="-s -w -extldflags=-static" -tags netgo,osusergo -o ./bin/ ./cmd/...
|
||||
archive="bin/kaspad-${{ github.event.release.tag_name }}-linux.zip"
|
||||
asset_name="kaspad-${{ github.event.release.tag_name }}-linux.zip"
|
||||
zip -r "${archive}" ./bin/*
|
||||
@ -42,7 +41,7 @@ jobs:
|
||||
if: runner.os == 'Windows'
|
||||
shell: bash
|
||||
run: |
|
||||
go build -v -ldflags="-s -w" -o bin/ . ./cmd/...
|
||||
go build -v -ldflags="-s -w" -o bin/ ./cmd/...
|
||||
archive="bin/kaspad-${{ github.event.release.tag_name }}-win64.zip"
|
||||
asset_name="kaspad-${{ github.event.release.tag_name }}-win64.zip"
|
||||
powershell "Compress-Archive bin/* \"${archive}\""
|
||||
@ -52,14 +51,13 @@ jobs:
|
||||
- name: Build on MacOS
|
||||
if: runner.os == 'macOS'
|
||||
run: |
|
||||
go build -v -ldflags="-s -w" -o ./bin/ . ./cmd/...
|
||||
go build -v -ldflags="-s -w" -o ./bin/ ./cmd/...
|
||||
archive="bin/kaspad-${{ github.event.release.tag_name }}-osx.zip"
|
||||
asset_name="kaspad-${{ github.event.release.tag_name }}-osx.zip"
|
||||
zip -r "${archive}" ./bin/*
|
||||
echo "archive=${archive}" >> $GITHUB_ENV
|
||||
echo "asset_name=${asset_name}" >> $GITHUB_ENV
|
||||
|
||||
|
||||
- name: Upload release asset
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
|
4
.github/workflows/race.yaml
vendored
4
.github/workflows/race.yaml
vendored
@ -11,7 +11,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
branch: [ master, latest ]
|
||||
branch: [master, latest]
|
||||
name: Race detection on ${{ matrix.branch }}
|
||||
steps:
|
||||
- name: Check out code into the Go module directory
|
||||
@ -22,7 +22,7 @@ jobs:
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: 1.19
|
||||
go-version: 1.23
|
||||
|
||||
- name: Set scheduled branch name
|
||||
shell: bash
|
||||
|
14
.github/workflows/tests.yaml
vendored
14
.github/workflows/tests.yaml
vendored
@ -8,16 +8,14 @@ on:
|
||||
types: [opened, synchronize, edited]
|
||||
|
||||
jobs:
|
||||
|
||||
build:
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ ubuntu-latest, macos-latest ]
|
||||
os: [ubuntu-latest, macos-latest]
|
||||
name: Tests, ${{ matrix.os }}
|
||||
steps:
|
||||
|
||||
- name: Fix CRLF on Windows
|
||||
if: runner.os == 'Windows'
|
||||
run: git config --global core.autocrlf false
|
||||
@ -33,8 +31,7 @@ jobs:
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: 1.19
|
||||
|
||||
go-version: 1.23
|
||||
|
||||
# Source: https://github.com/actions/cache/blob/main/examples.md#go---modules
|
||||
- name: Go Cache
|
||||
@ -49,16 +46,14 @@ jobs:
|
||||
shell: bash
|
||||
run: ./build_and_test.sh -v
|
||||
|
||||
|
||||
stability-test-fast:
|
||||
runs-on: ubuntu-latest
|
||||
name: Fast stability tests, ${{ github.head_ref }}
|
||||
steps:
|
||||
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: 1.19
|
||||
go-version: 1.23
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
@ -75,7 +70,6 @@ jobs:
|
||||
working-directory: stability-tests
|
||||
run: ./install_and_test.sh
|
||||
|
||||
|
||||
coverage:
|
||||
runs-on: ubuntu-latest
|
||||
name: Produce code coverage
|
||||
@ -86,7 +80,7 @@ jobs:
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: 1.19
|
||||
go-version: 1.23
|
||||
|
||||
- name: Delete the stability tests from coverage
|
||||
run: rm -r stability-tests
|
||||
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -53,6 +53,7 @@ _testmain.go
|
||||
debug
|
||||
debug.test
|
||||
__debug_bin
|
||||
*__debug_*
|
||||
|
||||
# CI
|
||||
version.txt
|
||||
|
@ -17,7 +17,7 @@ Kaspa is an attempt at a proof-of-work cryptocurrency with instant confirmations
|
||||
|
||||
## Requirements
|
||||
|
||||
Go 1.18 or later.
|
||||
Go 1.23 or later.
|
||||
|
||||
## Installation
|
||||
|
||||
|
@ -163,6 +163,10 @@ const (
|
||||
CmdGetMempoolEntriesByAddressesResponseMessage
|
||||
CmdGetCoinSupplyRequestMessage
|
||||
CmdGetCoinSupplyResponseMessage
|
||||
CmdGetFeeEstimateRequestMessage
|
||||
CmdGetFeeEstimateResponseMessage
|
||||
CmdSubmitTransactionReplacementRequestMessage
|
||||
CmdSubmitTransactionReplacementResponseMessage
|
||||
)
|
||||
|
||||
// ProtocolMessageCommandToString maps all MessageCommands to their string representation
|
||||
@ -300,6 +304,10 @@ var RPCMessageCommandToString = map[MessageCommand]string{
|
||||
CmdGetMempoolEntriesByAddressesResponseMessage: "GetMempoolEntriesByAddressesResponse",
|
||||
CmdGetCoinSupplyRequestMessage: "GetCoinSupplyRequest",
|
||||
CmdGetCoinSupplyResponseMessage: "GetCoinSupplyResponse",
|
||||
CmdGetFeeEstimateRequestMessage: "GetFeeEstimateRequest",
|
||||
CmdGetFeeEstimateResponseMessage: "GetFeeEstimateResponse",
|
||||
CmdSubmitTransactionReplacementRequestMessage: "SubmitTransactionReplacementRequest",
|
||||
CmdSubmitTransactionReplacementResponseMessage: "SubmitTransactionReplacementResponse",
|
||||
}
|
||||
|
||||
// Message is an interface that describes a kaspa message. A type that
|
||||
|
47
app/appmessage/rpc_fee_estimate.go
Normal file
47
app/appmessage/rpc_fee_estimate.go
Normal file
@ -0,0 +1,47 @@
|
||||
package appmessage
|
||||
|
||||
// GetFeeEstimateRequestMessage is an appmessage corresponding to
|
||||
// its respective RPC message
|
||||
type GetFeeEstimateRequestMessage struct {
|
||||
baseMessage
|
||||
}
|
||||
|
||||
// Command returns the protocol command string for the message
|
||||
func (msg *GetFeeEstimateRequestMessage) Command() MessageCommand {
|
||||
return CmdGetFeeEstimateRequestMessage
|
||||
}
|
||||
|
||||
// NewGetFeeEstimateRequestMessage returns a instance of the message
|
||||
func NewGetFeeEstimateRequestMessage() *GetFeeEstimateRequestMessage {
|
||||
return &GetFeeEstimateRequestMessage{}
|
||||
}
|
||||
|
||||
type RPCFeeRateBucket struct {
|
||||
Feerate float64
|
||||
EstimatedSeconds float64
|
||||
}
|
||||
|
||||
type RPCFeeEstimate struct {
|
||||
PriorityBucket RPCFeeRateBucket
|
||||
NormalBuckets []RPCFeeRateBucket
|
||||
LowBuckets []RPCFeeRateBucket
|
||||
}
|
||||
|
||||
// GetCoinSupplyResponseMessage is an appmessage corresponding to
|
||||
// its respective RPC message
|
||||
type GetFeeEstimateResponseMessage struct {
|
||||
baseMessage
|
||||
Estimate RPCFeeEstimate
|
||||
|
||||
Error *RPCError
|
||||
}
|
||||
|
||||
// Command returns the protocol command string for the message
|
||||
func (msg *GetFeeEstimateResponseMessage) Command() MessageCommand {
|
||||
return CmdGetFeeEstimateResponseMessage
|
||||
}
|
||||
|
||||
// NewGetFeeEstimateResponseMessage returns a instance of the message
|
||||
func NewGetFeeEstimateResponseMessage() *GetFeeEstimateResponseMessage {
|
||||
return &GetFeeEstimateResponseMessage{}
|
||||
}
|
42
app/appmessage/rpc_submit_transaction_replacement.go
Normal file
42
app/appmessage/rpc_submit_transaction_replacement.go
Normal file
@ -0,0 +1,42 @@
|
||||
package appmessage
|
||||
|
||||
// SubmitTransactionReplacementRequestMessage is an appmessage corresponding to
|
||||
// its respective RPC message
|
||||
type SubmitTransactionReplacementRequestMessage struct {
|
||||
baseMessage
|
||||
Transaction *RPCTransaction
|
||||
}
|
||||
|
||||
// Command returns the protocol command string for the message
|
||||
func (msg *SubmitTransactionReplacementRequestMessage) Command() MessageCommand {
|
||||
return CmdSubmitTransactionReplacementRequestMessage
|
||||
}
|
||||
|
||||
// NewSubmitTransactionReplacementRequestMessage returns a instance of the message
|
||||
func NewSubmitTransactionReplacementRequestMessage(transaction *RPCTransaction) *SubmitTransactionReplacementRequestMessage {
|
||||
return &SubmitTransactionReplacementRequestMessage{
|
||||
Transaction: transaction,
|
||||
}
|
||||
}
|
||||
|
||||
// SubmitTransactionReplacementResponseMessage is an appmessage corresponding to
|
||||
// its respective RPC message
|
||||
type SubmitTransactionReplacementResponseMessage struct {
|
||||
baseMessage
|
||||
TransactionID string
|
||||
ReplacedTransaction *RPCTransaction
|
||||
|
||||
Error *RPCError
|
||||
}
|
||||
|
||||
// Command returns the protocol command string for the message
|
||||
func (msg *SubmitTransactionReplacementResponseMessage) Command() MessageCommand {
|
||||
return CmdSubmitTransactionReplacementResponseMessage
|
||||
}
|
||||
|
||||
// NewSubmitTransactionReplacementResponseMessage returns a instance of the message
|
||||
func NewSubmitTransactionReplacementResponseMessage(transactionID string) *SubmitTransactionReplacementResponseMessage {
|
||||
return &SubmitTransactionReplacementResponseMessage{
|
||||
TransactionID: transactionID,
|
||||
}
|
||||
}
|
@ -5,13 +5,10 @@ FLAGS=$@
|
||||
go version
|
||||
|
||||
go get $FLAGS -t -d ./...
|
||||
GO111MODULE=off go get $FLAGS golang.org/x/lint/golint
|
||||
go install $FLAGS honnef.co/go/tools/cmd/staticcheck@latest
|
||||
|
||||
test -z "$(go fmt ./...)"
|
||||
|
||||
golint -set_exit_status ./...
|
||||
|
||||
staticcheck -checks SA4006,SA4008,SA4009,SA4010,SA5003,SA1004,SA1014,SA1021,SA1023,SA1024,SA1025,SA1026,SA1027,SA1028,SA2000,SA2001,SA2003,SA4000,SA4001,SA4003,SA4004,SA4011,SA4012,SA4013,SA4014,SA4015,SA4016,SA4017,SA4018,SA4019,SA4020,SA4021,SA4022,SA4023,SA5000,SA5002,SA5004,SA5005,SA5007,SA5008,SA5009,SA5010,SA5011,SA5012,SA6001,SA6002,SA9001,SA9002,SA9003,SA9004,SA9005,SA9006,ST1019 ./...
|
||||
|
||||
go build $FLAGS -o kaspad .
|
||||
|
@ -4,7 +4,7 @@ kaspactl is an RPC client for kaspad
|
||||
|
||||
## Requirements
|
||||
|
||||
Go 1.19 or later.
|
||||
Go 1.23 or later.
|
||||
|
||||
## Installation
|
||||
|
||||
@ -50,4 +50,4 @@ For example:
|
||||
$ kaspactl '{"getBlockDagInfoRequest":{}}'
|
||||
```
|
||||
|
||||
For a list of all available requests check out the [RPC documentation](infrastructure/network/netadapter/server/grpcserver/protowire/rpc.md)
|
||||
For a list of all available requests check out the [RPC documentation](infrastructure/network/netadapter/server/grpcserver/protowire/rpc.md)
|
||||
|
@ -1,5 +1,5 @@
|
||||
# -- multistage docker build: stage #1: build stage
|
||||
FROM golang:1.19-alpine AS build
|
||||
FROM golang:1.23-alpine AS build
|
||||
|
||||
RUN mkdir -p /go/src/github.com/kaspanet/kaspad
|
||||
|
||||
|
@ -4,7 +4,7 @@ Kaspaminer is a CPU-based miner for kaspad
|
||||
|
||||
## Requirements
|
||||
|
||||
Go 1.19 or later.
|
||||
Go 1.23 or later.
|
||||
|
||||
## Installation
|
||||
|
||||
@ -30,7 +30,7 @@ $ go install .
|
||||
- Kapaminer should now be installed in `$(go env GOPATH)/bin`. If you did
|
||||
not already add the bin directory to your system path during Go installation,
|
||||
you are encouraged to do so now.
|
||||
|
||||
|
||||
## Usage
|
||||
|
||||
The full kaspaminer configuration options can be seen with:
|
||||
@ -40,6 +40,7 @@ $ kaspaminer --help
|
||||
```
|
||||
|
||||
But the minimum configuration needed to run it is:
|
||||
|
||||
```bash
|
||||
$ kaspaminer --miningaddr=<YOUR_MINING_ADDRESS>
|
||||
```
|
||||
```
|
||||
|
@ -1,5 +1,5 @@
|
||||
# -- multistage docker build: stage #1: build stage
|
||||
FROM golang:1.19-alpine AS build
|
||||
FROM golang:1.23-alpine AS build
|
||||
|
||||
RUN mkdir -p /go/src/github.com/kaspanet/kaspad
|
||||
|
||||
|
@ -8,6 +8,7 @@ import (
|
||||
|
||||
"github.com/kaspanet/kaspad/cmd/kaspawallet/daemon/client"
|
||||
"github.com/kaspanet/kaspad/cmd/kaspawallet/daemon/pb"
|
||||
"github.com/kaspanet/kaspad/cmd/kaspawallet/daemon/server"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
@ -37,7 +38,7 @@ func broadcast(conf *broadcastConfig) error {
|
||||
transactionsHex = strings.TrimSpace(string(transactionHexBytes))
|
||||
}
|
||||
|
||||
transactions, err := decodeTransactionsFromHex(transactionsHex)
|
||||
transactions, err := server.DecodeTransactionsFromHex(transactionsHex)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
57
cmd/kaspawallet/broadcast_replacement.go
Normal file
57
cmd/kaspawallet/broadcast_replacement.go
Normal file
@ -0,0 +1,57 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"strings"
|
||||
|
||||
"github.com/kaspanet/kaspad/cmd/kaspawallet/daemon/client"
|
||||
"github.com/kaspanet/kaspad/cmd/kaspawallet/daemon/pb"
|
||||
"github.com/kaspanet/kaspad/cmd/kaspawallet/daemon/server"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func broadcastReplacement(conf *broadcastConfig) error {
|
||||
daemonClient, tearDown, err := client.Connect(conf.DaemonAddress)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer tearDown()
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), daemonTimeout)
|
||||
defer cancel()
|
||||
|
||||
if conf.Transactions == "" && conf.TransactionsFile == "" {
|
||||
return errors.Errorf("Either --transaction or --transaction-file is required")
|
||||
}
|
||||
if conf.Transactions != "" && conf.TransactionsFile != "" {
|
||||
return errors.Errorf("Both --transaction and --transaction-file cannot be passed at the same time")
|
||||
}
|
||||
|
||||
transactionsHex := conf.Transactions
|
||||
if conf.TransactionsFile != "" {
|
||||
transactionHexBytes, err := ioutil.ReadFile(conf.TransactionsFile)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "Could not read hex from %s", conf.TransactionsFile)
|
||||
}
|
||||
transactionsHex = strings.TrimSpace(string(transactionHexBytes))
|
||||
}
|
||||
|
||||
transactions, err := server.DecodeTransactionsFromHex(transactionsHex)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
response, err := daemonClient.BroadcastReplacement(ctx, &pb.BroadcastRequest{Transactions: transactions})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Println("Transactions were sent successfully")
|
||||
fmt.Println("Transaction ID(s): ")
|
||||
for _, txID := range response.TxIDs {
|
||||
fmt.Printf("\t%s\n", txID)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
117
cmd/kaspawallet/bump_fee.go
Normal file
117
cmd/kaspawallet/bump_fee.go
Normal file
@ -0,0 +1,117 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/kaspanet/kaspad/cmd/kaspawallet/daemon/client"
|
||||
"github.com/kaspanet/kaspad/cmd/kaspawallet/daemon/pb"
|
||||
"github.com/kaspanet/kaspad/cmd/kaspawallet/keys"
|
||||
"github.com/kaspanet/kaspad/cmd/kaspawallet/libkaspawallet"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func bumpFee(conf *bumpFeeConfig) error {
|
||||
keysFile, err := keys.ReadKeysFile(conf.NetParams(), conf.KeysFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(keysFile.ExtendedPublicKeys) > len(keysFile.EncryptedMnemonics) {
|
||||
return errors.Errorf("Cannot use 'bump-fee' command for multisig wallet without all of the keys")
|
||||
}
|
||||
|
||||
daemonClient, tearDown, err := client.Connect(conf.DaemonAddress)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer tearDown()
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), daemonTimeout)
|
||||
defer cancel()
|
||||
|
||||
var feePolicy *pb.FeePolicy
|
||||
if conf.FeeRate > 0 {
|
||||
feePolicy = &pb.FeePolicy{
|
||||
FeePolicy: &pb.FeePolicy_ExactFeeRate{
|
||||
ExactFeeRate: conf.FeeRate,
|
||||
},
|
||||
}
|
||||
} else if conf.MaxFeeRate > 0 {
|
||||
feePolicy = &pb.FeePolicy{
|
||||
FeePolicy: &pb.FeePolicy_MaxFeeRate{MaxFeeRate: conf.MaxFeeRate},
|
||||
}
|
||||
} else if conf.MaxFee > 0 {
|
||||
feePolicy = &pb.FeePolicy{
|
||||
FeePolicy: &pb.FeePolicy_MaxFee{MaxFee: conf.MaxFee},
|
||||
}
|
||||
}
|
||||
|
||||
createUnsignedTransactionsResponse, err :=
|
||||
daemonClient.BumpFee(ctx, &pb.BumpFeeRequest{
|
||||
TxID: conf.TxID,
|
||||
From: conf.FromAddresses,
|
||||
UseExistingChangeAddress: conf.UseExistingChangeAddress,
|
||||
FeePolicy: feePolicy,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(conf.Password) == 0 {
|
||||
conf.Password = keys.GetPassword("Password:")
|
||||
}
|
||||
mnemonics, err := keysFile.DecryptMnemonics(conf.Password)
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "message authentication failed") {
|
||||
fmt.Fprintf(os.Stderr, "Password decryption failed. Sometimes this is a result of not "+
|
||||
"specifying the same keys file used by the wallet daemon process.\n")
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
signedTransactions := make([][]byte, len(createUnsignedTransactionsResponse.Transactions))
|
||||
for i, unsignedTransaction := range createUnsignedTransactionsResponse.Transactions {
|
||||
signedTransaction, err := libkaspawallet.Sign(conf.NetParams(), mnemonics, unsignedTransaction, keysFile.ECDSA)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
signedTransactions[i] = signedTransaction
|
||||
}
|
||||
|
||||
fmt.Printf("Broadcasting %d transaction(s)\n", len(signedTransactions))
|
||||
// Since we waited for user input when getting the password, which could take unbound amount of time -
|
||||
// create a new context for broadcast, to reset the timeout.
|
||||
broadcastCtx, broadcastCancel := context.WithTimeout(context.Background(), daemonTimeout)
|
||||
defer broadcastCancel()
|
||||
|
||||
const chunkSize = 100 // To avoid sending a message bigger than the gRPC max message size, we split it to chunks
|
||||
for offset := 0; offset < len(signedTransactions); offset += chunkSize {
|
||||
end := len(signedTransactions)
|
||||
if offset+chunkSize <= len(signedTransactions) {
|
||||
end = offset + chunkSize
|
||||
}
|
||||
|
||||
chunk := signedTransactions[offset:end]
|
||||
response, err := daemonClient.BroadcastReplacement(broadcastCtx, &pb.BroadcastRequest{Transactions: chunk})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Printf("Broadcasted %d transaction(s) (broadcasted %.2f%% of the transactions so far)\n", len(chunk), 100*float64(end)/float64(len(signedTransactions)))
|
||||
fmt.Println("Broadcasted Transaction ID(s): ")
|
||||
for _, txID := range response.TxIDs {
|
||||
fmt.Printf("\t%s\n", txID)
|
||||
}
|
||||
}
|
||||
|
||||
if conf.Verbose {
|
||||
fmt.Println("Serialized Transaction(s) (can be parsed via the `parse` command or resent via `broadcast`): ")
|
||||
for _, signedTx := range signedTransactions {
|
||||
fmt.Printf("\t%x\n\n", signedTx)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
58
cmd/kaspawallet/bump_fee_unsigned.go
Normal file
58
cmd/kaspawallet/bump_fee_unsigned.go
Normal file
@ -0,0 +1,58 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/kaspanet/kaspad/cmd/kaspawallet/daemon/client"
|
||||
"github.com/kaspanet/kaspad/cmd/kaspawallet/daemon/pb"
|
||||
"github.com/kaspanet/kaspad/cmd/kaspawallet/daemon/server"
|
||||
)
|
||||
|
||||
func bumpFeeUnsigned(conf *bumpFeeUnsignedConfig) error {
|
||||
daemonClient, tearDown, err := client.Connect(conf.DaemonAddress)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer tearDown()
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), daemonTimeout)
|
||||
defer cancel()
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var feePolicy *pb.FeePolicy
|
||||
if conf.FeeRate > 0 {
|
||||
feePolicy = &pb.FeePolicy{
|
||||
FeePolicy: &pb.FeePolicy_ExactFeeRate{
|
||||
ExactFeeRate: conf.FeeRate,
|
||||
},
|
||||
}
|
||||
} else if conf.MaxFeeRate > 0 {
|
||||
feePolicy = &pb.FeePolicy{
|
||||
FeePolicy: &pb.FeePolicy_MaxFeeRate{MaxFeeRate: conf.MaxFeeRate},
|
||||
}
|
||||
} else if conf.MaxFee > 0 {
|
||||
feePolicy = &pb.FeePolicy{
|
||||
FeePolicy: &pb.FeePolicy_MaxFee{MaxFee: conf.MaxFee},
|
||||
}
|
||||
}
|
||||
|
||||
response, err := daemonClient.BumpFee(ctx, &pb.BumpFeeRequest{
|
||||
TxID: conf.TxID,
|
||||
From: conf.FromAddresses,
|
||||
UseExistingChangeAddress: conf.UseExistingChangeAddress,
|
||||
FeePolicy: feePolicy,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Fprintln(os.Stderr, "Created unsigned transaction")
|
||||
fmt.Println(server.EncodeTransactionsToHex(response.Transactions))
|
||||
|
||||
return nil
|
||||
}
|
@ -1,9 +1,10 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/kaspanet/kaspad/infrastructure/config"
|
||||
"github.com/pkg/errors"
|
||||
"os"
|
||||
|
||||
"github.com/jessevdk/go-flags"
|
||||
)
|
||||
@ -23,6 +24,9 @@ const (
|
||||
startDaemonSubCmd = "start-daemon"
|
||||
versionSubCmd = "version"
|
||||
getDaemonVersionSubCmd = "get-daemon-version"
|
||||
bumpFeeSubCmd = "bump-fee"
|
||||
bumpFeeUnsignedSubCmd = "bump-fee-unsigned"
|
||||
broadcastReplacementSubCmd = "broadcast-replacement"
|
||||
)
|
||||
|
||||
const (
|
||||
@ -62,6 +66,9 @@ type sendConfig struct {
|
||||
SendAmount string `long:"send-amount" short:"v" description:"An amount to send in Kaspa (e.g. 1234.12345678)"`
|
||||
IsSendAll bool `long:"send-all" description:"Send all the Kaspa in the wallet (mutually exclusive with --send-amount). If --from-address was used, will send all only from the specified addresses."`
|
||||
UseExistingChangeAddress bool `long:"use-existing-change-address" short:"u" description:"Will use an existing change address (in case no change address was ever used, it will use a new one)"`
|
||||
MaxFeeRate float64 `long:"max-fee-rate" short:"m" description:"Maximum fee rate in Sompi/gram to use for the transaction. The wallet will take the minimum between the fee rate estimate from the connected node and this value."`
|
||||
FeeRate float64 `long:"fee-rate" short:"r" description:"Fee rate in Sompi/gram to use for the transaction. This option will override any fee estimate from the connected node."`
|
||||
MaxFee uint64 `long:"max-fee" short:"x" description:"Maximum fee in Sompi (not Sompi/gram) to use for the transaction. The wallet will take the minimum between the fee estimate from the connected node and this value. If no other fee policy is specified, it will set the max fee to 1 KAS"`
|
||||
Verbose bool `long:"show-serialized" short:"s" description:"Show a list of hex encoded sent transactions"`
|
||||
config.NetworkFlags
|
||||
}
|
||||
@ -79,6 +86,9 @@ type createUnsignedTransactionConfig struct {
|
||||
SendAmount string `long:"send-amount" short:"v" description:"An amount to send in Kaspa (e.g. 1234.12345678)"`
|
||||
IsSendAll bool `long:"send-all" description:"Send all the Kaspa in the wallet (mutually exclusive with --send-amount)"`
|
||||
UseExistingChangeAddress bool `long:"use-existing-change-address" short:"u" description:"Will use an existing change address (in case no change address was ever used, it will use a new one)"`
|
||||
MaxFeeRate float64 `long:"max-fee-rate" short:"m" description:"Maximum fee rate in Sompi/gram to use for the transaction. The wallet will take the minimum between the fee rate estimate from the connected node and this value."`
|
||||
FeeRate float64 `long:"fee-rate" short:"r" description:"Fee rate in Sompi/gram to use for the transaction. This option will override any fee estimate from the connected node."`
|
||||
MaxFee uint64 `long:"max-fee" short:"x" description:"Maximum fee in Sompi (not Sompi/gram) to use for the transaction. The wallet will take the minimum between the fee estimate from the connected node and this value. If no other fee policy is specified, it will set the max fee to 1 KAS"`
|
||||
config.NetworkFlags
|
||||
}
|
||||
|
||||
@ -98,6 +108,7 @@ type broadcastConfig struct {
|
||||
}
|
||||
|
||||
type parseConfig struct {
|
||||
KeysFile string `long:"keys-file" short:"f" description:"Keys file location (default: ~/.kaspawallet/keys.json (*nix), %USERPROFILE%\\AppData\\Local\\Kaspawallet\\key.json (Windows))"`
|
||||
Transaction string `long:"transaction" short:"t" description:"The transaction to parse (encoded in hex)"`
|
||||
TransactionFile string `long:"transaction-file" short:"F" description:"The file containing the transaction to parse (encoded in hex)"`
|
||||
Verbose bool `long:"verbose" short:"v" description:"Verbose: show transaction inputs"`
|
||||
@ -131,6 +142,31 @@ type dumpUnencryptedDataConfig struct {
|
||||
config.NetworkFlags
|
||||
}
|
||||
|
||||
type bumpFeeUnsignedConfig struct {
|
||||
TxID string `long:"txid" short:"i" description:"The transaction ID to bump the fee for"`
|
||||
DaemonAddress string `long:"daemonaddress" short:"d" description:"Wallet daemon server to connect to"`
|
||||
FromAddresses []string `long:"from-address" short:"a" description:"Specific public address to send Kaspa from. Use multiple times to accept several addresses" required:"false"`
|
||||
UseExistingChangeAddress bool `long:"use-existing-change-address" short:"u" description:"Will use an existing change address (in case no change address was ever used, it will use a new one)"`
|
||||
MaxFeeRate float64 `long:"max-fee-rate" short:"m" description:"Maximum fee rate in Sompi/gram to use for the transaction. The wallet will take the minimum between the fee rate estimate from the connected node and this value."`
|
||||
FeeRate float64 `long:"fee-rate" short:"r" description:"Fee rate in Sompi/gram to use for the transaction. This option will override any fee estimate from the connected node."`
|
||||
MaxFee uint64 `long:"max-fee" short:"x" description:"Maximum fee in Sompi (not Sompi/gram) to use for the transaction. The wallet will take the minimum between the fee estimate from the connected node and this value. If no other fee policy is specified, it will set the max fee to 1 KAS"`
|
||||
config.NetworkFlags
|
||||
}
|
||||
|
||||
type bumpFeeConfig struct {
|
||||
TxID string `long:"txid" short:"i" description:"The transaction ID to bump the fee for"`
|
||||
KeysFile string `long:"keys-file" short:"f" description:"Keys file location (default: ~/.kaspawallet/keys.json (*nix), %USERPROFILE%\\AppData\\Local\\Kaspawallet\\key.json (Windows))"`
|
||||
Password string `long:"password" short:"p" description:"Wallet password"`
|
||||
DaemonAddress string `long:"daemonaddress" short:"d" description:"Wallet daemon server to connect to"`
|
||||
FromAddresses []string `long:"from-address" short:"a" description:"Specific public address to send Kaspa from. Repeat multiple times (adding -a before each) to accept several addresses" required:"false"`
|
||||
UseExistingChangeAddress bool `long:"use-existing-change-address" short:"u" description:"Will use an existing change address (in case no change address was ever used, it will use a new one)"`
|
||||
MaxFeeRate float64 `long:"max-fee-rate" short:"m" description:"Maximum fee rate in Sompi/gram to use for the transaction. The wallet will take the minimum between the fee rate estimate from the connected node and this value."`
|
||||
FeeRate float64 `long:"fee-rate" short:"r" description:"Fee rate in Sompi/gram to use for the transaction. This option will override any fee estimate from the connected node."`
|
||||
MaxFee uint64 `long:"max-fee" short:"x" description:"Maximum fee in Sompi (not Sompi/gram) to use for the transaction. The wallet will take the minimum between the fee estimate from the connected node and this value. If no other fee policy is specified, it will set the max fee to 1 KAS"`
|
||||
Verbose bool `long:"show-serialized" short:"s" description:"Show a list of hex encoded sent transactions"`
|
||||
config.NetworkFlags
|
||||
}
|
||||
|
||||
type versionConfig struct {
|
||||
}
|
||||
|
||||
@ -197,6 +233,12 @@ func parseCommandLine() (subCommand string, config interface{}) {
|
||||
parser.AddCommand(versionSubCmd, "Get the wallet version", "Get the wallet version", &versionConfig{})
|
||||
getDaemonVersionConf := &getDaemonVersionConfig{DaemonAddress: defaultListen}
|
||||
parser.AddCommand(getDaemonVersionSubCmd, "Get the wallet daemon version", "Get the wallet daemon version", getDaemonVersionConf)
|
||||
bumpFeeConf := &bumpFeeConfig{DaemonAddress: defaultListen}
|
||||
parser.AddCommand(bumpFeeSubCmd, "Bump transaction fee (with signing and broadcast)", "Bump transaction fee (with signing and broadcast)", bumpFeeConf)
|
||||
bumpFeeUnsignedConf := &bumpFeeUnsignedConfig{DaemonAddress: defaultListen}
|
||||
parser.AddCommand(bumpFeeUnsignedSubCmd, "Bump transaction fee (without signing)", "Bump transaction fee (without signing)", bumpFeeUnsignedConf)
|
||||
parser.AddCommand(broadcastReplacementSubCmd, "Broadcast the given transaction replacement",
|
||||
"Broadcast the given transaction replacement", broadcastConf)
|
||||
|
||||
_, err := parser.Parse()
|
||||
if err != nil {
|
||||
@ -267,6 +309,13 @@ func parseCommandLine() (subCommand string, config interface{}) {
|
||||
printErrorAndExit(err)
|
||||
}
|
||||
config = broadcastConf
|
||||
case broadcastReplacementSubCmd:
|
||||
combineNetworkFlags(&broadcastConf.NetworkFlags, &cfg.NetworkFlags)
|
||||
err := broadcastConf.ResolveNetwork(parser)
|
||||
if err != nil {
|
||||
printErrorAndExit(err)
|
||||
}
|
||||
config = broadcastConf
|
||||
case parseSubCmd:
|
||||
combineNetworkFlags(&parseConf.NetworkFlags, &cfg.NetworkFlags)
|
||||
err := parseConf.ResolveNetwork(parser)
|
||||
@ -305,6 +354,32 @@ func parseCommandLine() (subCommand string, config interface{}) {
|
||||
case versionSubCmd:
|
||||
case getDaemonVersionSubCmd:
|
||||
config = getDaemonVersionConf
|
||||
case bumpFeeSubCmd:
|
||||
combineNetworkFlags(&bumpFeeConf.NetworkFlags, &cfg.NetworkFlags)
|
||||
err := bumpFeeConf.ResolveNetwork(parser)
|
||||
if err != nil {
|
||||
printErrorAndExit(err)
|
||||
}
|
||||
|
||||
err = validateBumpFeeConfig(bumpFeeConf)
|
||||
if err != nil {
|
||||
printErrorAndExit(err)
|
||||
}
|
||||
|
||||
config = bumpFeeConf
|
||||
case bumpFeeUnsignedSubCmd:
|
||||
combineNetworkFlags(&bumpFeeUnsignedConf.NetworkFlags, &cfg.NetworkFlags)
|
||||
err := bumpFeeUnsignedConf.ResolveNetwork(parser)
|
||||
if err != nil {
|
||||
printErrorAndExit(err)
|
||||
}
|
||||
|
||||
err = validateBumpFeeUnsignedConfig(bumpFeeUnsignedConf)
|
||||
if err != nil {
|
||||
printErrorAndExit(err)
|
||||
}
|
||||
|
||||
config = bumpFeeUnsignedConf
|
||||
}
|
||||
|
||||
return parser.Command.Active.Name, config
|
||||
@ -316,6 +391,19 @@ func validateCreateUnsignedTransactionConf(conf *createUnsignedTransactionConfig
|
||||
|
||||
return errors.New("exactly one of '--send-amount' or '--all' must be specified")
|
||||
}
|
||||
|
||||
if conf.MaxFeeRate < 0 {
|
||||
return errors.New("--max-fee-rate must be a positive number")
|
||||
}
|
||||
|
||||
if conf.FeeRate < 0 {
|
||||
return errors.New("--fee-rate must be a positive number")
|
||||
}
|
||||
|
||||
if boolToUint8(conf.MaxFeeRate > 0)+boolToUint8(conf.FeeRate > 0)+boolToUint8(conf.MaxFee > 0) > 1 {
|
||||
return errors.New("at most one of '--max-fee-rate', '--fee-rate' or '--max-fee' can be specified")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -325,9 +413,61 @@ func validateSendConfig(conf *sendConfig) error {
|
||||
|
||||
return errors.New("exactly one of '--send-amount' or '--all' must be specified")
|
||||
}
|
||||
|
||||
if conf.MaxFeeRate < 0 {
|
||||
return errors.New("--max-fee-rate must be a positive number")
|
||||
}
|
||||
|
||||
if conf.FeeRate < 0 {
|
||||
return errors.New("--fee-rate must be a positive number")
|
||||
}
|
||||
|
||||
if boolToUint8(conf.MaxFeeRate > 0)+boolToUint8(conf.FeeRate > 0)+boolToUint8(conf.MaxFee > 0) > 1 {
|
||||
return errors.New("at most one of '--max-fee-rate', '--fee-rate' or '--max-fee' can be specified")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateBumpFeeConfig(conf *bumpFeeConfig) error {
|
||||
if conf.MaxFeeRate < 0 {
|
||||
return errors.New("--max-fee-rate must be a positive number")
|
||||
}
|
||||
|
||||
if conf.FeeRate < 0 {
|
||||
return errors.New("--fee-rate must be a positive number")
|
||||
}
|
||||
|
||||
if boolToUint8(conf.MaxFeeRate > 0)+boolToUint8(conf.FeeRate > 0)+boolToUint8(conf.MaxFee > 0) > 1 {
|
||||
return errors.New("at most one of '--max-fee-rate', '--fee-rate' or '--max-fee' can be specified")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateBumpFeeUnsignedConfig(conf *bumpFeeUnsignedConfig) error {
|
||||
if conf.MaxFeeRate < 0 {
|
||||
return errors.New("--max-fee-rate must be a positive number")
|
||||
}
|
||||
|
||||
if conf.FeeRate < 0 {
|
||||
return errors.New("--fee-rate must be a positive number")
|
||||
}
|
||||
|
||||
if boolToUint8(conf.MaxFeeRate > 0)+boolToUint8(conf.FeeRate > 0)+boolToUint8(conf.MaxFee > 0) > 1 {
|
||||
return errors.New("at most one of '--max-fee-rate', '--fee-rate' or '--max-fee' can be specified")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func boolToUint8(b bool) uint8 {
|
||||
if b {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func combineNetworkFlags(dst, src *config.NetworkFlags) {
|
||||
dst.Testnet = dst.Testnet || src.Testnet
|
||||
dst.Simnet = dst.Simnet || src.Simnet
|
||||
|
@ -7,6 +7,7 @@ import (
|
||||
|
||||
"github.com/kaspanet/kaspad/cmd/kaspawallet/daemon/client"
|
||||
"github.com/kaspanet/kaspad/cmd/kaspawallet/daemon/pb"
|
||||
"github.com/kaspanet/kaspad/cmd/kaspawallet/daemon/server"
|
||||
"github.com/kaspanet/kaspad/cmd/kaspawallet/utils"
|
||||
)
|
||||
|
||||
@ -20,10 +21,30 @@ func createUnsignedTransaction(conf *createUnsignedTransactionConfig) error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), daemonTimeout)
|
||||
defer cancel()
|
||||
|
||||
sendAmountSompi, err := utils.KasToSompi(conf.SendAmount)
|
||||
var sendAmountSompi uint64
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
if !conf.IsSendAll {
|
||||
sendAmountSompi, err = utils.KasToSompi(conf.SendAmount)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
var feePolicy *pb.FeePolicy
|
||||
if conf.FeeRate > 0 {
|
||||
feePolicy = &pb.FeePolicy{
|
||||
FeePolicy: &pb.FeePolicy_ExactFeeRate{
|
||||
ExactFeeRate: conf.FeeRate,
|
||||
},
|
||||
}
|
||||
} else if conf.MaxFeeRate > 0 {
|
||||
feePolicy = &pb.FeePolicy{
|
||||
FeePolicy: &pb.FeePolicy_MaxFeeRate{MaxFeeRate: conf.MaxFeeRate},
|
||||
}
|
||||
} else if conf.MaxFee > 0 {
|
||||
feePolicy = &pb.FeePolicy{
|
||||
FeePolicy: &pb.FeePolicy_MaxFee{MaxFee: conf.MaxFee},
|
||||
}
|
||||
}
|
||||
|
||||
response, err := daemonClient.CreateUnsignedTransactions(ctx, &pb.CreateUnsignedTransactionsRequest{
|
||||
@ -32,13 +53,14 @@ func createUnsignedTransaction(conf *createUnsignedTransactionConfig) error {
|
||||
Amount: sendAmountSompi,
|
||||
IsSendAll: conf.IsSendAll,
|
||||
UseExistingChangeAddress: conf.UseExistingChangeAddress,
|
||||
FeePolicy: feePolicy,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Fprintln(os.Stderr, "Created unsigned transaction")
|
||||
fmt.Println(encodeTransactionsToHex(response.UnsignedTransactions))
|
||||
fmt.Println(server.EncodeTransactionsToHex(response.UnsignedTransactions))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
@ -2,9 +2,10 @@ package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/kaspanet/kaspad/cmd/kaspawallet/daemon/server"
|
||||
"time"
|
||||
|
||||
"github.com/kaspanet/kaspad/cmd/kaspawallet/daemon/server"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/kaspanet/kaspad/cmd/kaspawallet/daemon/pb"
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -4,22 +4,28 @@ option go_package = "github.com/kaspanet/kaspad/cmd/kaspawallet/daemon/pb";
|
||||
package kaspawalletd;
|
||||
|
||||
service kaspawalletd {
|
||||
rpc GetBalance (GetBalanceRequest) returns (GetBalanceResponse) {}
|
||||
rpc GetExternalSpendableUTXOs (GetExternalSpendableUTXOsRequest) returns (GetExternalSpendableUTXOsResponse) {}
|
||||
rpc CreateUnsignedTransactions (CreateUnsignedTransactionsRequest) returns (CreateUnsignedTransactionsResponse) {}
|
||||
rpc ShowAddresses (ShowAddressesRequest) returns (ShowAddressesResponse) {}
|
||||
rpc NewAddress (NewAddressRequest) returns (NewAddressResponse) {}
|
||||
rpc Shutdown (ShutdownRequest) returns (ShutdownResponse) {}
|
||||
rpc Broadcast (BroadcastRequest) returns (BroadcastResponse) {}
|
||||
// Since SendRequest contains a password - this command should only be used on a trusted or secure connection
|
||||
rpc GetBalance(GetBalanceRequest) returns (GetBalanceResponse) {}
|
||||
rpc GetExternalSpendableUTXOs(GetExternalSpendableUTXOsRequest)
|
||||
returns (GetExternalSpendableUTXOsResponse) {}
|
||||
rpc CreateUnsignedTransactions(CreateUnsignedTransactionsRequest)
|
||||
returns (CreateUnsignedTransactionsResponse) {}
|
||||
rpc ShowAddresses(ShowAddressesRequest) returns (ShowAddressesResponse) {}
|
||||
rpc NewAddress(NewAddressRequest) returns (NewAddressResponse) {}
|
||||
rpc Shutdown(ShutdownRequest) returns (ShutdownResponse) {}
|
||||
rpc Broadcast(BroadcastRequest) returns (BroadcastResponse) {}
|
||||
// BroadcastReplacement assumes that all transactions depend on the first one
|
||||
rpc BroadcastReplacement(BroadcastRequest) returns (BroadcastResponse) {}
|
||||
// Since SendRequest contains a password - this command should only be used on
|
||||
// a trusted or secure connection
|
||||
rpc Send(SendRequest) returns (SendResponse) {}
|
||||
// Since SignRequest contains a password - this command should only be used on a trusted or secure connection
|
||||
// Since SignRequest contains a password - this command should only be used on
|
||||
// a trusted or secure connection
|
||||
rpc Sign(SignRequest) returns (SignResponse) {}
|
||||
rpc GetVersion(GetVersionRequest) returns (GetVersionResponse) {}
|
||||
rpc BumpFee(BumpFeeRequest) returns (BumpFeeResponse) {}
|
||||
}
|
||||
|
||||
message GetBalanceRequest {
|
||||
}
|
||||
message GetBalanceRequest {}
|
||||
|
||||
message GetBalanceResponse {
|
||||
uint64 available = 1;
|
||||
@ -33,46 +39,45 @@ message AddressBalances {
|
||||
uint64 pending = 3;
|
||||
}
|
||||
|
||||
message FeePolicy {
|
||||
oneof feePolicy {
|
||||
double maxFeeRate = 6;
|
||||
double exactFeeRate = 7;
|
||||
uint64 maxFee = 8;
|
||||
}
|
||||
}
|
||||
|
||||
message CreateUnsignedTransactionsRequest {
|
||||
string address = 1;
|
||||
uint64 amount = 2;
|
||||
repeated string from = 3;
|
||||
bool useExistingChangeAddress = 4;
|
||||
bool isSendAll = 5;
|
||||
FeePolicy feePolicy = 6;
|
||||
}
|
||||
|
||||
message CreateUnsignedTransactionsResponse {
|
||||
repeated bytes unsignedTransactions = 1;
|
||||
}
|
||||
|
||||
message ShowAddressesRequest {
|
||||
}
|
||||
message ShowAddressesRequest {}
|
||||
|
||||
message ShowAddressesResponse {
|
||||
repeated string address = 1;
|
||||
}
|
||||
message ShowAddressesResponse { repeated string address = 1; }
|
||||
|
||||
message NewAddressRequest {
|
||||
}
|
||||
message NewAddressRequest {}
|
||||
|
||||
message NewAddressResponse {
|
||||
string address = 1;
|
||||
}
|
||||
message NewAddressResponse { string address = 1; }
|
||||
|
||||
message BroadcastRequest {
|
||||
bool isDomain = 1;
|
||||
repeated bytes transactions = 2;
|
||||
}
|
||||
|
||||
message BroadcastResponse {
|
||||
repeated string txIDs = 1;
|
||||
}
|
||||
message BroadcastResponse { repeated string txIDs = 1; }
|
||||
|
||||
message ShutdownRequest {
|
||||
}
|
||||
message ShutdownRequest {}
|
||||
|
||||
message ShutdownResponse {
|
||||
}
|
||||
message ShutdownResponse {}
|
||||
|
||||
message Outpoint {
|
||||
string transactionId = 1;
|
||||
@ -97,41 +102,50 @@ message UtxoEntry {
|
||||
bool isCoinbase = 4;
|
||||
}
|
||||
|
||||
message GetExternalSpendableUTXOsRequest{
|
||||
string address = 1;
|
||||
}
|
||||
message GetExternalSpendableUTXOsRequest { string address = 1; }
|
||||
|
||||
message GetExternalSpendableUTXOsResponse{
|
||||
message GetExternalSpendableUTXOsResponse {
|
||||
repeated UtxosByAddressesEntry Entries = 1;
|
||||
}
|
||||
// Since SendRequest contains a password - this command should only be used on a trusted or secure connection
|
||||
message SendRequest{
|
||||
// Since SendRequest contains a password - this command should only be used on a
|
||||
// trusted or secure connection
|
||||
message SendRequest {
|
||||
string toAddress = 1;
|
||||
uint64 amount = 2;
|
||||
string password = 3;
|
||||
repeated string from = 4;
|
||||
bool useExistingChangeAddress = 5;
|
||||
bool isSendAll = 6;
|
||||
FeePolicy feePolicy = 7;
|
||||
}
|
||||
|
||||
message SendResponse{
|
||||
message SendResponse {
|
||||
repeated string txIDs = 1;
|
||||
repeated bytes signedTransactions = 2;
|
||||
}
|
||||
|
||||
// Since SignRequest contains a password - this command should only be used on a trusted or secure connection
|
||||
message SignRequest{
|
||||
// Since SignRequest contains a password - this command should only be used on a
|
||||
// trusted or secure connection
|
||||
message SignRequest {
|
||||
repeated bytes unsignedTransactions = 1;
|
||||
string password = 2;
|
||||
}
|
||||
|
||||
message SignResponse{
|
||||
repeated bytes signedTransactions = 1;
|
||||
message SignResponse { repeated bytes signedTransactions = 1; }
|
||||
|
||||
message GetVersionRequest {}
|
||||
|
||||
message GetVersionResponse { string version = 1; }
|
||||
|
||||
message BumpFeeRequest {
|
||||
string password = 1;
|
||||
repeated string from = 2;
|
||||
bool useExistingChangeAddress = 3;
|
||||
FeePolicy feePolicy = 4;
|
||||
string txID = 5;
|
||||
}
|
||||
|
||||
message GetVersionRequest{
|
||||
message BumpFeeResponse {
|
||||
repeated bytes transactions = 1;
|
||||
repeated string txIDs = 2;
|
||||
}
|
||||
|
||||
message GetVersionResponse{
|
||||
string version = 1;
|
||||
}
|
@ -29,11 +29,16 @@ type KaspawalletdClient interface {
|
||||
NewAddress(ctx context.Context, in *NewAddressRequest, opts ...grpc.CallOption) (*NewAddressResponse, error)
|
||||
Shutdown(ctx context.Context, in *ShutdownRequest, opts ...grpc.CallOption) (*ShutdownResponse, error)
|
||||
Broadcast(ctx context.Context, in *BroadcastRequest, opts ...grpc.CallOption) (*BroadcastResponse, error)
|
||||
// Since SendRequest contains a password - this command should only be used on a trusted or secure connection
|
||||
// BroadcastReplacement assumes that all transactions depend on the first one
|
||||
BroadcastReplacement(ctx context.Context, in *BroadcastRequest, opts ...grpc.CallOption) (*BroadcastResponse, error)
|
||||
// Since SendRequest contains a password - this command should only be used on
|
||||
// a trusted or secure connection
|
||||
Send(ctx context.Context, in *SendRequest, opts ...grpc.CallOption) (*SendResponse, error)
|
||||
// Since SignRequest contains a password - this command should only be used on a trusted or secure connection
|
||||
// Since SignRequest contains a password - this command should only be used on
|
||||
// a trusted or secure connection
|
||||
Sign(ctx context.Context, in *SignRequest, opts ...grpc.CallOption) (*SignResponse, error)
|
||||
GetVersion(ctx context.Context, in *GetVersionRequest, opts ...grpc.CallOption) (*GetVersionResponse, error)
|
||||
BumpFee(ctx context.Context, in *BumpFeeRequest, opts ...grpc.CallOption) (*BumpFeeResponse, error)
|
||||
}
|
||||
|
||||
type kaspawalletdClient struct {
|
||||
@ -107,6 +112,15 @@ func (c *kaspawalletdClient) Broadcast(ctx context.Context, in *BroadcastRequest
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *kaspawalletdClient) BroadcastReplacement(ctx context.Context, in *BroadcastRequest, opts ...grpc.CallOption) (*BroadcastResponse, error) {
|
||||
out := new(BroadcastResponse)
|
||||
err := c.cc.Invoke(ctx, "/kaspawalletd.kaspawalletd/BroadcastReplacement", in, out, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *kaspawalletdClient) Send(ctx context.Context, in *SendRequest, opts ...grpc.CallOption) (*SendResponse, error) {
|
||||
out := new(SendResponse)
|
||||
err := c.cc.Invoke(ctx, "/kaspawalletd.kaspawalletd/Send", in, out, opts...)
|
||||
@ -134,6 +148,15 @@ func (c *kaspawalletdClient) GetVersion(ctx context.Context, in *GetVersionReque
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *kaspawalletdClient) BumpFee(ctx context.Context, in *BumpFeeRequest, opts ...grpc.CallOption) (*BumpFeeResponse, error) {
|
||||
out := new(BumpFeeResponse)
|
||||
err := c.cc.Invoke(ctx, "/kaspawalletd.kaspawalletd/BumpFee", in, out, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// KaspawalletdServer is the server API for Kaspawalletd service.
|
||||
// All implementations must embed UnimplementedKaspawalletdServer
|
||||
// for forward compatibility
|
||||
@ -145,11 +168,16 @@ type KaspawalletdServer interface {
|
||||
NewAddress(context.Context, *NewAddressRequest) (*NewAddressResponse, error)
|
||||
Shutdown(context.Context, *ShutdownRequest) (*ShutdownResponse, error)
|
||||
Broadcast(context.Context, *BroadcastRequest) (*BroadcastResponse, error)
|
||||
// Since SendRequest contains a password - this command should only be used on a trusted or secure connection
|
||||
// BroadcastReplacement assumes that all transactions depend on the first one
|
||||
BroadcastReplacement(context.Context, *BroadcastRequest) (*BroadcastResponse, error)
|
||||
// Since SendRequest contains a password - this command should only be used on
|
||||
// a trusted or secure connection
|
||||
Send(context.Context, *SendRequest) (*SendResponse, error)
|
||||
// Since SignRequest contains a password - this command should only be used on a trusted or secure connection
|
||||
// Since SignRequest contains a password - this command should only be used on
|
||||
// a trusted or secure connection
|
||||
Sign(context.Context, *SignRequest) (*SignResponse, error)
|
||||
GetVersion(context.Context, *GetVersionRequest) (*GetVersionResponse, error)
|
||||
BumpFee(context.Context, *BumpFeeRequest) (*BumpFeeResponse, error)
|
||||
mustEmbedUnimplementedKaspawalletdServer()
|
||||
}
|
||||
|
||||
@ -178,6 +206,9 @@ func (UnimplementedKaspawalletdServer) Shutdown(context.Context, *ShutdownReques
|
||||
func (UnimplementedKaspawalletdServer) Broadcast(context.Context, *BroadcastRequest) (*BroadcastResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method Broadcast not implemented")
|
||||
}
|
||||
func (UnimplementedKaspawalletdServer) BroadcastReplacement(context.Context, *BroadcastRequest) (*BroadcastResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method BroadcastReplacement not implemented")
|
||||
}
|
||||
func (UnimplementedKaspawalletdServer) Send(context.Context, *SendRequest) (*SendResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method Send not implemented")
|
||||
}
|
||||
@ -187,6 +218,9 @@ func (UnimplementedKaspawalletdServer) Sign(context.Context, *SignRequest) (*Sig
|
||||
func (UnimplementedKaspawalletdServer) GetVersion(context.Context, *GetVersionRequest) (*GetVersionResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method GetVersion not implemented")
|
||||
}
|
||||
func (UnimplementedKaspawalletdServer) BumpFee(context.Context, *BumpFeeRequest) (*BumpFeeResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method BumpFee not implemented")
|
||||
}
|
||||
func (UnimplementedKaspawalletdServer) mustEmbedUnimplementedKaspawalletdServer() {}
|
||||
|
||||
// UnsafeKaspawalletdServer may be embedded to opt out of forward compatibility for this service.
|
||||
@ -326,6 +360,24 @@ func _Kaspawalletd_Broadcast_Handler(srv interface{}, ctx context.Context, dec f
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _Kaspawalletd_BroadcastReplacement_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(BroadcastRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(KaspawalletdServer).BroadcastReplacement(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: "/kaspawalletd.kaspawalletd/BroadcastReplacement",
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(KaspawalletdServer).BroadcastReplacement(ctx, req.(*BroadcastRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _Kaspawalletd_Send_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(SendRequest)
|
||||
if err := dec(in); err != nil {
|
||||
@ -380,6 +432,24 @@ func _Kaspawalletd_GetVersion_Handler(srv interface{}, ctx context.Context, dec
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _Kaspawalletd_BumpFee_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(BumpFeeRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(KaspawalletdServer).BumpFee(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: "/kaspawalletd.kaspawalletd/BumpFee",
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(KaspawalletdServer).BumpFee(ctx, req.(*BumpFeeRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
// Kaspawalletd_ServiceDesc is the grpc.ServiceDesc for Kaspawalletd service.
|
||||
// It's only intended for direct use with grpc.RegisterService,
|
||||
// and not to be introspected or modified (even as a copy)
|
||||
@ -415,6 +485,10 @@ var Kaspawalletd_ServiceDesc = grpc.ServiceDesc{
|
||||
MethodName: "Broadcast",
|
||||
Handler: _Kaspawalletd_Broadcast_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "BroadcastReplacement",
|
||||
Handler: _Kaspawalletd_BroadcastReplacement_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "Send",
|
||||
Handler: _Kaspawalletd_Send_Handler,
|
||||
@ -427,6 +501,10 @@ var Kaspawalletd_ServiceDesc = grpc.ServiceDesc{
|
||||
MethodName: "GetVersion",
|
||||
Handler: _Kaspawalletd_GetVersion_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "BumpFee",
|
||||
Handler: _Kaspawalletd_BumpFee_Handler,
|
||||
},
|
||||
},
|
||||
Streams: []grpc.StreamDesc{},
|
||||
Metadata: "kaspawalletd.proto",
|
||||
|
81
cmd/kaspawallet/daemon/server/broadcast_replacement.go
Normal file
81
cmd/kaspawallet/daemon/server/broadcast_replacement.go
Normal file
@ -0,0 +1,81 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/kaspanet/kaspad/app/appmessage"
|
||||
"github.com/kaspanet/kaspad/cmd/kaspawallet/daemon/pb"
|
||||
"github.com/kaspanet/kaspad/cmd/kaspawallet/libkaspawallet"
|
||||
"github.com/kaspanet/kaspad/cmd/kaspawallet/libkaspawallet/serialization"
|
||||
"github.com/kaspanet/kaspad/domain/consensus/model/externalapi"
|
||||
"github.com/kaspanet/kaspad/domain/consensus/utils/consensushashing"
|
||||
"github.com/kaspanet/kaspad/infrastructure/network/rpcclient"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func (s *server) BroadcastReplacement(_ context.Context, request *pb.BroadcastRequest) (*pb.BroadcastResponse, error) {
|
||||
s.lock.Lock()
|
||||
defer s.lock.Unlock()
|
||||
|
||||
txIDs, err := s.broadcastReplacement(request.Transactions, request.IsDomain)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &pb.BroadcastResponse{TxIDs: txIDs}, nil
|
||||
}
|
||||
|
||||
// broadcastReplacement assumes that all transactions depend on the first one
|
||||
func (s *server) broadcastReplacement(transactions [][]byte, isDomain bool) ([]string, error) {
|
||||
|
||||
txIDs := make([]string, len(transactions))
|
||||
var tx *externalapi.DomainTransaction
|
||||
var err error
|
||||
|
||||
for i, transaction := range transactions {
|
||||
|
||||
if isDomain {
|
||||
tx, err = serialization.DeserializeDomainTransaction(transaction)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else if !isDomain { //default in proto3 is false
|
||||
tx, err = libkaspawallet.ExtractTransaction(transaction, s.keysFile.ECDSA)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// Once the first transaction is added to the mempool, the transactions that depend
|
||||
// on the replaced transaction will be removed, so there's no need to submit them
|
||||
// as RBF transactions.
|
||||
if i == 0 {
|
||||
txIDs[i], err = sendTransactionRBF(s.rpcClient, tx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
txIDs[i], err = sendTransaction(s.rpcClient, tx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
for _, input := range tx.Inputs {
|
||||
s.usedOutpoints[input.PreviousOutpoint] = time.Now()
|
||||
}
|
||||
}
|
||||
|
||||
s.forceSync()
|
||||
return txIDs, nil
|
||||
}
|
||||
|
||||
func sendTransactionRBF(client *rpcclient.RPCClient, tx *externalapi.DomainTransaction) (string, error) {
|
||||
submitTransactionResponse, err := client.SubmitTransactionReplacement(appmessage.DomainTransactionToRPCTransaction(tx), consensushashing.TransactionID(tx).String())
|
||||
if err != nil {
|
||||
return "", errors.Wrapf(err, "error submitting transaction replacement")
|
||||
}
|
||||
return submitTransactionResponse.TransactionID, nil
|
||||
}
|
156
cmd/kaspawallet/daemon/server/bump_fee.go
Normal file
156
cmd/kaspawallet/daemon/server/bump_fee.go
Normal file
@ -0,0 +1,156 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/kaspanet/kaspad/app/appmessage"
|
||||
"github.com/kaspanet/kaspad/cmd/kaspawallet/daemon/pb"
|
||||
"github.com/kaspanet/kaspad/cmd/kaspawallet/libkaspawallet"
|
||||
"github.com/kaspanet/kaspad/domain/consensus/model/externalapi"
|
||||
"github.com/kaspanet/kaspad/domain/consensus/utils/txscript"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func (s *server) BumpFee(_ context.Context, request *pb.BumpFeeRequest) (*pb.BumpFeeResponse, error) {
|
||||
s.lock.Lock()
|
||||
defer s.lock.Unlock()
|
||||
|
||||
entry, err := s.rpcClient.GetMempoolEntry(request.TxID, false, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
domainTx, err := appmessage.RPCTransactionToDomainTransaction(entry.Entry.Transaction)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
outpointsToInputs := make(map[externalapi.DomainOutpoint]*externalapi.DomainTransactionInput)
|
||||
var maxUTXO *walletUTXO
|
||||
for _, input := range domainTx.Inputs {
|
||||
outpointsToInputs[input.PreviousOutpoint] = input
|
||||
utxo, ok := s.mempoolExcludedUTXOs[input.PreviousOutpoint]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
input.UTXOEntry = utxo.UTXOEntry
|
||||
if maxUTXO == nil || utxo.UTXOEntry.Amount() > maxUTXO.UTXOEntry.Amount() {
|
||||
maxUTXO = utxo
|
||||
}
|
||||
}
|
||||
|
||||
if maxUTXO == nil {
|
||||
// If we got here it means for some reason s.mempoolExcludedUTXOs is not up to date and we need to search for the UTXOs in s.utxosSortedByAmount
|
||||
for _, utxo := range s.utxosSortedByAmount {
|
||||
input, ok := outpointsToInputs[*utxo.Outpoint]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
input.UTXOEntry = utxo.UTXOEntry
|
||||
if maxUTXO == nil || utxo.UTXOEntry.Amount() > maxUTXO.UTXOEntry.Amount() {
|
||||
maxUTXO = utxo
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if maxUTXO == nil {
|
||||
return nil, errors.Errorf("no UTXOs were found for transaction %s. This probably means the transaction is already accepted", request.TxID)
|
||||
}
|
||||
|
||||
mass := s.txMassCalculator.CalculateTransactionOverallMass(domainTx)
|
||||
feeRate := float64(entry.Entry.Fee) / float64(mass)
|
||||
newFeeRate, maxFee, err := s.calculateFeeLimits(request.FeePolicy)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if feeRate >= newFeeRate {
|
||||
return nil, errors.Errorf("new fee rate (%f) is not higher than the current fee rate (%f)", newFeeRate, feeRate)
|
||||
}
|
||||
|
||||
if len(domainTx.Outputs) == 0 || len(domainTx.Outputs) > 2 {
|
||||
return nil, errors.Errorf("kaspawallet supports only transactions with 1 or 2 outputs in transaction %s, but this transaction got %d", request.TxID, len(domainTx.Outputs))
|
||||
}
|
||||
|
||||
var fromAddresses []*walletAddress
|
||||
for _, from := range request.From {
|
||||
fromAddress, exists := s.addressSet[from]
|
||||
if !exists {
|
||||
return nil, errors.Errorf("specified from address %s does not exists", from)
|
||||
}
|
||||
fromAddresses = append(fromAddresses, fromAddress)
|
||||
}
|
||||
|
||||
allowUsed := make(map[externalapi.DomainOutpoint]struct{})
|
||||
for outpoint := range outpointsToInputs {
|
||||
allowUsed[outpoint] = struct{}{}
|
||||
}
|
||||
selectedUTXOs, spendValue, changeSompi, err := s.selectUTXOsWithPreselected([]*walletUTXO{maxUTXO}, allowUsed, domainTx.Outputs[0].Value, false, newFeeRate, maxFee, fromAddresses)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
_, toAddress, err := txscript.ExtractScriptPubKeyAddress(domainTx.Outputs[0].ScriptPublicKey, s.params)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
changeAddress, changeWalletAddress, err := s.changeAddress(request.UseExistingChangeAddress, fromAddresses)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(selectedUTXOs) == 0 {
|
||||
return nil, errors.Errorf("couldn't find funds to spend")
|
||||
}
|
||||
|
||||
payments := []*libkaspawallet.Payment{{
|
||||
Address: toAddress,
|
||||
Amount: spendValue,
|
||||
}}
|
||||
if changeSompi > 0 {
|
||||
changeAddress, _, err := s.changeAddress(request.UseExistingChangeAddress, fromAddresses)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
payments = append(payments, &libkaspawallet.Payment{
|
||||
Address: changeAddress,
|
||||
Amount: changeSompi,
|
||||
})
|
||||
}
|
||||
unsignedTransaction, err := libkaspawallet.CreateUnsignedTransaction(s.keysFile.ExtendedPublicKeys,
|
||||
s.keysFile.MinimumSignatures,
|
||||
payments, selectedUTXOs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
unsignedTransactions, err := s.maybeAutoCompoundTransaction(unsignedTransaction, toAddress, changeAddress, changeWalletAddress, newFeeRate, maxFee)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if request.Password == "" {
|
||||
return &pb.BumpFeeResponse{
|
||||
Transactions: unsignedTransactions,
|
||||
}, nil
|
||||
}
|
||||
|
||||
signedTransactions, err := s.signTransactions(unsignedTransactions, request.Password)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
txIDs, err := s.broadcastReplacement(signedTransactions, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &pb.BumpFeeResponse{
|
||||
TxIDs: txIDs,
|
||||
Transactions: signedTransactions,
|
||||
}, nil
|
||||
}
|
@ -3,21 +3,27 @@ package server
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"math"
|
||||
|
||||
"github.com/kaspanet/kaspad/cmd/kaspawallet/daemon/pb"
|
||||
"github.com/kaspanet/kaspad/cmd/kaspawallet/libkaspawallet"
|
||||
"github.com/kaspanet/kaspad/domain/consensus/model/externalapi"
|
||||
"github.com/kaspanet/kaspad/domain/consensus/utils/constants"
|
||||
"github.com/kaspanet/kaspad/domain/consensus/utils/utxo"
|
||||
"github.com/kaspanet/kaspad/util"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// TODO: Implement a better fee estimation mechanism
|
||||
const feePerInput = 10000
|
||||
|
||||
// The minimal change amount to target in order to avoid large storage mass (see KIP9 for more details).
|
||||
// By having at least 0.2KAS in the change output we make sure that every transaction with send value >= 0.2KAS
|
||||
// should succeed (at most 50K storage mass for each output, thus overall lower than standard mass upper bound which is 100K gram)
|
||||
const minChangeTarget = constants.SompiPerKaspa / 5
|
||||
// By having at least 10KAS in the change output we make sure that the storage mass charged for change is
|
||||
// at most 1000 gram. Generally, if the payment is above 10KAS as well, the resulting storage mass will be
|
||||
// in the order of magnitude of compute mass and wil not incur additional charges.
|
||||
// Additionally, every transaction with send value > ~0.1 KAS should succeed (at most ~99K storage mass for payment
|
||||
// output, thus overall lower than standard mass upper bound which is 100K gram)
|
||||
const minChangeTarget = constants.SompiPerKaspa * 10
|
||||
|
||||
// The current minimal fee rate according to mempool standards
|
||||
const minFeeRate = 1.0
|
||||
|
||||
func (s *server) CreateUnsignedTransactions(_ context.Context, request *pb.CreateUnsignedTransactionsRequest) (
|
||||
*pb.CreateUnsignedTransactionsResponse, error,
|
||||
@ -26,7 +32,7 @@ func (s *server) CreateUnsignedTransactions(_ context.Context, request *pb.Creat
|
||||
defer s.lock.Unlock()
|
||||
|
||||
unsignedTransactions, err := s.createUnsignedTransactions(request.Address, request.Amount, request.IsSendAll,
|
||||
request.From, request.UseExistingChangeAddress)
|
||||
request.From, request.UseExistingChangeAddress, request.FeePolicy)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -34,10 +40,59 @@ func (s *server) CreateUnsignedTransactions(_ context.Context, request *pb.Creat
|
||||
return &pb.CreateUnsignedTransactionsResponse{UnsignedTransactions: unsignedTransactions}, nil
|
||||
}
|
||||
|
||||
func (s *server) createUnsignedTransactions(address string, amount uint64, isSendAll bool, fromAddressesString []string, useExistingChangeAddress bool) ([][]byte, error) {
|
||||
func (s *server) calculateFeeLimits(requestFeePolicy *pb.FeePolicy) (feeRate float64, maxFee uint64, err error) {
|
||||
feeRate = minFeeRate
|
||||
maxFee = math.MaxUint64
|
||||
|
||||
if requestFeePolicy == nil {
|
||||
requestFeePolicy = &pb.FeePolicy{}
|
||||
}
|
||||
|
||||
switch requestFeePolicy := requestFeePolicy.FeePolicy.(type) {
|
||||
case *pb.FeePolicy_ExactFeeRate:
|
||||
feeRate = requestFeePolicy.ExactFeeRate
|
||||
if feeRate < minFeeRate {
|
||||
return 0, 0, errors.Errorf("requested fee rate %f is too low, minimum fee rate is %f", feeRate, minFeeRate)
|
||||
}
|
||||
case *pb.FeePolicy_MaxFeeRate:
|
||||
estimate, err := s.rpcClient.GetFeeEstimate()
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
if requestFeePolicy.MaxFeeRate < minFeeRate {
|
||||
return 0, 0, errors.Errorf("requested max fee rate %f is too low, minimum fee rate is %f", requestFeePolicy.MaxFeeRate, minFeeRate)
|
||||
}
|
||||
feeRate = math.Min(estimate.Estimate.NormalBuckets[0].Feerate, requestFeePolicy.MaxFeeRate)
|
||||
case *pb.FeePolicy_MaxFee:
|
||||
estimate, err := s.rpcClient.GetFeeEstimate()
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
feeRate = estimate.Estimate.NormalBuckets[0].Feerate
|
||||
maxFee = requestFeePolicy.MaxFee
|
||||
case nil:
|
||||
estimate, err := s.rpcClient.GetFeeEstimate()
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
feeRate = estimate.Estimate.NormalBuckets[0].Feerate
|
||||
// Default to a bound of max 1 KAS as fee
|
||||
maxFee = constants.SompiPerKaspa
|
||||
}
|
||||
|
||||
return feeRate, maxFee, nil
|
||||
}
|
||||
|
||||
func (s *server) createUnsignedTransactions(address string, amount uint64, isSendAll bool, fromAddressesString []string, useExistingChangeAddress bool, requestFeePolicy *pb.FeePolicy) ([][]byte, error) {
|
||||
if !s.isSynced() {
|
||||
return nil, errors.Errorf("wallet daemon is not synced yet, %s", s.formatSyncStateReport())
|
||||
}
|
||||
|
||||
feeRate, maxFee, err := s.calculateFeeLimits(requestFeePolicy)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// make sure address string is correct before proceeding to a
|
||||
// potentially long UTXO refreshment operation
|
||||
toAddress, err := util.DecodeAddress(address, s.params.Prefix)
|
||||
@ -54,7 +109,12 @@ func (s *server) createUnsignedTransactions(address string, amount uint64, isSen
|
||||
fromAddresses = append(fromAddresses, fromAddress)
|
||||
}
|
||||
|
||||
selectedUTXOs, spendValue, changeSompi, err := s.selectUTXOs(amount, isSendAll, feePerInput, fromAddresses)
|
||||
changeAddress, changeWalletAddress, err := s.changeAddress(useExistingChangeAddress, fromAddresses)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
selectedUTXOs, spendValue, changeSompi, err := s.selectUTXOs(amount, isSendAll, feeRate, maxFee, fromAddresses)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -63,11 +123,6 @@ func (s *server) createUnsignedTransactions(address string, amount uint64, isSen
|
||||
return nil, errors.Errorf("couldn't find funds to spend")
|
||||
}
|
||||
|
||||
changeAddress, changeWalletAddress, err := s.changeAddress(useExistingChangeAddress, fromAddresses)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
payments := []*libkaspawallet.Payment{{
|
||||
Address: toAddress,
|
||||
Amount: spendValue,
|
||||
@ -85,17 +140,25 @@ func (s *server) createUnsignedTransactions(address string, amount uint64, isSen
|
||||
return nil, err
|
||||
}
|
||||
|
||||
unsignedTransactions, err := s.maybeAutoCompoundTransaction(unsignedTransaction, toAddress, changeAddress, changeWalletAddress)
|
||||
unsignedTransactions, err := s.maybeAutoCompoundTransaction(unsignedTransaction, toAddress, changeAddress, changeWalletAddress, feeRate, maxFee)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return unsignedTransactions, nil
|
||||
}
|
||||
|
||||
func (s *server) selectUTXOs(spendAmount uint64, isSendAll bool, feePerInput uint64, fromAddresses []*walletAddress) (
|
||||
func (s *server) selectUTXOs(spendAmount uint64, isSendAll bool, feeRate float64, maxFee uint64, fromAddresses []*walletAddress) (
|
||||
selectedUTXOs []*libkaspawallet.UTXO, totalReceived uint64, changeSompi uint64, err error) {
|
||||
return s.selectUTXOsWithPreselected(nil, map[externalapi.DomainOutpoint]struct{}{}, spendAmount, isSendAll, feeRate, maxFee, fromAddresses)
|
||||
}
|
||||
|
||||
func (s *server) selectUTXOsWithPreselected(preSelectedUTXOs []*walletUTXO, allowUsed map[externalapi.DomainOutpoint]struct{}, spendAmount uint64, isSendAll bool, feeRate float64, maxFee uint64, fromAddresses []*walletAddress) (
|
||||
selectedUTXOs []*libkaspawallet.UTXO, totalReceived uint64, changeSompi uint64, err error) {
|
||||
|
||||
selectedUTXOs = []*libkaspawallet.UTXO{}
|
||||
preSelectedSet := make(map[externalapi.DomainOutpoint]struct{})
|
||||
for _, utxo := range preSelectedUTXOs {
|
||||
preSelectedSet[*utxo.Outpoint] = struct{}{}
|
||||
}
|
||||
totalValue := uint64(0)
|
||||
|
||||
dagInfo, err := s.rpcClient.GetBlockDAGInfo()
|
||||
@ -103,17 +166,26 @@ func (s *server) selectUTXOs(spendAmount uint64, isSendAll bool, feePerInput uin
|
||||
return nil, 0, 0, err
|
||||
}
|
||||
|
||||
for _, utxo := range s.utxosSortedByAmount {
|
||||
var fee uint64
|
||||
iteration := func(utxo *walletUTXO, avoidPreselected bool) (bool, error) {
|
||||
if (fromAddresses != nil && !walletAddressesContain(fromAddresses, utxo.address)) ||
|
||||
!s.isUTXOSpendable(utxo, dagInfo.VirtualDAAScore) {
|
||||
continue
|
||||
return true, nil
|
||||
}
|
||||
|
||||
if broadcastTime, ok := s.usedOutpoints[*utxo.Outpoint]; ok {
|
||||
if s.usedOutpointHasExpired(broadcastTime) {
|
||||
delete(s.usedOutpoints, *utxo.Outpoint)
|
||||
} else {
|
||||
continue
|
||||
if _, ok := allowUsed[*utxo.Outpoint]; !ok {
|
||||
if s.usedOutpointHasExpired(broadcastTime) {
|
||||
delete(s.usedOutpoints, *utxo.Outpoint)
|
||||
} else {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if avoidPreselected {
|
||||
if _, ok := preSelectedSet[*utxo.Outpoint]; ok {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
|
||||
@ -124,8 +196,16 @@ func (s *server) selectUTXOs(spendAmount uint64, isSendAll bool, feePerInput uin
|
||||
})
|
||||
|
||||
totalValue += utxo.UTXOEntry.Amount()
|
||||
estimatedRecipientValue := spendAmount
|
||||
if isSendAll {
|
||||
estimatedRecipientValue = totalValue
|
||||
}
|
||||
|
||||
fee, err = s.estimateFee(selectedUTXOs, feeRate, maxFee, estimatedRecipientValue)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
fee := feePerInput * uint64(len(selectedUTXOs))
|
||||
totalSpend := spendAmount + fee
|
||||
// Two break cases (if not send all):
|
||||
// 1. totalValue == totalSpend, so there's no change needed -> number of outputs = 1, so a single input is sufficient
|
||||
@ -133,11 +213,37 @@ func (s *server) selectUTXOs(spendAmount uint64, isSendAll bool, feePerInput uin
|
||||
// 2.1 go-nodes dust patch we try and find at least 2 inputs (even though the next one is not necessary in terms of spend value)
|
||||
// 2.2 KIP9 we try and make sure that the change amount is not too small
|
||||
if !isSendAll && (totalValue == totalSpend || (totalValue >= totalSpend+minChangeTarget && len(selectedUTXOs) > 1)) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
shouldContinue := true
|
||||
for _, utxo := range preSelectedUTXOs {
|
||||
shouldContinue, err = iteration(utxo, false)
|
||||
if err != nil {
|
||||
return nil, 0, 0, err
|
||||
}
|
||||
|
||||
if !shouldContinue {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
fee := feePerInput * uint64(len(selectedUTXOs))
|
||||
if shouldContinue {
|
||||
for _, utxo := range s.utxosSortedByAmount {
|
||||
shouldContinue, err := iteration(utxo, true)
|
||||
if err != nil {
|
||||
return nil, 0, 0, err
|
||||
}
|
||||
|
||||
if !shouldContinue {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var totalSpend uint64
|
||||
if isSendAll {
|
||||
totalSpend = totalValue
|
||||
@ -154,6 +260,99 @@ func (s *server) selectUTXOs(spendAmount uint64, isSendAll bool, feePerInput uin
|
||||
return selectedUTXOs, totalReceived, totalValue - totalSpend, nil
|
||||
}
|
||||
|
||||
func (s *server) estimateFee(selectedUTXOs []*libkaspawallet.UTXO, feeRate float64, maxFee uint64, recipientValue uint64) (uint64, error) {
|
||||
fakePubKey := [util.PublicKeySizeECDSA]byte{}
|
||||
fakeAddr, err := util.NewAddressPublicKeyECDSA(fakePubKey[:], s.params.Prefix) // We assume the worst case where the recipient address is ECDSA. In this case the scriptPubKey will be the longest.
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
totalValue := uint64(0)
|
||||
for _, utxo := range selectedUTXOs {
|
||||
totalValue += utxo.UTXOEntry.Amount()
|
||||
}
|
||||
|
||||
// This is an approximation for the distribution of value between the recipient output and the change output.
|
||||
var mockPayments []*libkaspawallet.Payment
|
||||
if totalValue > recipientValue {
|
||||
mockPayments = []*libkaspawallet.Payment{
|
||||
{
|
||||
Address: fakeAddr,
|
||||
Amount: recipientValue,
|
||||
},
|
||||
{
|
||||
Address: fakeAddr,
|
||||
Amount: totalValue - recipientValue, // We ignore the fee since we expect it to be insignificant in mass calculation.
|
||||
},
|
||||
}
|
||||
} else {
|
||||
mockPayments = []*libkaspawallet.Payment{
|
||||
{
|
||||
Address: fakeAddr,
|
||||
Amount: totalValue,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
mockTx, err := libkaspawallet.CreateUnsignedTransaction(s.keysFile.ExtendedPublicKeys,
|
||||
s.keysFile.MinimumSignatures,
|
||||
mockPayments, selectedUTXOs)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
mass, err := s.estimateMassAfterSignatures(mockTx)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return min(uint64(math.Ceil(float64(mass)*feeRate)), maxFee), nil
|
||||
}
|
||||
|
||||
func (s *server) estimateFeePerInput(feeRate float64) (uint64, error) {
|
||||
mockUTXO := &libkaspawallet.UTXO{
|
||||
Outpoint: &externalapi.DomainOutpoint{
|
||||
TransactionID: externalapi.DomainTransactionID{},
|
||||
Index: 0,
|
||||
},
|
||||
UTXOEntry: utxo.NewUTXOEntry(1, &externalapi.ScriptPublicKey{
|
||||
Script: nil,
|
||||
Version: 0,
|
||||
}, false, 0),
|
||||
DerivationPath: "m",
|
||||
}
|
||||
|
||||
mockTx, err := libkaspawallet.CreateUnsignedTransaction(s.keysFile.ExtendedPublicKeys,
|
||||
s.keysFile.MinimumSignatures,
|
||||
nil, []*libkaspawallet.UTXO{mockUTXO})
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
// Here we use compute mass to avoid dividing by zero. This is ok since `s.estimateFeePerInput` is only used
|
||||
// in the case of compound transactions that have a compute mass higher than its storage mass.
|
||||
mass, err := s.estimateComputeMassAfterSignatures(mockTx)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
mockTxWithoutUTXO, err := libkaspawallet.CreateUnsignedTransaction(s.keysFile.ExtendedPublicKeys,
|
||||
s.keysFile.MinimumSignatures,
|
||||
nil, nil)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
massWithoutUTXO, err := s.estimateComputeMassAfterSignatures(mockTxWithoutUTXO)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
inputMass := mass - massWithoutUTXO
|
||||
|
||||
return uint64(float64(inputMass) * feeRate), nil
|
||||
}
|
||||
|
||||
func walletAddressesContain(addresses []*walletAddress, contain *walletAddress) bool {
|
||||
for _, address := range addresses {
|
||||
if *address == *contain {
|
||||
|
@ -21,7 +21,15 @@ func (s *server) GetExternalSpendableUTXOs(_ context.Context, request *pb.GetExt
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
selectedUTXOs, err := s.selectExternalSpendableUTXOs(externalUTXOs, request.Address)
|
||||
|
||||
estimate, err := s.rpcClient.GetFeeEstimate()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
feeRate := estimate.Estimate.NormalBuckets[0].Feerate
|
||||
|
||||
selectedUTXOs, err := s.selectExternalSpendableUTXOs(externalUTXOs, feeRate)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -30,7 +38,7 @@ func (s *server) GetExternalSpendableUTXOs(_ context.Context, request *pb.GetExt
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *server) selectExternalSpendableUTXOs(externalUTXOs *appmessage.GetUTXOsByAddressesResponseMessage, address string) ([]*pb.UtxosByAddressesEntry, error) {
|
||||
func (s *server) selectExternalSpendableUTXOs(externalUTXOs *appmessage.GetUTXOsByAddressesResponseMessage, feeRate float64) ([]*pb.UtxosByAddressesEntry, error) {
|
||||
dagInfo, err := s.rpcClient.GetBlockDAGInfo()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -42,8 +50,13 @@ func (s *server) selectExternalSpendableUTXOs(externalUTXOs *appmessage.GetUTXOs
|
||||
//we do not make because we do not know size, because of unspendable utxos
|
||||
var selectedExternalUtxos []*pb.UtxosByAddressesEntry
|
||||
|
||||
feePerInput, err := s.estimateFeePerInput(feeRate)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, entry := range externalUTXOs.Entries {
|
||||
if !isExternalUTXOSpendable(entry, daaScore, maturity) {
|
||||
if !isExternalUTXOSpendable(entry, daaScore, maturity, feePerInput) {
|
||||
continue
|
||||
}
|
||||
selectedExternalUtxos = append(selectedExternalUtxos, libkaspawallet.AppMessageUTXOToKaspawalletdUTXO(entry))
|
||||
@ -52,7 +65,7 @@ func (s *server) selectExternalSpendableUTXOs(externalUTXOs *appmessage.GetUTXOs
|
||||
return selectedExternalUtxos, nil
|
||||
}
|
||||
|
||||
func isExternalUTXOSpendable(entry *appmessage.UTXOsByAddressesEntry, virtualDAAScore uint64, coinbaseMaturity uint64) bool {
|
||||
func isExternalUTXOSpendable(entry *appmessage.UTXOsByAddressesEntry, virtualDAAScore uint64, coinbaseMaturity uint64, feePerInput uint64) bool {
|
||||
if !entry.UTXOEntry.IsCoinbase {
|
||||
return true
|
||||
} else if entry.UTXOEntry.Amount <= feePerInput {
|
||||
|
@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
|
||||
"github.com/kaspanet/kaspad/cmd/kaspawallet/daemon/pb"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func (s *server) Send(_ context.Context, request *pb.SendRequest) (*pb.SendResponse, error) {
|
||||
@ -11,7 +12,7 @@ func (s *server) Send(_ context.Context, request *pb.SendRequest) (*pb.SendRespo
|
||||
defer s.lock.Unlock()
|
||||
|
||||
unsignedTransactions, err := s.createUnsignedTransactions(request.ToAddress, request.Amount, request.IsSendAll,
|
||||
request.From, request.UseExistingChangeAddress)
|
||||
request.From, request.UseExistingChangeAddress, request.FeePolicy)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -24,7 +25,7 @@ func (s *server) Send(_ context.Context, request *pb.SendRequest) (*pb.SendRespo
|
||||
|
||||
txIDs, err := s.broadcast(signedTransactions, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, errors.Wrapf(err, "error broadcasting transactions %s", EncodeTransactionsToHex(signedTransactions))
|
||||
}
|
||||
|
||||
return &pb.SendResponse{TxIDs: txIDs, SignedTransactions: signedTransactions}, nil
|
||||
|
@ -37,6 +37,7 @@ type server struct {
|
||||
|
||||
lock sync.RWMutex
|
||||
utxosSortedByAmount []*walletUTXO
|
||||
mempoolExcludedUTXOs map[externalapi.DomainOutpoint]*walletUTXO
|
||||
nextSyncStartIndex uint32
|
||||
keysFile *keys.File
|
||||
shutdown chan struct{}
|
||||
@ -111,6 +112,7 @@ func Start(params *dagconfig.Params, listen, rpcServer string, keysFilePath stri
|
||||
params: params,
|
||||
coinbaseMaturity: coinbaseMaturity,
|
||||
utxosSortedByAmount: []*walletUTXO{},
|
||||
mempoolExcludedUTXOs: map[externalapi.DomainOutpoint]*walletUTXO{},
|
||||
nextSyncStartIndex: 0,
|
||||
keysFile: keysFile,
|
||||
shutdown: make(chan struct{}),
|
||||
|
@ -12,6 +12,7 @@ import (
|
||||
"github.com/kaspanet/kaspad/domain/consensus/utils/utxo"
|
||||
"github.com/kaspanet/kaspad/domain/miningmanager/mempool"
|
||||
"github.com/kaspanet/kaspad/util"
|
||||
"github.com/kaspanet/kaspad/util/txmass"
|
||||
)
|
||||
|
||||
// maybeAutoCompoundTransaction checks if a transaction's mass is higher that what is allowed for a standard
|
||||
@ -20,14 +21,10 @@ import (
|
||||
// into a change address.
|
||||
// An additional `mergeTransaction` is generated - which merges the outputs of the above splits into a single output
|
||||
// paying to the original transaction's payee.
|
||||
func (s *server) maybeAutoCompoundTransaction(transactionBytes []byte, toAddress util.Address,
|
||||
changeAddress util.Address, changeWalletAddress *walletAddress) ([][]byte, error) {
|
||||
transaction, err := serialization.DeserializePartiallySignedTransaction(transactionBytes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
func (s *server) maybeAutoCompoundTransaction(transaction *serialization.PartiallySignedTransaction, toAddress util.Address,
|
||||
changeAddress util.Address, changeWalletAddress *walletAddress, feeRate float64, maxFee uint64) ([][]byte, error) {
|
||||
|
||||
splitTransactions, err := s.maybeSplitAndMergeTransaction(transaction, toAddress, changeAddress, changeWalletAddress)
|
||||
splitTransactions, err := s.maybeSplitAndMergeTransaction(transaction, toAddress, changeAddress, changeWalletAddress, feeRate, maxFee)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -47,6 +44,8 @@ func (s *server) mergeTransaction(
|
||||
toAddress util.Address,
|
||||
changeAddress util.Address,
|
||||
changeWalletAddress *walletAddress,
|
||||
feeRate float64,
|
||||
maxFee uint64,
|
||||
) (*serialization.PartiallySignedTransaction, error) {
|
||||
numOutputs := len(originalTransaction.Tx.Outputs)
|
||||
if numOutputs > 2 || numOutputs == 0 {
|
||||
@ -71,13 +70,19 @@ func (s *server) mergeTransaction(
|
||||
DerivationPath: s.walletAddressPath(changeWalletAddress),
|
||||
}
|
||||
totalValue += output.Value
|
||||
totalValue -= feePerInput
|
||||
}
|
||||
// We're overestimating a bit by assuming that any transaction will have a change output
|
||||
fee, err := s.estimateFee(utxos, feeRate, maxFee, sentValue)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
totalValue -= fee
|
||||
|
||||
if totalValue < sentValue {
|
||||
// sometimes the fees from compound transactions make the total output higher than what's available from selected
|
||||
// utxos, in such cases - find one more UTXO and use it.
|
||||
additionalUTXOs, totalValueAdded, err := s.moreUTXOsForMergeTransaction(utxos, sentValue-totalValue)
|
||||
additionalUTXOs, totalValueAdded, err := s.moreUTXOsForMergeTransaction(utxos, sentValue-totalValue, feeRate)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -96,19 +101,54 @@ func (s *server) mergeTransaction(
|
||||
})
|
||||
}
|
||||
|
||||
mergeTransactionBytes, err := libkaspawallet.CreateUnsignedTransaction(s.keysFile.ExtendedPublicKeys,
|
||||
return libkaspawallet.CreateUnsignedTransaction(s.keysFile.ExtendedPublicKeys,
|
||||
s.keysFile.MinimumSignatures, payments, utxos)
|
||||
}
|
||||
|
||||
func (s *server) transactionFeeRate(psTx *serialization.PartiallySignedTransaction) (float64, error) {
|
||||
totalOuts := 0
|
||||
for _, output := range psTx.Tx.Outputs {
|
||||
totalOuts += int(output.Value)
|
||||
}
|
||||
|
||||
totalIns := 0
|
||||
for _, input := range psTx.PartiallySignedInputs {
|
||||
totalIns += int(input.PrevOutput.Value)
|
||||
}
|
||||
|
||||
if totalIns < totalOuts {
|
||||
return 0, errors.Errorf("Transaction don't have enough funds to pay for the outputs")
|
||||
}
|
||||
fee := totalIns - totalOuts
|
||||
mass, err := s.estimateComputeMassAfterSignatures(psTx)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return float64(fee) / float64(mass), nil
|
||||
}
|
||||
|
||||
func (s *server) checkTransactionFeeRate(psTx *serialization.PartiallySignedTransaction, maxFee uint64) error {
|
||||
feeRate, err := s.transactionFeeRate(psTx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if feeRate < 1 {
|
||||
return errors.Errorf("setting --max-fee to %d results in a fee rate of %f, which is below the minimum allowed fee rate of 1 sompi/gram", maxFee, feeRate)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *server) maybeSplitAndMergeTransaction(transaction *serialization.PartiallySignedTransaction, toAddress util.Address,
|
||||
changeAddress util.Address, changeWalletAddress *walletAddress, feeRate float64, maxFee uint64) ([]*serialization.PartiallySignedTransaction, error) {
|
||||
|
||||
err := s.checkTransactionFeeRate(transaction, maxFee)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return serialization.DeserializePartiallySignedTransaction(mergeTransactionBytes)
|
||||
}
|
||||
|
||||
func (s *server) maybeSplitAndMergeTransaction(transaction *serialization.PartiallySignedTransaction, toAddress util.Address,
|
||||
changeAddress util.Address, changeWalletAddress *walletAddress) ([]*serialization.PartiallySignedTransaction, error) {
|
||||
|
||||
transactionMass, err := s.estimateMassAfterSignatures(transaction)
|
||||
transactionMass, err := s.estimateComputeMassAfterSignatures(transaction)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -117,7 +157,7 @@ func (s *server) maybeSplitAndMergeTransaction(transaction *serialization.Partia
|
||||
return []*serialization.PartiallySignedTransaction{transaction}, nil
|
||||
}
|
||||
|
||||
splitCount, inputCountPerSplit, err := s.splitAndInputPerSplitCounts(transaction, transactionMass, changeAddress)
|
||||
splitCount, inputCountPerSplit, err := s.splitAndInputPerSplitCounts(transaction, transactionMass, changeAddress, feeRate, maxFee)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -127,19 +167,24 @@ func (s *server) maybeSplitAndMergeTransaction(transaction *serialization.Partia
|
||||
startIndex := i * inputCountPerSplit
|
||||
endIndex := startIndex + inputCountPerSplit
|
||||
var err error
|
||||
splitTransactions[i], err = s.createSplitTransaction(transaction, changeAddress, startIndex, endIndex)
|
||||
splitTransactions[i], err = s.createSplitTransaction(transaction, changeAddress, startIndex, endIndex, feeRate, maxFee)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = s.checkTransactionFeeRate(splitTransactions[i], maxFee)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if len(splitTransactions) > 1 {
|
||||
mergeTransaction, err := s.mergeTransaction(splitTransactions, transaction, toAddress, changeAddress, changeWalletAddress)
|
||||
mergeTransaction, err := s.mergeTransaction(splitTransactions, transaction, toAddress, changeAddress, changeWalletAddress, feeRate, maxFee)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Recursion will be 2-3 iterations deep even in the rarest` cases, so considered safe..
|
||||
splitMergeTransaction, err := s.maybeSplitAndMergeTransaction(mergeTransaction, toAddress, changeAddress, changeWalletAddress)
|
||||
splitMergeTransaction, err := s.maybeSplitAndMergeTransaction(mergeTransaction, toAddress, changeAddress, changeWalletAddress, feeRate, maxFee)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -152,7 +197,7 @@ func (s *server) maybeSplitAndMergeTransaction(transaction *serialization.Partia
|
||||
|
||||
// splitAndInputPerSplitCounts calculates the number of splits to create, and the number of inputs to assign per split.
|
||||
func (s *server) splitAndInputPerSplitCounts(transaction *serialization.PartiallySignedTransaction, transactionMass uint64,
|
||||
changeAddress util.Address) (splitCount, inputsPerSplitCount int, err error) {
|
||||
changeAddress util.Address, feeRate float64, maxFee uint64) (splitCount, inputsPerSplitCount int, err error) {
|
||||
|
||||
// Create a dummy transaction which is a clone of the original transaction, but without inputs,
|
||||
// to calculate how much mass do all the inputs have
|
||||
@ -172,7 +217,7 @@ func (s *server) splitAndInputPerSplitCounts(transaction *serialization.Partiall
|
||||
|
||||
// Create another dummy transaction, this time one similar to the split transactions we wish to generate,
|
||||
// but with 0 inputs, to calculate how much mass for inputs do we have available in the split transactions
|
||||
splitTransactionWithoutInputs, err := s.createSplitTransaction(transaction, changeAddress, 0, 0)
|
||||
splitTransactionWithoutInputs, err := s.createSplitTransaction(transaction, changeAddress, 0, 0, feeRate, maxFee)
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
@ -190,7 +235,7 @@ func (s *server) splitAndInputPerSplitCounts(transaction *serialization.Partiall
|
||||
}
|
||||
|
||||
func (s *server) createSplitTransaction(transaction *serialization.PartiallySignedTransaction,
|
||||
changeAddress util.Address, startIndex int, endIndex int) (*serialization.PartiallySignedTransaction, error) {
|
||||
changeAddress util.Address, startIndex int, endIndex int, feeRate float64, maxFee uint64) (*serialization.PartiallySignedTransaction, error) {
|
||||
|
||||
selectedUTXOs := make([]*libkaspawallet.UTXO, 0, endIndex-startIndex)
|
||||
totalSompi := uint64(0)
|
||||
@ -206,25 +251,36 @@ func (s *server) createSplitTransaction(transaction *serialization.PartiallySign
|
||||
})
|
||||
|
||||
totalSompi += selectedUTXOs[i-startIndex].UTXOEntry.Amount()
|
||||
totalSompi -= feePerInput
|
||||
}
|
||||
unsignedTransactionBytes, err := libkaspawallet.CreateUnsignedTransaction(s.keysFile.ExtendedPublicKeys,
|
||||
if len(selectedUTXOs) != 0 {
|
||||
fee, err := s.estimateFee(selectedUTXOs, feeRate, maxFee, totalSompi)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
totalSompi -= fee
|
||||
}
|
||||
|
||||
return libkaspawallet.CreateUnsignedTransaction(s.keysFile.ExtendedPublicKeys,
|
||||
s.keysFile.MinimumSignatures,
|
||||
[]*libkaspawallet.Payment{{
|
||||
Address: changeAddress,
|
||||
Amount: totalSompi,
|
||||
}}, selectedUTXOs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return serialization.DeserializePartiallySignedTransaction(unsignedTransactionBytes)
|
||||
}
|
||||
|
||||
func (s *server) estimateMassAfterSignatures(transaction *serialization.PartiallySignedTransaction) (uint64, error) {
|
||||
return EstimateMassAfterSignatures(transaction, s.keysFile.ECDSA, s.keysFile.MinimumSignatures, s.txMassCalculator)
|
||||
}
|
||||
|
||||
func (s *server) estimateComputeMassAfterSignatures(transaction *serialization.PartiallySignedTransaction) (uint64, error) {
|
||||
return estimateComputeMassAfterSignatures(transaction, s.keysFile.ECDSA, s.keysFile.MinimumSignatures, s.txMassCalculator)
|
||||
}
|
||||
|
||||
func createTransactionWithJunkFieldsForMassCalculation(transaction *serialization.PartiallySignedTransaction, ecdsa bool, minimumSignatures uint32, txMassCalculator *txmass.Calculator) (*externalapi.DomainTransaction, error) {
|
||||
transaction = transaction.Clone()
|
||||
var signatureSize uint64
|
||||
if s.keysFile.ECDSA {
|
||||
if ecdsa {
|
||||
signatureSize = secp256k1.SerializedECDSASignatureSize
|
||||
} else {
|
||||
signatureSize = secp256k1.SerializedSchnorrSignatureSize
|
||||
@ -232,7 +288,7 @@ func (s *server) estimateMassAfterSignatures(transaction *serialization.Partiall
|
||||
|
||||
for i, input := range transaction.PartiallySignedInputs {
|
||||
for j, pubKeyPair := range input.PubKeySignaturePairs {
|
||||
if uint32(j) >= s.keysFile.MinimumSignatures {
|
||||
if uint32(j) >= minimumSignatures {
|
||||
break
|
||||
}
|
||||
pubKeyPair.Signature = make([]byte, signatureSize+1) // +1 for SigHashType
|
||||
@ -240,15 +296,28 @@ func (s *server) estimateMassAfterSignatures(transaction *serialization.Partiall
|
||||
transaction.Tx.Inputs[i].SigOpCount = byte(len(input.PubKeySignaturePairs))
|
||||
}
|
||||
|
||||
transactionWithSignatures, err := libkaspawallet.ExtractTransactionDeserialized(transaction, s.keysFile.ECDSA)
|
||||
return libkaspawallet.ExtractTransactionDeserialized(transaction, ecdsa)
|
||||
}
|
||||
|
||||
func estimateComputeMassAfterSignatures(transaction *serialization.PartiallySignedTransaction, ecdsa bool, minimumSignatures uint32, txMassCalculator *txmass.Calculator) (uint64, error) {
|
||||
transactionWithSignatures, err := createTransactionWithJunkFieldsForMassCalculation(transaction, ecdsa, minimumSignatures, txMassCalculator)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return s.txMassCalculator.CalculateTransactionMass(transactionWithSignatures), nil
|
||||
return txMassCalculator.CalculateTransactionMass(transactionWithSignatures), nil
|
||||
}
|
||||
|
||||
func (s *server) moreUTXOsForMergeTransaction(alreadySelectedUTXOs []*libkaspawallet.UTXO, requiredAmount uint64) (
|
||||
func EstimateMassAfterSignatures(transaction *serialization.PartiallySignedTransaction, ecdsa bool, minimumSignatures uint32, txMassCalculator *txmass.Calculator) (uint64, error) {
|
||||
transactionWithSignatures, err := createTransactionWithJunkFieldsForMassCalculation(transaction, ecdsa, minimumSignatures, txMassCalculator)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return txMassCalculator.CalculateTransactionOverallMass(transactionWithSignatures), nil
|
||||
}
|
||||
|
||||
func (s *server) moreUTXOsForMergeTransaction(alreadySelectedUTXOs []*libkaspawallet.UTXO, requiredAmount uint64, feeRate float64) (
|
||||
additionalUTXOs []*libkaspawallet.UTXO, totalValueAdded uint64, err error) {
|
||||
|
||||
dagInfo, err := s.rpcClient.GetBlockDAGInfo()
|
||||
@ -260,6 +329,11 @@ func (s *server) moreUTXOsForMergeTransaction(alreadySelectedUTXOs []*libkaspawa
|
||||
alreadySelectedUTXOsMap[*alreadySelectedUTXO.Outpoint] = struct{}{}
|
||||
}
|
||||
|
||||
feePerInput, err := s.estimateFeePerInput(feeRate)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
for _, utxo := range s.utxosSortedByAmount {
|
||||
if _, ok := alreadySelectedUTXOsMap[*utxo.Outpoint]; ok {
|
||||
continue
|
||||
|
@ -20,9 +20,9 @@ import (
|
||||
"github.com/kaspanet/kaspad/domain/consensus/utils/testutils"
|
||||
)
|
||||
|
||||
func TestEstimateMassAfterSignatures(t *testing.T) {
|
||||
func TestEstimateComputeMassAfterSignatures(t *testing.T) {
|
||||
testutils.ForAllNets(t, true, func(t *testing.T, consensusConfig *consensus.Config) {
|
||||
unsignedTransactionBytes, mnemonics, params, teardown := testEstimateMassIncreaseForSignaturesSetUp(t, consensusConfig)
|
||||
unsignedTransaction, mnemonics, params, teardown := testEstimateMassIncreaseForSignaturesSetUp(t, consensusConfig)
|
||||
defer teardown(false)
|
||||
|
||||
serverInstance := &server{
|
||||
@ -33,14 +33,14 @@ func TestEstimateMassAfterSignatures(t *testing.T) {
|
||||
txMassCalculator: txmass.NewCalculator(params.MassPerTxByte, params.MassPerScriptPubKeyByte, params.MassPerSigOp),
|
||||
}
|
||||
|
||||
unsignedTransaction, err := serialization.DeserializePartiallySignedTransaction(unsignedTransactionBytes)
|
||||
estimatedMassAfterSignatures, err := serverInstance.estimateComputeMassAfterSignatures(unsignedTransaction)
|
||||
if err != nil {
|
||||
t.Fatalf("Error deserializing unsignedTransaction: %s", err)
|
||||
t.Fatalf("Error from estimateComputeMassAfterSignatures: %s", err)
|
||||
}
|
||||
|
||||
estimatedMassAfterSignatures, err := serverInstance.estimateMassAfterSignatures(unsignedTransaction)
|
||||
unsignedTransactionBytes, err := serialization.SerializePartiallySignedTransaction(unsignedTransaction)
|
||||
if err != nil {
|
||||
t.Fatalf("Error from estimateMassAfterSignatures: %s", err)
|
||||
t.Fatalf("Error deserializing unsignedTransaction: %s", err)
|
||||
}
|
||||
|
||||
signedTxStep1Bytes, err := libkaspawallet.Sign(params, mnemonics[:1], unsignedTransactionBytes, false)
|
||||
@ -67,8 +67,67 @@ func TestEstimateMassAfterSignatures(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestEstimateMassAfterSignatures(t *testing.T) {
|
||||
testutils.ForAllNets(t, true, func(t *testing.T, consensusConfig *consensus.Config) {
|
||||
unsignedTransaction, mnemonics, params, teardown := testEstimateMassIncreaseForSignaturesSetUp(t, consensusConfig)
|
||||
defer teardown(false)
|
||||
|
||||
for i := range unsignedTransaction.Tx.Inputs {
|
||||
unsignedTransaction.Tx.Inputs[i].UTXOEntry = utxo.NewUTXOEntry(1, &externalapi.ScriptPublicKey{}, false, 0)
|
||||
unsignedTransaction.PartiallySignedInputs[i].PrevOutput = &externalapi.DomainTransactionOutput{
|
||||
Value: 1,
|
||||
ScriptPublicKey: &externalapi.ScriptPublicKey{},
|
||||
}
|
||||
}
|
||||
|
||||
serverInstance := &server{
|
||||
params: params,
|
||||
keysFile: &keys.File{MinimumSignatures: 2},
|
||||
shutdown: make(chan struct{}),
|
||||
addressSet: make(walletAddressSet),
|
||||
txMassCalculator: txmass.NewCalculator(params.MassPerTxByte, params.MassPerScriptPubKeyByte, params.MassPerSigOp),
|
||||
}
|
||||
|
||||
estimatedMassAfterSignatures, err := serverInstance.estimateMassAfterSignatures(unsignedTransaction)
|
||||
if err != nil {
|
||||
t.Fatalf("Error from estimateMassAfterSignatures: %s", err)
|
||||
}
|
||||
|
||||
unsignedTransactionBytes, err := serialization.SerializePartiallySignedTransaction(unsignedTransaction)
|
||||
if err != nil {
|
||||
t.Fatalf("Error deserializing unsignedTransaction: %s", err)
|
||||
}
|
||||
|
||||
signedTxStep1Bytes, err := libkaspawallet.Sign(params, mnemonics[:1], unsignedTransactionBytes, false)
|
||||
if err != nil {
|
||||
t.Fatalf("Sign: %+v", err)
|
||||
}
|
||||
|
||||
signedTxStep2Bytes, err := libkaspawallet.Sign(params, mnemonics[1:2], signedTxStep1Bytes, false)
|
||||
if err != nil {
|
||||
t.Fatalf("Sign: %+v", err)
|
||||
}
|
||||
|
||||
extractedSignedTx, err := libkaspawallet.ExtractTransaction(signedTxStep2Bytes, false)
|
||||
if err != nil {
|
||||
t.Fatalf("ExtractTransaction: %+v", err)
|
||||
}
|
||||
|
||||
for i := range extractedSignedTx.Inputs {
|
||||
extractedSignedTx.Inputs[i].UTXOEntry = utxo.NewUTXOEntry(1, &externalapi.ScriptPublicKey{}, false, 0)
|
||||
}
|
||||
|
||||
actualMassAfterSignatures := serverInstance.txMassCalculator.CalculateTransactionOverallMass(extractedSignedTx)
|
||||
|
||||
if estimatedMassAfterSignatures != actualMassAfterSignatures {
|
||||
t.Errorf("Estimated mass after signatures: %d but actually got %d",
|
||||
estimatedMassAfterSignatures, actualMassAfterSignatures)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func testEstimateMassIncreaseForSignaturesSetUp(t *testing.T, consensusConfig *consensus.Config) (
|
||||
[]byte, []string, *dagconfig.Params, func(keepDataDir bool)) {
|
||||
*serialization.PartiallySignedTransaction, []string, *dagconfig.Params, func(keepDataDir bool)) {
|
||||
|
||||
consensusConfig.BlockCoinbaseMaturity = 0
|
||||
params := &consensusConfig.Params
|
||||
|
@ -6,6 +6,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/kaspanet/kaspad/cmd/kaspawallet/libkaspawallet"
|
||||
"github.com/kaspanet/kaspad/domain/consensus/model/externalapi"
|
||||
|
||||
"github.com/kaspanet/kaspad/app/appmessage"
|
||||
"github.com/pkg/errors"
|
||||
@ -240,11 +241,8 @@ func (s *server) updateUTXOSet(entries []*appmessage.UTXOsByAddressesEntry, memp
|
||||
}
|
||||
}
|
||||
|
||||
mempoolExcludedUTXOs := make(map[externalapi.DomainOutpoint]*walletUTXO)
|
||||
for _, entry := range entries {
|
||||
if _, ok := exclude[*entry.Outpoint]; ok {
|
||||
continue
|
||||
}
|
||||
|
||||
outpoint, err := appmessage.RPCOutpointToDomainOutpoint(entry.Outpoint)
|
||||
if err != nil {
|
||||
return err
|
||||
@ -260,11 +258,22 @@ func (s *server) updateUTXOSet(entries []*appmessage.UTXOsByAddressesEntry, memp
|
||||
if !ok {
|
||||
return errors.Errorf("Got result from address %s even though it wasn't requested", entry.Address)
|
||||
}
|
||||
utxos = append(utxos, &walletUTXO{
|
||||
|
||||
utxo := &walletUTXO{
|
||||
Outpoint: outpoint,
|
||||
UTXOEntry: utxoEntry,
|
||||
address: address,
|
||||
})
|
||||
}
|
||||
|
||||
if _, ok := exclude[*entry.Outpoint]; ok {
|
||||
mempoolExcludedUTXOs[*outpoint] = utxo
|
||||
} else {
|
||||
utxos = append(utxos, &walletUTXO{
|
||||
Outpoint: outpoint,
|
||||
UTXOEntry: utxoEntry,
|
||||
address: address,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
sort.Slice(utxos, func(i, j int) bool { return utxos[i].UTXOEntry.Amount() > utxos[j].UTXOEntry.Amount() })
|
||||
@ -272,6 +281,7 @@ func (s *server) updateUTXOSet(entries []*appmessage.UTXOsByAddressesEntry, memp
|
||||
s.lock.Lock()
|
||||
s.startTimeOfLastCompletedRefresh = refreshStart
|
||||
s.utxosSortedByAmount = utxos
|
||||
s.mempoolExcludedUTXOs = mempoolExcludedUTXOs
|
||||
|
||||
// Cleanup expired used outpoints to avoid a memory leak
|
||||
for outpoint, broadcastTime := range s.usedOutpoints {
|
||||
|
@ -1,4 +1,4 @@
|
||||
package main
|
||||
package server
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
@ -9,7 +9,7 @@ import (
|
||||
// We use a separator that is not in the hex alphabet, but which will not split selection with a double click
|
||||
const hexTransactionsSeparator = "_"
|
||||
|
||||
func encodeTransactionsToHex(transactions [][]byte) string {
|
||||
func EncodeTransactionsToHex(transactions [][]byte) string {
|
||||
transactionsInHex := make([]string, len(transactions))
|
||||
for i, transaction := range transactions {
|
||||
transactionsInHex[i] = hex.EncodeToString(transaction)
|
||||
@ -17,7 +17,7 @@ func encodeTransactionsToHex(transactions [][]byte) string {
|
||||
return strings.Join(transactionsInHex, hexTransactionsSeparator)
|
||||
}
|
||||
|
||||
func decodeTransactionsFromHex(transactionsHex string) ([][]byte, error) {
|
||||
func DecodeTransactionsFromHex(transactionsHex string) ([][]byte, error) {
|
||||
splitTransactionsHexes := strings.Split(transactionsHex, hexTransactionsSeparator)
|
||||
transactions := make([][]byte, len(splitTransactionsHexes))
|
||||
|
@ -1,5 +1,5 @@
|
||||
# -- multistage docker build: stage #1: build stage
|
||||
FROM golang:1.18-alpine AS build
|
||||
FROM golang:1.23-alpine AS build
|
||||
|
||||
RUN mkdir -p /go/src/github.com/kaspanet/kaspad
|
||||
|
||||
|
@ -7,6 +7,7 @@ import (
|
||||
"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"
|
||||
)
|
||||
@ -31,15 +32,10 @@ func CreateUnsignedTransaction(
|
||||
extendedPublicKeys []string,
|
||||
minimumSignatures uint32,
|
||||
payments []*Payment,
|
||||
selectedUTXOs []*UTXO) ([]byte, error) {
|
||||
selectedUTXOs []*UTXO) (*serialization.PartiallySignedTransaction, error) {
|
||||
|
||||
sortPublicKeys(extendedPublicKeys)
|
||||
unsignedTransaction, err := createUnsignedTransaction(extendedPublicKeys, minimumSignatures, payments, selectedUTXOs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return serialization.SerializePartiallySignedTransaction(unsignedTransaction)
|
||||
return createUnsignedTransaction(extendedPublicKeys, minimumSignatures, payments, selectedUTXOs)
|
||||
}
|
||||
|
||||
func multiSigRedeemScript(extendedPublicKeys []string, minimumSignatures uint32, path string, ecdsa bool) ([]byte, error) {
|
||||
@ -247,6 +243,12 @@ func ExtractTransactionDeserialized(partiallySignedTransaction *serialization.Pa
|
||||
}
|
||||
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
|
||||
}
|
||||
|
@ -2,6 +2,7 @@ package libkaspawallet_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/kaspanet/kaspad/cmd/kaspawallet/libkaspawallet/serialization"
|
||||
"github.com/kaspanet/kaspad/domain/consensus/utils/constants"
|
||||
"strings"
|
||||
"testing"
|
||||
@ -26,6 +27,20 @@ func forSchnorrAndECDSA(t *testing.T, testFunc func(t *testing.T, ecdsa bool)) {
|
||||
})
|
||||
}
|
||||
|
||||
func createUnsignedTransactionSerialized(
|
||||
extendedPublicKeys []string,
|
||||
minimumSignatures uint32,
|
||||
payments []*libkaspawallet.Payment,
|
||||
selectedUTXOs []*libkaspawallet.UTXO) ([]byte, error) {
|
||||
|
||||
tx, err := libkaspawallet.CreateUnsignedTransaction(extendedPublicKeys, minimumSignatures, payments, selectedUTXOs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return serialization.SerializePartiallySignedTransaction(tx)
|
||||
}
|
||||
|
||||
func TestMultisig(t *testing.T) {
|
||||
testutils.ForAllNets(t, true, func(t *testing.T, consensusConfig *consensus.Config) {
|
||||
params := &consensusConfig.Params
|
||||
@ -102,7 +117,7 @@ func TestMultisig(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
unsignedTransaction, err := libkaspawallet.CreateUnsignedTransaction(publicKeys, minimumSignatures,
|
||||
unsignedTransaction, err := createUnsignedTransactionSerialized(publicKeys, minimumSignatures,
|
||||
[]*libkaspawallet.Payment{{
|
||||
Address: address,
|
||||
Amount: 10,
|
||||
@ -263,7 +278,7 @@ func TestP2PK(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
unsignedTransaction, err := libkaspawallet.CreateUnsignedTransaction(publicKeys, minimumSignatures,
|
||||
unsignedTransaction, err := createUnsignedTransactionSerialized(publicKeys, minimumSignatures,
|
||||
[]*libkaspawallet.Payment{{
|
||||
Address: address,
|
||||
Amount: 10,
|
||||
@ -425,7 +440,7 @@ func TestMaxSompi(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
unsignedTxWithLargeInputAmount, err := libkaspawallet.CreateUnsignedTransaction(publicKeys, minimumSignatures,
|
||||
unsignedTxWithLargeInputAmount, err := createUnsignedTransactionSerialized(publicKeys, minimumSignatures,
|
||||
[]*libkaspawallet.Payment{{
|
||||
Address: address,
|
||||
Amount: 10,
|
||||
@ -476,7 +491,7 @@ func TestMaxSompi(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
unsignedTxWithLargeInputAndOutputAmount, err := libkaspawallet.CreateUnsignedTransaction(publicKeys, minimumSignatures,
|
||||
unsignedTxWithLargeInputAndOutputAmount, err := createUnsignedTransactionSerialized(publicKeys, minimumSignatures,
|
||||
[]*libkaspawallet.Payment{{
|
||||
Address: address,
|
||||
Amount: 22e6 * constants.SompiPerKaspa,
|
||||
|
@ -20,6 +20,8 @@ func main() {
|
||||
err = sign(config.(*signConfig))
|
||||
case broadcastSubCmd:
|
||||
err = broadcast(config.(*broadcastConfig))
|
||||
case broadcastReplacementSubCmd:
|
||||
err = broadcastReplacement(config.(*broadcastConfig))
|
||||
case parseSubCmd:
|
||||
err = parse(config.(*parseConfig))
|
||||
case showAddressesSubCmd:
|
||||
@ -36,6 +38,10 @@ func main() {
|
||||
showVersion()
|
||||
case getDaemonVersionSubCmd:
|
||||
err = getDaemonVersion(config.(*getDaemonVersionConfig))
|
||||
case bumpFeeSubCmd:
|
||||
err = bumpFee(config.(*bumpFeeConfig))
|
||||
case bumpFeeUnsignedSubCmd:
|
||||
err = bumpFeeUnsigned(config.(*bumpFeeUnsignedConfig))
|
||||
default:
|
||||
err = errors.Errorf("Unknown sub-command '%s'\n", subCmd)
|
||||
}
|
||||
|
@ -3,13 +3,17 @@ package main
|
||||
import (
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"strings"
|
||||
|
||||
"github.com/kaspanet/kaspad/cmd/kaspawallet/daemon/server"
|
||||
"github.com/kaspanet/kaspad/cmd/kaspawallet/keys"
|
||||
"github.com/kaspanet/kaspad/cmd/kaspawallet/libkaspawallet/serialization"
|
||||
"github.com/kaspanet/kaspad/domain/consensus/utils/consensushashing"
|
||||
"github.com/kaspanet/kaspad/domain/consensus/utils/constants"
|
||||
"github.com/kaspanet/kaspad/domain/consensus/utils/txscript"
|
||||
"github.com/kaspanet/kaspad/util/txmass"
|
||||
"github.com/pkg/errors"
|
||||
"io/ioutil"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func parse(conf *parseConfig) error {
|
||||
@ -20,6 +24,11 @@ func parse(conf *parseConfig) error {
|
||||
return errors.Errorf("Both --transaction and --transaction-file cannot be passed at the same time")
|
||||
}
|
||||
|
||||
keysFile, err := keys.ReadKeysFile(conf.NetParams(), conf.KeysFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
transactionHex := conf.Transaction
|
||||
if conf.TransactionFile != "" {
|
||||
transactionHexBytes, err := ioutil.ReadFile(conf.TransactionFile)
|
||||
@ -29,10 +38,12 @@ func parse(conf *parseConfig) error {
|
||||
transactionHex = strings.TrimSpace(string(transactionHexBytes))
|
||||
}
|
||||
|
||||
transactions, err := decodeTransactionsFromHex(transactionHex)
|
||||
transactions, err := server.DecodeTransactionsFromHex(transactionHex)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
txMassCalculator := txmass.NewCalculator(conf.NetParams().MassPerTxByte, conf.NetParams().MassPerScriptPubKeyByte, conf.NetParams().MassPerSigOp)
|
||||
for i, transaction := range transactions {
|
||||
|
||||
partiallySignedTransaction, err := serialization.DeserializePartiallySignedTransaction(transaction)
|
||||
@ -78,7 +89,16 @@ func parse(conf *parseConfig) error {
|
||||
}
|
||||
fmt.Println()
|
||||
|
||||
fmt.Printf("Fee:\t%d Sompi\n\n", allInputSompi-allOutputSompi)
|
||||
fee := allInputSompi - allOutputSompi
|
||||
fmt.Printf("Fee:\t%d Sompi (%f KAS)\n", fee, float64(fee)/float64(constants.SompiPerKaspa))
|
||||
mass, err := server.EstimateMassAfterSignatures(partiallySignedTransaction, keysFile.ECDSA, keysFile.MinimumSignatures, txMassCalculator)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Printf("Mass: %d grams\n", mass)
|
||||
feeRate := float64(fee) / float64(mass)
|
||||
fmt.Printf("Fee rate: %.2f Sompi/Gram\n", feeRate)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
@ -42,6 +42,23 @@ func send(conf *sendConfig) error {
|
||||
}
|
||||
}
|
||||
|
||||
var feePolicy *pb.FeePolicy
|
||||
if conf.FeeRate > 0 {
|
||||
feePolicy = &pb.FeePolicy{
|
||||
FeePolicy: &pb.FeePolicy_ExactFeeRate{
|
||||
ExactFeeRate: conf.FeeRate,
|
||||
},
|
||||
}
|
||||
} else if conf.MaxFeeRate > 0 {
|
||||
feePolicy = &pb.FeePolicy{
|
||||
FeePolicy: &pb.FeePolicy_MaxFeeRate{MaxFeeRate: conf.MaxFeeRate},
|
||||
}
|
||||
} else if conf.MaxFee > 0 {
|
||||
feePolicy = &pb.FeePolicy{
|
||||
FeePolicy: &pb.FeePolicy_MaxFee{MaxFee: conf.MaxFee},
|
||||
}
|
||||
}
|
||||
|
||||
createUnsignedTransactionsResponse, err :=
|
||||
daemonClient.CreateUnsignedTransactions(ctx, &pb.CreateUnsignedTransactionsRequest{
|
||||
From: conf.FromAddresses,
|
||||
@ -49,6 +66,7 @@ func send(conf *sendConfig) error {
|
||||
Amount: sendAmountSompi,
|
||||
IsSendAll: conf.IsSendAll,
|
||||
UseExistingChangeAddress: conf.UseExistingChangeAddress,
|
||||
FeePolicy: feePolicy,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -6,6 +6,7 @@ import (
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/kaspanet/kaspad/cmd/kaspawallet/daemon/server"
|
||||
"github.com/kaspanet/kaspad/cmd/kaspawallet/keys"
|
||||
"github.com/kaspanet/kaspad/cmd/kaspawallet/libkaspawallet"
|
||||
"github.com/pkg/errors"
|
||||
@ -40,7 +41,7 @@ func sign(conf *signConfig) error {
|
||||
}
|
||||
transactionsHex = strings.TrimSpace(string(transactionHexBytes))
|
||||
}
|
||||
partiallySignedTransactions, err := decodeTransactionsFromHex(transactionsHex)
|
||||
partiallySignedTransactions, err := server.DecodeTransactionsFromHex(transactionsHex)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -72,6 +73,6 @@ func sign(conf *signConfig) error {
|
||||
fmt.Fprintln(os.Stderr, "Successfully signed transaction")
|
||||
}
|
||||
|
||||
fmt.Println(encodeTransactionsToHex(updatedPartiallySignedTransactions))
|
||||
fmt.Println(server.EncodeTransactionsToHex(updatedPartiallySignedTransactions))
|
||||
return nil
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
# -- multistage docker build: stage #1: build stage
|
||||
FROM golang:1.19-alpine AS build
|
||||
FROM golang:1.23-alpine AS build
|
||||
|
||||
RUN mkdir -p /go/src/github.com/kaspanet/kaspad
|
||||
|
||||
|
2
go.mod
2
go.mod
@ -1,6 +1,6 @@
|
||||
module github.com/kaspanet/kaspad
|
||||
|
||||
go 1.18
|
||||
go 1.23
|
||||
|
||||
require (
|
||||
github.com/btcsuite/btcutil v1.0.2
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -139,6 +139,28 @@ message KaspadMessage {
|
||||
GetMempoolEntriesByAddressesResponseMessage getMempoolEntriesByAddressesResponse = 1085;
|
||||
GetCoinSupplyRequestMessage getCoinSupplyRequest = 1086;
|
||||
GetCoinSupplyResponseMessage getCoinSupplyResponse= 1087;
|
||||
PingRequestMessage pingRequest = 1088;
|
||||
GetMetricsRequestMessage getMetricsRequest = 1090;
|
||||
GetServerInfoRequestMessage getServerInfoRequest = 1092;
|
||||
GetSyncStatusRequestMessage getSyncStatusRequest = 1094;
|
||||
GetDaaScoreTimestampEstimateRequestMessage getDaaScoreTimestampEstimateRequest = 1096;
|
||||
SubmitTransactionReplacementRequestMessage submitTransactionReplacementRequest = 1100;
|
||||
GetConnectionsRequestMessage getConnectionsRequest = 1102;
|
||||
GetSystemInfoRequestMessage getSystemInfoRequest = 1104;
|
||||
GetFeeEstimateRequestMessage getFeeEstimateRequest = 1106;
|
||||
GetFeeEstimateExperimentalRequestMessage getFeeEstimateExperimentalRequest = 1108;
|
||||
GetCurrentBlockColorRequestMessage getCurrentBlockColorRequest = 1110;
|
||||
PingResponseMessage pingResponse= 1089;
|
||||
GetMetricsResponseMessage getMetricsResponse= 1091;
|
||||
GetServerInfoResponseMessage getServerInfoResponse = 1093;
|
||||
GetSyncStatusResponseMessage getSyncStatusResponse = 1095;
|
||||
GetDaaScoreTimestampEstimateResponseMessage getDaaScoreTimestampEstimateResponse = 1097;
|
||||
SubmitTransactionReplacementResponseMessage submitTransactionReplacementResponse = 1101;
|
||||
GetConnectionsResponseMessage getConnectionsResponse= 1103;
|
||||
GetSystemInfoResponseMessage getSystemInfoResponse= 1105;
|
||||
GetFeeEstimateResponseMessage getFeeEstimateResponse = 1107;
|
||||
GetFeeEstimateExperimentalResponseMessage getFeeEstimateExperimentalResponse = 1109;
|
||||
GetCurrentBlockColorResponseMessage getCurrentBlockColorResponse = 1111;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
|
||||
// versions:
|
||||
// - protoc-gen-go-grpc v1.2.0
|
||||
// - protoc v3.17.2
|
||||
// - protoc v3.12.3
|
||||
// source: messages.proto
|
||||
|
||||
package protowire
|
||||
|
@ -1,7 +1,7 @@
|
||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-go v1.26.0
|
||||
// protoc v3.17.2
|
||||
// protoc-gen-go v1.28.1
|
||||
// protoc v3.12.3
|
||||
// source: p2p.proto
|
||||
|
||||
package protowire
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -721,3 +721,221 @@ message GetCoinSupplyResponseMessage{
|
||||
|
||||
RPCError error = 1000;
|
||||
}
|
||||
|
||||
message PingRequestMessage{
|
||||
}
|
||||
|
||||
message PingResponseMessage { RPCError error = 1000; }
|
||||
|
||||
message ProcessMetrics {
|
||||
uint64 residentSetSize = 1;
|
||||
uint64 virtualMemorySize = 2;
|
||||
uint32 coreNum = 3;
|
||||
float cpuUsage = 4;
|
||||
uint32 fdNum = 5;
|
||||
uint64 diskIoReadBytes = 6;
|
||||
uint64 diskIoWriteBytes = 7;
|
||||
float diskIoReadPerSec = 8;
|
||||
float diskIoWritePerSec = 9;
|
||||
}
|
||||
|
||||
message ConnectionMetrics {
|
||||
uint32 borshLiveConnections = 31;
|
||||
uint64 borshConnectionAttempts = 32;
|
||||
uint64 borshHandshakeFailures = 33;
|
||||
|
||||
uint32 jsonLiveConnections = 41;
|
||||
uint64 jsonConnectionAttempts = 42;
|
||||
uint64 jsonHandshakeFailures = 43;
|
||||
|
||||
uint32 activePeers = 51;
|
||||
}
|
||||
|
||||
message BandwidthMetrics {
|
||||
uint64 borshBytesTx = 61;
|
||||
uint64 borshBytesRx = 62;
|
||||
uint64 jsonBytesTx = 63;
|
||||
uint64 jsonBytesRx = 64;
|
||||
uint64 grpcP2pBytesTx = 65;
|
||||
uint64 grpcP2pBytesRx = 66;
|
||||
uint64 grpcUserBytesTx = 67;
|
||||
uint64 grpcUserBytesRx = 68;
|
||||
}
|
||||
|
||||
message ConsensusMetrics {
|
||||
uint64 blocksSubmitted = 1;
|
||||
uint64 headerCounts = 2;
|
||||
uint64 depCounts = 3;
|
||||
uint64 bodyCounts = 4;
|
||||
uint64 txsCounts = 5;
|
||||
uint64 chainBlockCounts = 6;
|
||||
uint64 massCounts = 7;
|
||||
|
||||
uint64 blockCount = 11;
|
||||
uint64 headerCount = 12;
|
||||
uint64 mempoolSize = 13;
|
||||
uint32 tipHashesCount = 14;
|
||||
double difficulty = 15;
|
||||
uint64 pastMedianTime = 16;
|
||||
uint32 virtualParentHashesCount = 17;
|
||||
uint64 virtualDaaScore = 18;
|
||||
}
|
||||
|
||||
message StorageMetrics { uint64 storageSizeBytes = 1; }
|
||||
|
||||
message GetConnectionsRequestMessage { bool includeProfileData = 1; }
|
||||
|
||||
message ConnectionsProfileData {
|
||||
double cpuUsage = 1;
|
||||
uint64 memoryUsage = 2;
|
||||
}
|
||||
|
||||
message GetConnectionsResponseMessage {
|
||||
uint32 clients = 1;
|
||||
uint32 peers = 2;
|
||||
ConnectionsProfileData profileData = 3;
|
||||
RPCError error = 1000;
|
||||
}
|
||||
|
||||
message GetSystemInfoRequestMessage {}
|
||||
|
||||
message GetSystemInfoResponseMessage {
|
||||
string version = 1;
|
||||
string systemId = 2;
|
||||
string gitHash = 3;
|
||||
uint32 coreNum = 4;
|
||||
uint64 totalMemory = 5;
|
||||
uint32 fdLimit = 6;
|
||||
RPCError error = 1000;
|
||||
}
|
||||
|
||||
message GetMetricsRequestMessage {
|
||||
bool processMetrics = 1;
|
||||
bool connectionMetrics = 2;
|
||||
bool bandwidthMetrics = 3;
|
||||
bool consensusMetrics = 4;
|
||||
bool storageMetrics = 5;
|
||||
bool customMetrics = 6;
|
||||
}
|
||||
|
||||
message GetMetricsResponseMessage {
|
||||
uint64 serverTime = 1;
|
||||
ProcessMetrics processMetrics = 11;
|
||||
ConnectionMetrics connectionMetrics = 12;
|
||||
BandwidthMetrics bandwidthMetrics = 13;
|
||||
ConsensusMetrics consensusMetrics = 14;
|
||||
StorageMetrics storageMetrics = 15;
|
||||
RPCError error = 1000;
|
||||
}
|
||||
|
||||
message GetServerInfoRequestMessage {}
|
||||
|
||||
message GetServerInfoResponseMessage {
|
||||
uint32 rpcApiVersion = 1;
|
||||
uint32 rpcApiRevision = 2;
|
||||
string serverVersion = 3;
|
||||
string networkId = 4;
|
||||
bool hasUtxoIndex = 5;
|
||||
bool isSynced = 6;
|
||||
uint64 virtualDaaScore = 7;
|
||||
RPCError error = 1000;
|
||||
}
|
||||
|
||||
message GetSyncStatusRequestMessage {}
|
||||
|
||||
message GetSyncStatusResponseMessage {
|
||||
bool isSynced = 1;
|
||||
RPCError error = 1000;
|
||||
}
|
||||
|
||||
message GetDaaScoreTimestampEstimateRequestMessage {
|
||||
repeated uint64 daaScores = 1;
|
||||
}
|
||||
|
||||
message GetDaaScoreTimestampEstimateResponseMessage{
|
||||
repeated uint64 timestamps = 1;
|
||||
RPCError error = 1000;
|
||||
}
|
||||
|
||||
message RpcFeerateBucket {
|
||||
// Fee/mass of a transaction in `sompi/gram` units
|
||||
double feerate = 1;
|
||||
double estimatedSeconds = 2;
|
||||
}
|
||||
|
||||
// Data required for making fee estimates.
|
||||
//
|
||||
// Feerate values represent fee/mass of a transaction in `sompi/gram` units.
|
||||
// Given a feerate value recommendation, calculate the required fee by
|
||||
// taking the transaction mass and multiplying it by feerate: `fee = feerate *
|
||||
// mass(tx)`
|
||||
message RpcFeeEstimate {
|
||||
// Top-priority feerate bucket. Provides an estimation of the feerate required
|
||||
// for sub-second DAG inclusion.
|
||||
RpcFeerateBucket priority_bucket = 1;
|
||||
|
||||
// A vector of *normal* priority feerate values. The first value of this
|
||||
// vector is guaranteed to exist and provide an estimation for sub-*minute*
|
||||
// DAG inclusion. All other values will have shorter estimation times than all
|
||||
// `lowBuckets` values. Therefor by chaining `[priority] | normal | low` and
|
||||
// interpolating between them, one can compose a complete feerate function on
|
||||
// the client side. The API makes an effort to sample enough "interesting"
|
||||
// points on the feerate-to-time curve, so that the interpolation is
|
||||
// meaningful.
|
||||
repeated RpcFeerateBucket normalBuckets = 2;
|
||||
|
||||
// A vector of *low* priority feerate values. The first value of this vector
|
||||
// is guaranteed to exist and provide an estimation for sub-*hour* DAG
|
||||
// inclusion.
|
||||
repeated RpcFeerateBucket lowBuckets = 3;
|
||||
}
|
||||
|
||||
message RpcFeeEstimateVerboseExperimentalData {
|
||||
uint64 mempoolReadyTransactionsCount = 1;
|
||||
uint64 mempoolReadyTransactionsTotalMass = 2;
|
||||
uint64 networkMassPerSecond = 3;
|
||||
|
||||
double nextBlockTemplateFeerateMin = 11;
|
||||
double nextBlockTemplateFeerateMedian = 12;
|
||||
double nextBlockTemplateFeerateMax = 13;
|
||||
}
|
||||
|
||||
message GetFeeEstimateRequestMessage {}
|
||||
|
||||
message GetFeeEstimateResponseMessage {
|
||||
RpcFeeEstimate estimate = 1;
|
||||
RPCError error = 1000;
|
||||
}
|
||||
|
||||
message GetFeeEstimateExperimentalRequestMessage { bool verbose = 1; }
|
||||
|
||||
message GetFeeEstimateExperimentalResponseMessage {
|
||||
RpcFeeEstimate estimate = 1;
|
||||
RpcFeeEstimateVerboseExperimentalData verbose = 2;
|
||||
|
||||
RPCError error = 1000;
|
||||
}
|
||||
|
||||
message GetCurrentBlockColorRequestMessage { string hash = 1; }
|
||||
|
||||
message GetCurrentBlockColorResponseMessage {
|
||||
bool blue = 1;
|
||||
|
||||
RPCError error = 1000;
|
||||
}
|
||||
|
||||
// SubmitTransactionReplacementRequestMessage submits a transaction to the
|
||||
// mempool, applying a mandatory Replace by Fee policy
|
||||
message SubmitTransactionReplacementRequestMessage {
|
||||
RpcTransaction transaction = 1;
|
||||
}
|
||||
|
||||
message SubmitTransactionReplacementResponseMessage {
|
||||
// The transaction ID of the submitted transaction
|
||||
string transactionId = 1;
|
||||
|
||||
// The previous transaction replaced in the mempool by the newly submitted one
|
||||
RpcTransaction replacedTransaction = 2;
|
||||
|
||||
RPCError error = 1000;
|
||||
}
|
@ -17,7 +17,7 @@ func (x *KaspadMessage_GetCurrentNetworkResponse) toAppMessage() (appmessage.Mes
|
||||
if x == nil {
|
||||
return nil, errors.Wrapf(errorNil, "KaspadMessage_GetCurrentNetworkResponse is nil")
|
||||
}
|
||||
return x.toAppMessage()
|
||||
return x.GetCurrentNetworkResponse.toAppMessage()
|
||||
}
|
||||
|
||||
func (x *KaspadMessage_GetCurrentNetworkResponse) fromAppMessage(message *appmessage.GetCurrentNetworkResponseMessage) error {
|
||||
|
@ -0,0 +1,67 @@
|
||||
package protowire
|
||||
|
||||
import (
|
||||
"github.com/kaspanet/kaspad/app/appmessage"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func (x *KaspadMessage_GetFeeEstimateRequest) toAppMessage() (appmessage.Message, error) {
|
||||
return &appmessage.GetFeeEstimateRequestMessage{}, nil
|
||||
}
|
||||
|
||||
func (x *KaspadMessage_GetFeeEstimateRequest) fromAppMessage(_ *appmessage.GetFeeEstimateRequestMessage) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *KaspadMessage_GetFeeEstimateResponse) toAppMessage() (appmessage.Message, error) {
|
||||
if x == nil {
|
||||
return nil, errors.Wrapf(errorNil, "KaspadMessage_GetFeeEstimateResponse is nil")
|
||||
}
|
||||
return x.GetFeeEstimateResponse.toAppMessage()
|
||||
}
|
||||
|
||||
func (x *GetFeeEstimateResponseMessage) toAppMessage() (appmessage.Message, error) {
|
||||
if x == nil {
|
||||
return nil, errors.Wrapf(errorNil, "GetFeeEstimateResponseMessage is nil")
|
||||
}
|
||||
rpcErr, err := x.Error.toAppMessage()
|
||||
// Error is an optional field
|
||||
if err != nil && !errors.Is(err, errorNil) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
estimate, err := x.Estimate.toAppMessage()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &appmessage.GetFeeEstimateResponseMessage{
|
||||
Error: rpcErr,
|
||||
Estimate: estimate,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (x *RpcFeeEstimate) toAppMessage() (appmessage.RPCFeeEstimate, error) {
|
||||
if x == nil {
|
||||
return appmessage.RPCFeeEstimate{}, errors.Wrapf(errorNil, "RpcFeeEstimate is nil")
|
||||
}
|
||||
return appmessage.RPCFeeEstimate{
|
||||
PriorityBucket: appmessage.RPCFeeRateBucket{
|
||||
Feerate: x.PriorityBucket.Feerate,
|
||||
EstimatedSeconds: x.PriorityBucket.EstimatedSeconds,
|
||||
},
|
||||
NormalBuckets: feeRateBucketsToAppMessage(x.NormalBuckets),
|
||||
LowBuckets: feeRateBucketsToAppMessage(x.LowBuckets),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func feeRateBucketsToAppMessage(protoBuckets []*RpcFeerateBucket) []appmessage.RPCFeeRateBucket {
|
||||
appMsgBuckets := make([]appmessage.RPCFeeRateBucket, len(protoBuckets))
|
||||
for i, bucket := range protoBuckets {
|
||||
appMsgBuckets[i] = appmessage.RPCFeeRateBucket{
|
||||
Feerate: bucket.Feerate,
|
||||
EstimatedSeconds: bucket.EstimatedSeconds,
|
||||
}
|
||||
}
|
||||
return appMsgBuckets
|
||||
}
|
@ -0,0 +1,85 @@
|
||||
package protowire
|
||||
|
||||
import (
|
||||
"github.com/kaspanet/kaspad/app/appmessage"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func (x *KaspadMessage_SubmitTransactionReplacementRequest) toAppMessage() (appmessage.Message, error) {
|
||||
if x == nil {
|
||||
return nil, errors.Wrapf(errorNil, "KaspadMessage_SubmitTransactionReplacementRequest is nil")
|
||||
}
|
||||
return x.SubmitTransactionReplacementRequest.toAppMessage()
|
||||
}
|
||||
|
||||
func (x *KaspadMessage_SubmitTransactionReplacementRequest) fromAppMessage(message *appmessage.SubmitTransactionReplacementRequestMessage) error {
|
||||
x.SubmitTransactionReplacementRequest = &SubmitTransactionReplacementRequestMessage{
|
||||
Transaction: &RpcTransaction{},
|
||||
}
|
||||
x.SubmitTransactionReplacementRequest.Transaction.fromAppMessage(message.Transaction)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *SubmitTransactionReplacementRequestMessage) toAppMessage() (appmessage.Message, error) {
|
||||
if x == nil {
|
||||
return nil, errors.Wrapf(errorNil, "SubmitBlockRequestMessage is nil")
|
||||
}
|
||||
rpcTransaction, err := x.Transaction.toAppMessage()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &appmessage.SubmitTransactionReplacementRequestMessage{
|
||||
Transaction: rpcTransaction,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (x *KaspadMessage_SubmitTransactionReplacementResponse) toAppMessage() (appmessage.Message, error) {
|
||||
if x == nil {
|
||||
return nil, errors.Wrapf(errorNil, "KaspadMessage_SubmitTransactionReplacementResponse is nil")
|
||||
}
|
||||
return x.SubmitTransactionReplacementResponse.toAppMessage()
|
||||
}
|
||||
|
||||
func (x *KaspadMessage_SubmitTransactionReplacementResponse) fromAppMessage(message *appmessage.SubmitTransactionReplacementResponseMessage) error {
|
||||
var err *RPCError
|
||||
if message.Error != nil {
|
||||
err = &RPCError{Message: message.Error.Message}
|
||||
}
|
||||
x.SubmitTransactionReplacementResponse = &SubmitTransactionReplacementResponseMessage{
|
||||
TransactionId: message.TransactionID,
|
||||
ReplacedTransaction: &RpcTransaction{},
|
||||
Error: err,
|
||||
}
|
||||
if message.ReplacedTransaction != nil {
|
||||
x.SubmitTransactionReplacementResponse.ReplacedTransaction.fromAppMessage(message.ReplacedTransaction)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *SubmitTransactionReplacementResponseMessage) toAppMessage() (appmessage.Message, error) {
|
||||
if x == nil {
|
||||
return nil, errors.Wrapf(errorNil, "SubmitTransactionReplacementResponseMessage is nil")
|
||||
}
|
||||
|
||||
if x.Error != nil {
|
||||
rpcErr, err := x.Error.toAppMessage()
|
||||
// Error is an optional field
|
||||
if err != nil && !errors.Is(err, errorNil) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &appmessage.SubmitTransactionReplacementResponseMessage{
|
||||
TransactionID: x.TransactionId,
|
||||
Error: rpcErr,
|
||||
}, nil
|
||||
}
|
||||
|
||||
replacedTx, err := x.ReplacedTransaction.toAppMessage()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &appmessage.SubmitTransactionReplacementResponseMessage{
|
||||
TransactionID: x.TransactionId,
|
||||
ReplacedTransaction: replacedTx,
|
||||
}, nil
|
||||
}
|
@ -968,6 +968,27 @@ func toRPCPayload(message appmessage.Message) (isKaspadMessage_Payload, error) {
|
||||
return nil, err
|
||||
}
|
||||
return payload, nil
|
||||
case *appmessage.GetFeeEstimateRequestMessage:
|
||||
payload := new(KaspadMessage_GetFeeEstimateRequest)
|
||||
err := payload.fromAppMessage(message)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return payload, nil
|
||||
case *appmessage.SubmitTransactionReplacementRequestMessage:
|
||||
payload := new(KaspadMessage_SubmitTransactionReplacementRequest)
|
||||
err := payload.fromAppMessage(message)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return payload, nil
|
||||
case *appmessage.SubmitTransactionReplacementResponseMessage:
|
||||
payload := new(KaspadMessage_SubmitTransactionReplacementResponse)
|
||||
err := payload.fromAppMessage(message)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return payload, nil
|
||||
default:
|
||||
return nil, nil
|
||||
}
|
||||
|
20
infrastructure/network/rpcclient/rpc_get_fee_estimate.go
Normal file
20
infrastructure/network/rpcclient/rpc_get_fee_estimate.go
Normal file
@ -0,0 +1,20 @@
|
||||
package rpcclient
|
||||
|
||||
import "github.com/kaspanet/kaspad/app/appmessage"
|
||||
|
||||
// GetFeeEstimate sends an RPC request respective to the function's name and returns the RPC server's response
|
||||
func (c *RPCClient) GetFeeEstimate() (*appmessage.GetFeeEstimateResponseMessage, error) {
|
||||
err := c.rpcRouter.outgoingRoute().Enqueue(appmessage.NewGetFeeEstimateRequestMessage())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
response, err := c.route(appmessage.CmdGetFeeEstimateResponseMessage).DequeueWithTimeout(c.timeout)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resp := response.(*appmessage.GetFeeEstimateResponseMessage)
|
||||
if resp.Error != nil {
|
||||
return nil, c.convertRPCError(resp.Error)
|
||||
}
|
||||
return resp, nil
|
||||
}
|
@ -0,0 +1,44 @@
|
||||
package rpcclient
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/kaspanet/kaspad/app/appmessage"
|
||||
)
|
||||
|
||||
// SubmitTransactionReplacement sends an RPC request respective to the function's name and returns the RPC server's response
|
||||
func (c *RPCClient) SubmitTransactionReplacement(transaction *appmessage.RPCTransaction, transactionID string) (*appmessage.SubmitTransactionReplacementResponseMessage, error) {
|
||||
err := c.rpcRouter.outgoingRoute().Enqueue(appmessage.NewSubmitTransactionReplacementRequestMessage(transaction))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for {
|
||||
response, err := c.route(appmessage.CmdSubmitTransactionReplacementResponseMessage).DequeueWithTimeout(c.timeout)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
SubmitTransactionReplacementResponse := response.(*appmessage.SubmitTransactionReplacementResponseMessage)
|
||||
// Match the response to the expected ID. If they are different it means we got an old response which we
|
||||
// previously timed-out on, so we log and continue waiting for the correct current response.
|
||||
if SubmitTransactionReplacementResponse.TransactionID != transactionID {
|
||||
if SubmitTransactionReplacementResponse.Error != nil {
|
||||
// A non-updated Kaspad might return an empty ID in the case of error, so in
|
||||
// such a case we fallback to checking if the error contains the expected ID
|
||||
if SubmitTransactionReplacementResponse.TransactionID != "" || !strings.Contains(SubmitTransactionReplacementResponse.Error.Message, transactionID) {
|
||||
log.Warnf("SubmitTransactionReplacement: received an error response for previous request: %s", SubmitTransactionReplacementResponse.Error)
|
||||
continue
|
||||
}
|
||||
|
||||
} else {
|
||||
log.Warnf("SubmitTransactionReplacement: received a successful response for previous request with ID %s",
|
||||
SubmitTransactionReplacementResponse.TransactionID)
|
||||
continue
|
||||
}
|
||||
}
|
||||
if SubmitTransactionReplacementResponse.Error != nil {
|
||||
return nil, c.convertRPCError(SubmitTransactionReplacementResponse.Error)
|
||||
}
|
||||
|
||||
return SubmitTransactionReplacementResponse, nil
|
||||
}
|
||||
}
|
@ -4,7 +4,7 @@ ARG KASPAMINER_IMAGE
|
||||
FROM ${KASPAD_IMAGE} as kaspad
|
||||
FROM ${KASPAMINER_IMAGE} as kaspaminer
|
||||
|
||||
FROM golang:1.19-alpine
|
||||
FROM golang:1.23-alpine
|
||||
|
||||
RUN mkdir -p /go/src/github.com/kaspanet/kaspad
|
||||
|
||||
|
@ -5,7 +5,6 @@ FLAGS=$@
|
||||
go version
|
||||
|
||||
go get $FLAGS -t -d ../...
|
||||
GO111MODULE=off go get $FLAGS golang.org/x/lint/golint
|
||||
go install $FLAGS honnef.co/go/tools/cmd/staticcheck@latest
|
||||
|
||||
test -z "$(go fmt ./...)"
|
||||
@ -13,7 +12,6 @@ test -z "$(go fmt ./...)"
|
||||
staticcheck -checks SA4006,SA4008,SA4009,SA4010,SA5003,SA1004,SA1014,SA1021,SA1023,SA1024,SA1025,SA1026,SA1027,SA1028,SA2000,SA2001,SA2003,SA4000,SA4001,SA4003,SA4004,SA4011,SA4012,SA4013,SA4014,SA4015,SA4016,SA4017,SA4018,SA4019,SA4020,SA4021,SA4022,SA4023,SA5000,SA5002,SA5004,SA5005,SA5007,SA5008,SA5009,SA5010,SA5011,SA5012,SA6001,SA6002,SA9001,SA9002,SA9003,SA9004,SA9005,SA9006,ST1019 ./...
|
||||
|
||||
go vet -composites=false $FLAGS ./...
|
||||
golint -set_exit_status $FLAGS ./...
|
||||
|
||||
go install $FLAGS ../...
|
||||
|
||||
|
@ -2,6 +2,7 @@ package txmass
|
||||
|
||||
import (
|
||||
"github.com/kaspanet/kaspad/domain/consensus/model/externalapi"
|
||||
"github.com/kaspanet/kaspad/domain/consensus/utils/constants"
|
||||
"github.com/kaspanet/kaspad/domain/consensus/utils/transactionhelper"
|
||||
)
|
||||
|
||||
@ -10,6 +11,9 @@ type Calculator struct {
|
||||
massPerTxByte uint64
|
||||
massPerScriptPubKeyByte uint64
|
||||
massPerSigOp uint64
|
||||
|
||||
// The parameter for scaling inverse KAS value to mass units (KIP-0009)
|
||||
storageMassParameter uint64
|
||||
}
|
||||
|
||||
// NewCalculator creates a new instance of Calculator
|
||||
@ -18,6 +22,7 @@ func NewCalculator(massPerTxByte, massPerScriptPubKeyByte, massPerSigOp uint64)
|
||||
massPerTxByte: massPerTxByte,
|
||||
massPerScriptPubKeyByte: massPerScriptPubKeyByte,
|
||||
massPerSigOp: massPerSigOp,
|
||||
storageMassParameter: constants.SompiPerKaspa * 10_000,
|
||||
}
|
||||
}
|
||||
|
||||
@ -59,6 +64,75 @@ func (c *Calculator) CalculateTransactionMass(transaction *externalapi.DomainTra
|
||||
return massForSize + massForScriptPubKey + massForSigOps
|
||||
}
|
||||
|
||||
// CalculateTransactionStorageMass calculates the storage mass of the given transaction (see KIP-0009)
|
||||
func (c *Calculator) CalculateTransactionStorageMass(transaction *externalapi.DomainTransaction) uint64 {
|
||||
if transactionhelper.IsCoinBase(transaction) {
|
||||
return 0
|
||||
}
|
||||
|
||||
outsLen := uint64(len(transaction.Outputs))
|
||||
insLen := uint64(len(transaction.Inputs))
|
||||
|
||||
if insLen == 0 {
|
||||
panic("Storage mass calculation expects at least one input")
|
||||
}
|
||||
|
||||
harmonicOuts := uint64(0)
|
||||
for _, output := range transaction.Outputs {
|
||||
inverseOut := c.storageMassParameter / output.Value
|
||||
if harmonicOuts+inverseOut < harmonicOuts {
|
||||
// Overflow detected. This requires 10^7 outputs so is unrealistic for wallet usages.
|
||||
// If this method is ever used for consensus, this case should be handled by returning an err
|
||||
panic("Unexpected overflow in storage mass calculation")
|
||||
}
|
||||
harmonicOuts += inverseOut
|
||||
}
|
||||
|
||||
if outsLen == 1 || insLen == 1 || (outsLen == 2 && insLen == 2) {
|
||||
harmonicDiff := harmonicOuts
|
||||
for _, input := range transaction.Inputs {
|
||||
if input.UTXOEntry == nil {
|
||||
panic("Storage mass calculation expects a fully populated transaction")
|
||||
}
|
||||
inverseIn := c.storageMassParameter / input.UTXOEntry.Amount()
|
||||
if harmonicDiff < inverseIn {
|
||||
harmonicDiff = 0
|
||||
} else {
|
||||
harmonicDiff -= inverseIn
|
||||
}
|
||||
}
|
||||
return harmonicDiff
|
||||
}
|
||||
|
||||
sumIns := uint64(0)
|
||||
for _, input := range transaction.Inputs {
|
||||
if input.UTXOEntry == nil {
|
||||
panic("Storage mass calculation expects a fully populated transaction")
|
||||
}
|
||||
// Total supply is bounded, so a sum of existing UTXO entries cannot overflow (nor can it be zero)
|
||||
sumIns += input.UTXOEntry.Amount()
|
||||
}
|
||||
meanIns := sumIns / insLen
|
||||
inverseMeanIns := c.storageMassParameter / meanIns
|
||||
arithmeticIns := insLen * inverseMeanIns
|
||||
|
||||
if arithmeticIns < inverseMeanIns {
|
||||
// overflow (so subtraction would be negative)
|
||||
return 0
|
||||
}
|
||||
if harmonicOuts < arithmeticIns {
|
||||
// underflow
|
||||
return 0
|
||||
} else {
|
||||
return harmonicOuts - arithmeticIns
|
||||
}
|
||||
}
|
||||
|
||||
// CalculateTransactionOverallMass calculates the overall mass of the transaction including compute and storage mass components (see KIP-0009)
|
||||
func (c *Calculator) CalculateTransactionOverallMass(transaction *externalapi.DomainTransaction) uint64 {
|
||||
return max(c.CalculateTransactionMass(transaction), c.CalculateTransactionStorageMass(transaction))
|
||||
}
|
||||
|
||||
// transactionEstimatedSerializedSize is the estimated size of a transaction in some
|
||||
// serialization. This has to be deterministic, but not necessarily accurate, since
|
||||
// it's only used as the size component in the transaction and block mass limit
|
||||
|
@ -11,7 +11,7 @@ const validCharacters = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrs
|
||||
const (
|
||||
appMajor uint = 0
|
||||
appMinor uint = 12
|
||||
appPatch uint = 17
|
||||
appPatch uint = 18
|
||||
)
|
||||
|
||||
// appBuild is defined as a variable so it can be overridden during the build
|
||||
|
Loading…
x
Reference in New Issue
Block a user