Add missing db stubs and begin db implementation

This commit is contained in:
jholdstock 2020-05-13 21:25:35 +01:00
parent 1c72b8f1e5
commit f919d4d8fc
8 changed files with 172 additions and 55 deletions

View File

@ -1,8 +1,25 @@
# dcrvsp # dcrvsp
- `main.go` initialises the program with hard-coded config. ## Design decisions
- `responses.go` contains the API response types copied from #625.
- `router.go` contains the webserver init and config. - [gin-gonic](https://github.com/gin-gonic/gin) webserver
- `methods.go` contains an implementation of payfee, copied from #625. - [bbolt](https://github.com/etcd-io/bbolt) database
- `database.go` contains stubbed database methods.
- `wallet.go` contains stubbed dcrwallet calls. ## MVP features
- VSP API "v3" as described in [dcrstakepool #574](https://github.com/decred/dcrstakepool/issues/574)
and implemented in [dcrstakepool #625](https://github.com/decred/dcrstakepool/pull/625)
- Request fee amount
- Request fee address
- Pay fee
- Set voting preferences
- A minimal, static, web front-end providing pool stats and basic connection instructions.
## Future features
- Write database backups to disk periodically.
- Backup over http.
- Status check API call as described in [dcrstakepool #628](https://github.com/decred/dcrstakepool/issues/628).
- Accountability for both client and server changes to voting preferences.
- Consistency checking across connected wallets.
- Validate votebits provided in PayFee request are valid per current agendas.

View File

@ -1,27 +0,0 @@
package main
type Database struct {
}
type Fees struct {
TicketHash string
CommitmentSignature string
FeeAddress string
Address string
SDiff int64
BlockHeight int64
VoteBits uint16
VotingKey string
}
func (db *Database) GetInactiveFeeAddresses() ([]string, error) {
return []string{""}, nil
}
func (db *Database) GetFeesByFeeAddress(feeAddr string) (Fees, error) {
return Fees{}, nil
}
func (db *Database) InsertFeeAddressVotingKey(address, votingKey string, voteBits uint16) error {
return nil
}

64
database/database.go Normal file
View File

@ -0,0 +1,64 @@
package database
import (
"encoding/binary"
"fmt"
"time"
bolt "go.etcd.io/bbolt"
)
// VspDatabase wraps an instance of bbolt DB and provides VSP specific
// convenience functions.
type VspDatabase struct {
db *bolt.DB
}
var (
// vspBkt is the main parent bucket of the VSP. All values and other buckets
// are nested within it.
vspBkt = []byte("vspbkt")
feesBkt = []byte("feesbkt")
versionK = []byte("version")
backupFile = "backup.kv"
version = 1
)
// New initialises and returns a database connection. If no database file is
// found at the provided path, a new one will be created. Returns an open
// database connection which should be closed after use.
func New(dbFile string) (*VspDatabase, error) {
db, err := bolt.Open(dbFile, 0600, &bolt.Options{Timeout: 1 * time.Second})
if err != nil {
return nil, fmt.Errorf("unable to open db file: %v", err)
}
err = createBuckets(db)
if err != nil {
return nil, err
}
return &VspDatabase{db: db}, nil
}
// createBuckets creates all storage buckets of the VSP.
func createBuckets(db *bolt.DB) error {
return db.Update(func(tx *bolt.Tx) error {
pbkt := tx.Bucket(feesBkt)
if pbkt == nil {
pbkt, err := tx.CreateBucket(feesBkt)
if err != nil {
return fmt.Errorf("failed to create %s bucket: %v", string(feesBkt), err)
}
vbytes := make([]byte, 4)
binary.LittleEndian.PutUint32(vbytes, uint32(version))
err = pbkt.Put(versionK, vbytes)
if err != nil {
return err
}
}
return nil
})
}

32
database/fees.go Normal file
View File

@ -0,0 +1,32 @@
package database
type Fees struct {
TicketHash string
CommitmentSignature string
FeeAddress string
Address string
SDiff int64
BlockHeight int64
VoteBits uint16
VotingKey string
}
func (db *VspDatabase) InsertFeeAddressVotingKey(address, votingKey string, voteBits uint16) error {
return nil
}
func (db *VspDatabase) InsertFeeAddress() error {
return nil
}
func (db *VspDatabase) GetInactiveFeeAddresses() ([]string, error) {
return []string{""}, nil
}
func (db *VspDatabase) GetFeesByFeeAddress(feeAddr string) (Fees, error) {
return Fees{}, nil
}
func (db *VspDatabase) GetFeeAddressByTicketHash() (Fees, error) {
return Fees{}, nil
}

9
go.mod
View File

@ -1,15 +1,16 @@
module dcrvsp module github.com/jholdstock/dcrvsp
go 1.14 go 1.14
require ( require (
decred.org/dcrwallet v1.2.3-0.20200507155221-397dd551e317 decred.org/dcrwallet v1.2.3-0.20200507155221-397dd551e317
github.com/decred/dcrd/chaincfg/chainhash v1.0.2 github.com/decred/dcrd/chaincfg/chainhash v1.0.2
github.com/decred/dcrd/chaincfg/v3 v3.0.0-20200311044114-143c1884e4c8 github.com/decred/dcrd/chaincfg/v3 v3.0.0-20200511175520-d08cb3f72b3b
github.com/decred/dcrd/dcrec v1.0.0 github.com/decred/dcrd/dcrec v1.0.0
github.com/decred/dcrd/dcrutil/v3 v3.0.0-20200311044114-143c1884e4c8 github.com/decred/dcrd/dcrutil/v3 v3.0.0-20200511175520-d08cb3f72b3b
github.com/decred/dcrd/rpcclient v1.1.0 github.com/decred/dcrd/rpcclient v1.1.0
github.com/decred/dcrd/txscript/v3 v3.0.0-20200421213827-b60c60ffe98b github.com/decred/dcrd/txscript/v3 v3.0.0-20200511175520-d08cb3f72b3b
github.com/decred/dcrd/wire v1.3.0 github.com/decred/dcrd/wire v1.3.0
github.com/gin-gonic/gin v1.6.3 github.com/gin-gonic/gin v1.6.3
go.etcd.io/bbolt v1.3.4
) )

7
go.sum
View File

@ -44,6 +44,8 @@ github.com/decred/dcrd/chaincfg/v3 v3.0.0-20200215023918-6247af01d5e3/go.mod h1:
github.com/decred/dcrd/chaincfg/v3 v3.0.0-20200215031403-6b2ce76f0986/go.mod h1:v4oyBPQ/ZstYCV7+B0y6HogFByW76xTjr+72fOm66Y8= github.com/decred/dcrd/chaincfg/v3 v3.0.0-20200215031403-6b2ce76f0986/go.mod h1:v4oyBPQ/ZstYCV7+B0y6HogFByW76xTjr+72fOm66Y8=
github.com/decred/dcrd/chaincfg/v3 v3.0.0-20200311044114-143c1884e4c8 h1:UweAZ771bCDNxumIVk7b0y2EJ8JUqeWhXbPpdXKsClc= github.com/decred/dcrd/chaincfg/v3 v3.0.0-20200311044114-143c1884e4c8 h1:UweAZ771bCDNxumIVk7b0y2EJ8JUqeWhXbPpdXKsClc=
github.com/decred/dcrd/chaincfg/v3 v3.0.0-20200311044114-143c1884e4c8/go.mod h1:v4oyBPQ/ZstYCV7+B0y6HogFByW76xTjr+72fOm66Y8= github.com/decred/dcrd/chaincfg/v3 v3.0.0-20200311044114-143c1884e4c8/go.mod h1:v4oyBPQ/ZstYCV7+B0y6HogFByW76xTjr+72fOm66Y8=
github.com/decred/dcrd/chaincfg/v3 v3.0.0-20200511175520-d08cb3f72b3b h1:L6qM+5ISaaYSnNMAFMouu/FGcIN0tP42rJUdtAJqKgY=
github.com/decred/dcrd/chaincfg/v3 v3.0.0-20200511175520-d08cb3f72b3b/go.mod h1:OHbKBa6UZZOXCU1Y8f9Ta3O+GShto7nB1O0nuEutKq4=
github.com/decred/dcrd/connmgr/v3 v3.0.0-20200311044114-143c1884e4c8/go.mod h1:mvIMJsrOEngogmVrq+tdbPIZchHVgGnVBZeNwj1cW6E= github.com/decred/dcrd/connmgr/v3 v3.0.0-20200311044114-143c1884e4c8/go.mod h1:mvIMJsrOEngogmVrq+tdbPIZchHVgGnVBZeNwj1cW6E=
github.com/decred/dcrd/crypto/blake256 v1.0.0 h1:/8DMNYp9SGi5f0w7uCm6d6M4OU2rGFK09Y2A4Xv7EE0= github.com/decred/dcrd/crypto/blake256 v1.0.0 h1:/8DMNYp9SGi5f0w7uCm6d6M4OU2rGFK09Y2A4Xv7EE0=
github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc= github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc=
@ -82,6 +84,8 @@ github.com/decred/dcrd/dcrutil/v3 v3.0.0-20200215023918-6247af01d5e3/go.mod h1:4
github.com/decred/dcrd/dcrutil/v3 v3.0.0-20200215031403-6b2ce76f0986/go.mod h1:jFxEd2LWDLvrWlrIiyx9ZGTQjvoFHZ0OVfBdyIX7jSw= github.com/decred/dcrd/dcrutil/v3 v3.0.0-20200215031403-6b2ce76f0986/go.mod h1:jFxEd2LWDLvrWlrIiyx9ZGTQjvoFHZ0OVfBdyIX7jSw=
github.com/decred/dcrd/dcrutil/v3 v3.0.0-20200311044114-143c1884e4c8 h1:l7N4vMUp1k8Ugs+ol3WLRKB3Xij0MOf5hC2bY/W6bS4= github.com/decred/dcrd/dcrutil/v3 v3.0.0-20200311044114-143c1884e4c8 h1:l7N4vMUp1k8Ugs+ol3WLRKB3Xij0MOf5hC2bY/W6bS4=
github.com/decred/dcrd/dcrutil/v3 v3.0.0-20200311044114-143c1884e4c8/go.mod h1:/CDBC1SOXKrmihavgXviaTr6eVZSAWKQqEbRmacDxgg= github.com/decred/dcrd/dcrutil/v3 v3.0.0-20200311044114-143c1884e4c8/go.mod h1:/CDBC1SOXKrmihavgXviaTr6eVZSAWKQqEbRmacDxgg=
github.com/decred/dcrd/dcrutil/v3 v3.0.0-20200511175520-d08cb3f72b3b h1:94fhkXS9ObD1P0SxOxk1TAFVhdFBpVeQeUWl0nYb6X8=
github.com/decred/dcrd/dcrutil/v3 v3.0.0-20200511175520-d08cb3f72b3b/go.mod h1:85NtF/fmqL2UDf0/gLhTHG/m/0HQHwG+erQKkwWW27A=
github.com/decred/dcrd/gcs v1.0.1 h1:MpJXLskT41+JDaD3RLdlSlF2vlu1sxPpZgiRI7FVTWw= github.com/decred/dcrd/gcs v1.0.1 h1:MpJXLskT41+JDaD3RLdlSlF2vlu1sxPpZgiRI7FVTWw=
github.com/decred/dcrd/gcs v1.0.1/go.mod h1:YwutGzusSdJM79CJtxCo9t7WRCvnkLtWSD19TPo1i9g= github.com/decred/dcrd/gcs v1.0.1/go.mod h1:YwutGzusSdJM79CJtxCo9t7WRCvnkLtWSD19TPo1i9g=
github.com/decred/dcrd/gcs/v2 v2.0.0/go.mod h1:3XjKcrtvB+r2ezhIsyNCLk6dRnXRJVyYmsd1P3SkU3o= github.com/decred/dcrd/gcs/v2 v2.0.0/go.mod h1:3XjKcrtvB+r2ezhIsyNCLk6dRnXRJVyYmsd1P3SkU3o=
@ -97,6 +101,8 @@ github.com/decred/dcrd/txscript/v3 v3.0.0-20200215023918-6247af01d5e3/go.mod h1:
github.com/decred/dcrd/txscript/v3 v3.0.0-20200215031403-6b2ce76f0986/go.mod h1:KsDS7McU1yFaCYR9LCIwk6YnE15YN3wJUDxhKdFqlsc= github.com/decred/dcrd/txscript/v3 v3.0.0-20200215031403-6b2ce76f0986/go.mod h1:KsDS7McU1yFaCYR9LCIwk6YnE15YN3wJUDxhKdFqlsc=
github.com/decred/dcrd/txscript/v3 v3.0.0-20200421213827-b60c60ffe98b h1:HqIl8oViATdNtoYDFbHzGhHe4q9irezWXF16fhKy8/4= github.com/decred/dcrd/txscript/v3 v3.0.0-20200421213827-b60c60ffe98b h1:HqIl8oViATdNtoYDFbHzGhHe4q9irezWXF16fhKy8/4=
github.com/decred/dcrd/txscript/v3 v3.0.0-20200421213827-b60c60ffe98b/go.mod h1:vrm3R/AesmA9slTf0rFcwhD0SduAJAWxocyaWVi8dM0= github.com/decred/dcrd/txscript/v3 v3.0.0-20200421213827-b60c60ffe98b/go.mod h1:vrm3R/AesmA9slTf0rFcwhD0SduAJAWxocyaWVi8dM0=
github.com/decred/dcrd/txscript/v3 v3.0.0-20200511175520-d08cb3f72b3b h1:PbEqUN+q0hg/TJdi2IQ0Y/5Qc8GNFrnioXeBXm060n8=
github.com/decred/dcrd/txscript/v3 v3.0.0-20200511175520-d08cb3f72b3b/go.mod h1:vrm3R/AesmA9slTf0rFcwhD0SduAJAWxocyaWVi8dM0=
github.com/decred/dcrd/wire v1.1.0/go.mod h1:/JKOsLInOJu6InN+/zH5AyCq3YDIOW/EqcffvU8fJHM= github.com/decred/dcrd/wire v1.1.0/go.mod h1:/JKOsLInOJu6InN+/zH5AyCq3YDIOW/EqcffvU8fJHM=
github.com/decred/dcrd/wire v1.2.0/go.mod h1:/JKOsLInOJu6InN+/zH5AyCq3YDIOW/EqcffvU8fJHM= github.com/decred/dcrd/wire v1.2.0/go.mod h1:/JKOsLInOJu6InN+/zH5AyCq3YDIOW/EqcffvU8fJHM=
github.com/decred/dcrd/wire v1.3.0 h1:X76I2/a8esUmxXmFpJpAvXEi014IA4twgwcOBeIS8lE= github.com/decred/dcrd/wire v1.3.0 h1:X76I2/a8esUmxXmFpJpAvXEi014IA4twgwcOBeIS8lE=
@ -153,6 +159,7 @@ github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo=
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs= github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs=
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
go.etcd.io/bbolt v1.3.4 h1:hi1bXHMVrlQh6WwxAy+qZCV/SYIlqo+Ushwdpa4tAKg=
go.etcd.io/bbolt v1.3.4/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ= go.etcd.io/bbolt v1.3.4/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ=
golang.org/x/crypto v0.0.0-20180718160520-a2144134853f/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20180718160520-a2144134853f/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=

47
main.go
View File

@ -3,6 +3,8 @@ package main
import ( import (
"crypto/ed25519" "crypto/ed25519"
"crypto/rand" "crypto/rand"
"errors"
"fmt"
"io/ioutil" "io/ioutil"
"log" "log"
"os" "os"
@ -10,39 +12,46 @@ import (
"github.com/decred/dcrd/chaincfg/v3" "github.com/decred/dcrd/chaincfg/v3"
"github.com/decred/dcrd/rpcclient" "github.com/decred/dcrd/rpcclient"
"github.com/jholdstock/dcrvsp/database"
) )
const listen = ":3000" const listen = ":3000"
// Config vars type Config struct {
var (
signKey ed25519.PrivateKey signKey ed25519.PrivateKey
pubKey ed25519.PublicKey pubKey ed25519.PublicKey
poolFees float64 poolFees float64
netParams *chaincfg.Params netParams *chaincfg.Params
) dbFile string
}
var cfg Config
// Database with stubbed methods // Database with stubbed methods
var db Database var db *database.VspDatabase
// RPC clients
var nodeConnection *rpcclient.Client var nodeConnection *rpcclient.Client
var walletConnection *WalletClient var walletConnection *WalletClient
func initConfig() { func initConfig() (*Config, error) {
seedPath := filepath.Join("dcrvsp", "sign.seed") homePath := "~/.dcrvsp"
seedPath := filepath.Join(homePath, "sign.seed")
seed, err := ioutil.ReadFile(seedPath) seed, err := ioutil.ReadFile(seedPath)
var signKey ed25519.PrivateKey
if err != nil { if err != nil {
if !os.IsNotExist(err) { if !os.IsNotExist(err) {
log.Fatal("seedPath does not exist") return nil, errors.New("seedPath does not exist")
} }
_, signKey, err = ed25519.GenerateKey(rand.Reader) _, signKey, err = ed25519.GenerateKey(rand.Reader)
if err != nil { if err != nil {
log.Fatalf("failed to generate signing key: %v", err) return nil, fmt.Errorf("failed to generate signing key: %v", err)
} }
err = ioutil.WriteFile(seedPath, signKey.Seed(), 0400) err = ioutil.WriteFile(seedPath, signKey.Seed(), 0400)
if err != nil { if err != nil {
log.Fatalf("failed to save signing key: %v", err) return nil, fmt.Errorf("failed to save signing key: %v", err)
} }
} else { } else {
signKey = ed25519.NewKeyFromSeed(seed) signKey = ed25519.NewKeyFromSeed(seed)
@ -50,15 +59,29 @@ func initConfig() {
pubKey, ok := signKey.Public().(ed25519.PublicKey) pubKey, ok := signKey.Public().(ed25519.PublicKey)
if !ok { if !ok {
log.Fatalf("failed to cast signing key: %T", pubKey) return nil, fmt.Errorf("failed to cast signing key: %T", pubKey)
} }
netParams = chaincfg.TestNet3Params() return &Config{
netParams: chaincfg.TestNet3Params(),
dbFile: filepath.Join(homePath, "database.db"),
pubKey: pubKey,
poolFees: 0.1,
signKey: signKey,
}, nil
} }
func main() { func main() {
initConfig() cfg, err := initConfig()
if err != nil {
log.Fatalf("config error: %v", err)
}
db, err = database.New(cfg.dbFile)
if err != nil {
log.Fatalf("database error: %v", err)
}
// Start HTTP server // Start HTTP server
log.Printf("Listening on %s", listen) log.Printf("Listening on %s", listen)

View File

@ -28,7 +28,7 @@ func sendJSONResponse(resp interface{}, code int, c *gin.Context) {
return return
} }
sig := ed25519.Sign(signKey, dec) sig := ed25519.Sign(cfg.signKey, dec)
c.Writer.Header().Set("Content-Type", "application/json; charset=utf-8") c.Writer.Header().Set("Content-Type", "application/json; charset=utf-8")
c.Writer.Header().Set("VSP-Signature", hex.EncodeToString(sig)) c.Writer.Header().Set("VSP-Signature", hex.EncodeToString(sig))
c.Writer.Write(dec) c.Writer.Write(dec)
@ -41,7 +41,7 @@ func payFee(c *gin.Context) {
// voteBits - voting preferences in little endian // voteBits - voting preferences in little endian
votingKey := c.Param("votingKey") votingKey := c.Param("votingKey")
votingWIF, err := dcrutil.DecodeWIF(votingKey, netParams.PrivateKeyID) votingWIF, err := dcrutil.DecodeWIF(votingKey, cfg.netParams.PrivateKeyID)
if err != nil { if err != nil {
c.AbortWithError(http.StatusInternalServerError, err) c.AbortWithError(http.StatusInternalServerError, err)
return return
@ -84,7 +84,7 @@ func payFee(c *gin.Context) {
findAddress: findAddress:
for _, txOut := range feeTx.TxOut { for _, txOut := range feeTx.TxOut {
_, addresses, _, err := txscript.ExtractPkScriptAddrs(scriptVersion, _, addresses, _, err := txscript.ExtractPkScriptAddrs(scriptVersion,
txOut.PkScript, netParams) txOut.PkScript, cfg.netParams)
if err != nil { if err != nil {
fmt.Printf("Extract: %v", err) fmt.Printf("Extract: %v", err)
c.AbortWithError(http.StatusInternalServerError, err) c.AbortWithError(http.StatusInternalServerError, err)
@ -113,13 +113,13 @@ findAddress:
c.AbortWithError(http.StatusInternalServerError, errors.New("database error")) c.AbortWithError(http.StatusInternalServerError, errors.New("database error"))
return return
} }
voteAddr, err := dcrutil.DecodeAddress(feeEntry.Address, netParams) voteAddr, err := dcrutil.DecodeAddress(feeEntry.Address, cfg.netParams)
if err != nil { if err != nil {
fmt.Errorf("PayFee: DecodeAddress: %v", err) fmt.Errorf("PayFee: DecodeAddress: %v", err)
c.AbortWithError(http.StatusInternalServerError, errors.New("database error")) c.AbortWithError(http.StatusInternalServerError, errors.New("database error"))
return return
} }
_, err = dcrutil.NewAddressPubKeyHash(dcrutil.Hash160(votingWIF.PubKey()), netParams, _, err = dcrutil.NewAddressPubKeyHash(dcrutil.Hash160(votingWIF.PubKey()), cfg.netParams,
dcrec.STEcdsaSecp256k1) dcrec.STEcdsaSecp256k1)
if err != nil { if err != nil {
fmt.Errorf("PayFee: NewAddressPubKeyHash: %v", err) fmt.Errorf("PayFee: NewAddressPubKeyHash: %v", err)
@ -137,7 +137,7 @@ findAddress:
return return
} }
minFee := txrules.StakePoolTicketFee(sDiff, relayFee, int32(feeEntry.BlockHeight), poolFees, netParams) minFee := txrules.StakePoolTicketFee(sDiff, relayFee, int32(feeEntry.BlockHeight), cfg.poolFees, cfg.netParams)
if feeAmount < minFee { if feeAmount < minFee {
fmt.Printf("too cheap: %v %v", feeAmount, minFee) fmt.Printf("too cheap: %v %v", feeAmount, minFee)
c.AbortWithError(http.StatusInternalServerError, fmt.Errorf("dont get cheap on me, dodgson (sent:%v required:%v)", feeAmount, minFee)) c.AbortWithError(http.StatusInternalServerError, fmt.Errorf("dont get cheap on me, dodgson (sent:%v required:%v)", feeAmount, minFee))