From 4f51acbbdb75d800bcb3b6adf0f9a463fe21b2d1 Mon Sep 17 00:00:00 2001 From: Alex Yocom-Piatt Date: Mon, 29 Feb 2016 14:08:33 -0600 Subject: [PATCH] Add get/setticketfee rpc handlers and fix fees in purchaseTicket Fix estimateSSTxSize calcultion First use EstMaxTicketFeeAmount to ensure you get enough utxos. Then when actually calculating fees for tickets, use new consts: sstxTicketCommitmentEstimate sstxSubsidyCommitmentEstimate sstxChangeOutputEstimate --- internal/rpchelp/helpdescs_en_US.go | 9 +++++ internal/rpchelp/methods.go | 2 ++ rpc/legacyrpc/methods.go | 26 +++++++++++++++ rpc/legacyrpc/rpcserverhelp.go | 4 ++- wallet/createtx.go | 52 +++++++++++++++++++++-------- wallet/wallet.go | 29 +++++++++++++--- 6 files changed, 103 insertions(+), 19 deletions(-) diff --git a/internal/rpchelp/helpdescs_en_US.go b/internal/rpchelp/helpdescs_en_US.go index 063397cb3..d5f5b1aef 100644 --- a/internal/rpchelp/helpdescs_en_US.go +++ b/internal/rpchelp/helpdescs_en_US.go @@ -571,4 +571,13 @@ var helpDescsEnUS = map[string]string{ "sendtossgen-blockhash": "Hash for the block being voted on", "sendtossgen-tickethash": "Hash of the ticket used for vote", "sendtossgen-fromaccount": "The account to use (default=\"default\")", + + // SetTxFeeCmd help. + "setticketfee--synopsis": "Modify the increment used each time more fee is required for an authored stake transaction.", + "setticketfee-fee": "The new fee increment valued in decred", + "setticketfee--result0": "The boolean 'true'", + + // SetTxFeeCmd help. + "getticketfee--synopsis": "Get the current fee increment used for an authored stake transaction.", + "getticketfee--result0": "The current fee", } diff --git a/internal/rpchelp/methods.go b/internal/rpchelp/methods.go index 6e08154b0..80189de99 100644 --- a/internal/rpchelp/methods.go +++ b/internal/rpchelp/methods.go @@ -95,6 +95,8 @@ var Methods = []struct { {"sendtosstx", returnsString}, {"sendtossgen", returnsString}, {"getstakeinfo", []interface{}{(*dcrjson.GetStakeInfoResult)(nil)}}, + {"getticketfee", returnsNumber}, + {"setticketfee", returnsBool}, } var HelpDescs = []struct { diff --git a/rpc/legacyrpc/methods.go b/rpc/legacyrpc/methods.go index 148313752..dc755117f 100644 --- a/rpc/legacyrpc/methods.go +++ b/rpc/legacyrpc/methods.go @@ -119,6 +119,7 @@ var rpcHandlers = map[string]struct { "getreceivedbyaddress": {handler: GetReceivedByAddress}, "getseed": {handler: GetSeed, requireUnsafeOnMainNet: true}, "getstakeinfo": {handlerWithChain: GetStakeInfo}, + "getticketfee": {handler: GetTicketFee}, "getticketmaxprice": {handler: GetTicketMaxPrice}, "gettickets": {handlerWithChain: GetTickets}, "getticketvotebits": {handler: GetTicketVoteBits}, @@ -146,6 +147,7 @@ var rpcHandlers = map[string]struct { "sendtossgen": {handler: SendToSSGen}, "sendtossrtx": {handlerWithChain: SendToSSRtx}, "setgenerate": {handler: SetGenerate}, + "setticketfee": {handler: SetTicketFee}, "setticketmaxprice": {handler: SetTicketMaxPrice}, "setticketvotebits": {handler: SetTicketVoteBits}, "settxfee": {handler: SetTxFee}, @@ -1285,6 +1287,11 @@ func GetStakeInfo(icmd interface{}, w *wallet.Wallet, chainClient *chain.RPCClie return resp, nil } +// GetTicketFee gets the currently set price per kb for tickets +func GetTicketFee(icmd interface{}, w *wallet.Wallet) (interface{}, error) { + return w.TicketFeeIncrement().ToCoin(), nil +} + // GetTicketMaxPrice gets the maximum price the user is willing to pay for a // ticket. func GetTicketMaxPrice(icmd interface{}, w *wallet.Wallet) (interface{}, error) { @@ -2742,6 +2749,25 @@ func SetTicketVoteBits(icmd interface{}, w *wallet.Wallet) (interface{}, error) return nil, nil } +// SetTicketFee sets the transaction fee per kilobyte added to tickets. +func SetTicketFee(icmd interface{}, w *wallet.Wallet) (interface{}, error) { + cmd := icmd.(*dcrjson.SetTicketFeeCmd) + + // Check that amount is not negative. + if cmd.Fee < 0 { + return nil, ErrNeedPositiveAmount + } + + incr, err := dcrutil.NewAmount(cmd.Fee) + if err != nil { + return nil, err + } + w.SetTicketFeeIncrement(incr) + + // A boolean true result is returned upon success. + return true, nil +} + // SetTxFee sets the transaction fee per kilobyte added to transactions. func SetTxFee(icmd interface{}, w *wallet.Wallet) (interface{}, error) { cmd := icmd.(*dcrjson.SetTxFeeCmd) diff --git a/rpc/legacyrpc/rpcserverhelp.go b/rpc/legacyrpc/rpcserverhelp.go index 6b4b198cd..aeb631be3 100644 --- a/rpc/legacyrpc/rpcserverhelp.go +++ b/rpc/legacyrpc/rpcserverhelp.go @@ -65,6 +65,8 @@ func helpDescsEnUS() map[string]string { "sendtosstx": "sendtosstx \"fromaccount\" amounts [{\"txid\":\"value\",\"vout\":n,\"tree\":n,\"amt\":n},...] [{\"addr\":\"value\",\"commitamt\":n,\"changeaddr\":\"value\",\"changeamt\":n},...] (minconf=1 \"comment\")\n\nSend to SStx\n\nArguments:\n1. fromaccount (string, required) The account sent from\n2. amounts (object, required) Amounts to send\n{\n \"Key\": Value, (object) Unused\n ...\n}\n3. inputs (array of object, required) Inputs for the tx\n[{\n \"txid\": \"value\", (string) Txid to use\n \"vout\": n, (numeric) Vout for the input tx\n \"tree\": n, (numeric) Input tree\n \"amt\": n, (numeric) Amount\n},...]\n4. couts (array of object, required) Couts for the tx\n[{\n \"addr\": \"value\", (string) Address to use\n \"commitamt\": n, (numeric) Amount to commit\n \"changeaddr\": \"value\", (string) Change address to use\n \"changeamt\": n, (numeric) Change amount\n},...]\n5. minconf (numeric, optional, default=1) Minimum number of block confirmations required\n6. comment (string, optional) Unused\n\nResult:\n\"value\" (string) txid of the resulting transaction\n", "sendtossgen": "sendtossgen \"fromaccount\" \"tickethash\" \"blockhash\" height votebits (\"comment\")\n\nGenerate a vote tx\n\nArguments:\n1. fromaccount (string, required) The account to use (default=\"default\")\n2. tickethash (string, required) Hash of the ticket used for vote\n3. blockhash (string, required) Hash for the block being voted on\n4. height (numeric, required) Blockheight for vote\n5. votebits (numeric, required) Votebits to set\n6. comment (string, optional) Unused\n\nResult:\n\"value\" (string) txid of the resulting transaction\n", "getstakeinfo": "getstakeinfo\n\nReturns statistics about staking from the wallet.\n\nArguments:\nNone\n\nResult:\n{\n \"poolsize\": n, (numeric) Number of live tickets in the ticket pool.\n \"difficulty\": n.nnn, (numeric) Current stake difficulty.\n \"allmempooltix\": n, (numeric) Number of tickets currently in the mempool\n \"ownmempooltix\": n, (numeric) Number of tickets submitted by this wallet currently in mempool\n \"immature\": n, (numeric) Number of tickets from this wallet that are in the blockchain but which are not yet mature\n \"live\": n, (numeric) Number of mature, active tickets owned by this wallet\n \"proportionlive\": n.nnn, (numeric) (Live / PoolSize)\n \"voted\": n, (numeric) Number of votes cast by this wallet\n \"totalsubsidy\": n.nnn, (numeric) Total amount of coins earned by stake mining\n \"missed\": n, (numeric) Number of missed tickets (failing to vote or expired)\n \"proportionmissed\": n.nnn, (numeric) (Missed / (Missed + Voted))\n \"revoked\": n, (numeric) Number of missed tickets that were missed and then revoked\n} \n", + "getticketfee": "getticketfee\n\nGet the current fee increment used for an authored stake transaction.\n\nArguments:\nNone\n\nResult:\nn.nnn (numeric) The current fee\n", + "setticketfee": "setticketfee fee\n\nModify the increment used each time more fee is required for an authored stake transaction.\n\nArguments:\n1. fee (numeric, required) The new fee increment valued in decred\n\nResult:\ntrue|false (boolean) The boolean 'true'\n", } } @@ -72,4 +74,4 @@ var localeHelpDescs = map[string]func() map[string]string{ "en_US": helpDescsEnUS, } -var requestUsages = "addmultisigaddress nrequired [\"key\",...] (\"account\")\ncreatemultisig nrequired [\"key\",...]\ndumpprivkey \"address\"\ngetaccount \"address\"\ngetaccountaddress \"account\"\ngetaddressesbyaccount \"account\"\ngetbalance (\"account\" minconf=1 \"balancetype\")\ngetbestblockhash\ngetblockcount\ngetinfo\ngetmasterpubkey\ngetmultisigoutinfo \"hash\" index\ngetseed\ngetnewaddress (\"account\" verbose=false)\ngetrawchangeaddress (\"account\" verbose=false)\ngetreceivedbyaccount \"account\" (minconf=1)\ngetreceivedbyaddress \"address\" (minconf=1)\ngettickets includeimmature\ngetticketmaxprice\ngettransaction \"txid\" (includewatchonly=false)\nhelp (\"command\")\nimportprivkey \"privkey\" (\"label\" rescan=true)\nimportscript \"hex\"\nkeypoolrefill (newsize=100)\nlistaccounts (minconf=1)\nlistlockunspent\nlistreceivedbyaccount (minconf=1 includeempty=false includewatchonly=false)\nlistreceivedbyaddress (minconf=1 includeempty=false includewatchonly=false)\nlistsinceblock (\"blockhash\" targetconfirmations=1 includewatchonly=false)\nlisttransactions (\"account\" count=10 from=0 includewatchonly=false)\nlistunspent (minconf=1 maxconf=9999999 [\"address\",...])\nlockunspent unlock [{\"txid\":\"value\",\"vout\":n,\"tree\":n},...]\nredeemmultisigout \"hash\" index tree (\"address\")\nredeemmultisigouts \"fromscraddress\" (\"toaddress\" number)\nsendfrom \"fromaccount\" \"toaddress\" amount (minconf=1 \"comment\" \"commentto\")\nsendmany \"fromaccount\" {\"address\":amount,...} (minconf=1 \"comment\")\nsendtoaddress \"address\" amount (\"comment\" \"commentto\")\nsendtomultisig \"fromaccount\" amount [\"pubkey\",...] (nrequired=1 minconf=1 \"comment\")\nsetticketmaxprice max\nsettxfee amount\nsignmessage \"address\" \"message\"\nsignrawtransaction \"rawtx\" ([{\"txid\":\"value\",\"vout\":n,\"tree\":n,\"scriptpubkey\":\"value\",\"redeemscript\":\"value\"},...] [\"privkey\",...] flags=\"ALL\")\nsignrawtransactions [\"rawtx\",...] (send=true)\nvalidateaddress \"address\"\nverifymessage \"address\" \"signature\" \"message\"\nwalletlock\nwalletpassphrase \"passphrase\" timeout\nwalletpassphrasechange \"oldpassphrase\" \"newpassphrase\"\ncreatenewaccount \"account\"\nexportwatchingwallet (\"account\" download=false)\ngetbestblock\ngetunconfirmedbalance (\"account\")\nlistaddresstransactions [\"address\",...] (\"account\")\nlistalltransactions (\"account\")\nrenameaccount \"oldaccount\" \"newaccount\"\nwalletislocked\npurchaseticket \"fromaccount\" spendlimit (minconf=1 \"ticketaddress\" \"comment\")\nsendtossrtx \"fromaccount\" \"tickethash\" (\"comment\")\nsendtosstx \"fromaccount\" amounts [{\"txid\":\"value\",\"vout\":n,\"tree\":n,\"amt\":n},...] [{\"addr\":\"value\",\"commitamt\":n,\"changeaddr\":\"value\",\"changeamt\":n},...] (minconf=1 \"comment\")\nsendtossgen \"fromaccount\" \"tickethash\" \"blockhash\" height votebits (\"comment\")\ngetstakeinfo" +var requestUsages = "addmultisigaddress nrequired [\"key\",...] (\"account\")\ncreatemultisig nrequired [\"key\",...]\ndumpprivkey \"address\"\ngetaccount \"address\"\ngetaccountaddress \"account\"\ngetaddressesbyaccount \"account\"\ngetbalance (\"account\" minconf=1 \"balancetype\")\ngetbestblockhash\ngetblockcount\ngetinfo\ngetmasterpubkey\ngetmultisigoutinfo \"hash\" index\ngetseed\ngetnewaddress (\"account\" verbose=false)\ngetrawchangeaddress (\"account\" verbose=false)\ngetreceivedbyaccount \"account\" (minconf=1)\ngetreceivedbyaddress \"address\" (minconf=1)\ngettickets includeimmature\ngetticketmaxprice\ngettransaction \"txid\" (includewatchonly=false)\nhelp (\"command\")\nimportprivkey \"privkey\" (\"label\" rescan=true)\nimportscript \"hex\"\nkeypoolrefill (newsize=100)\nlistaccounts (minconf=1)\nlistlockunspent\nlistreceivedbyaccount (minconf=1 includeempty=false includewatchonly=false)\nlistreceivedbyaddress (minconf=1 includeempty=false includewatchonly=false)\nlistsinceblock (\"blockhash\" targetconfirmations=1 includewatchonly=false)\nlisttransactions (\"account\" count=10 from=0 includewatchonly=false)\nlistunspent (minconf=1 maxconf=9999999 [\"address\",...])\nlockunspent unlock [{\"txid\":\"value\",\"vout\":n,\"tree\":n},...]\nredeemmultisigout \"hash\" index tree (\"address\")\nredeemmultisigouts \"fromscraddress\" (\"toaddress\" number)\nsendfrom \"fromaccount\" \"toaddress\" amount (minconf=1 \"comment\" \"commentto\")\nsendmany \"fromaccount\" {\"address\":amount,...} (minconf=1 \"comment\")\nsendtoaddress \"address\" amount (\"comment\" \"commentto\")\nsendtomultisig \"fromaccount\" amount [\"pubkey\",...] (nrequired=1 minconf=1 \"comment\")\nsetticketmaxprice max\nsettxfee amount\nsignmessage \"address\" \"message\"\nsignrawtransaction \"rawtx\" ([{\"txid\":\"value\",\"vout\":n,\"tree\":n,\"scriptpubkey\":\"value\",\"redeemscript\":\"value\"},...] [\"privkey\",...] flags=\"ALL\")\nsignrawtransactions [\"rawtx\",...] (send=true)\nvalidateaddress \"address\"\nverifymessage \"address\" \"signature\" \"message\"\nwalletlock\nwalletpassphrase \"passphrase\" timeout\nwalletpassphrasechange \"oldpassphrase\" \"newpassphrase\"\ncreatenewaccount \"account\"\nexportwatchingwallet (\"account\" download=false)\ngetbestblock\ngetunconfirmedbalance (\"account\")\nlistaddresstransactions [\"address\",...] (\"account\")\nlistalltransactions (\"account\")\nrenameaccount \"oldaccount\" \"newaccount\"\nwalletislocked\npurchaseticket \"fromaccount\" spendlimit (minconf=1 \"ticketaddress\" \"comment\")\nsendtossrtx \"fromaccount\" \"tickethash\" (\"comment\")\nsendtosstx \"fromaccount\" amounts [{\"txid\":\"value\",\"vout\":n,\"tree\":n,\"amt\":n},...] [{\"addr\":\"value\",\"commitamt\":n,\"changeaddr\":\"value\",\"changeamt\":n},...] (minconf=1 \"comment\")\nsendtossgen \"fromaccount\" \"tickethash\" \"blockhash\" height votebits (\"comment\")\ngetstakeinfo\ngetticketfee\nsetticketfee fee" diff --git a/wallet/createtx.go b/wallet/createtx.go index f65ba04d5..3db68bf47 100644 --- a/wallet/createtx.go +++ b/wallet/createtx.go @@ -58,6 +58,20 @@ const ( // fraud proof, and the estimated signature script size. txInEstimate = 32 + 4 + 1 + 12 + 4 + sigScriptEstimate + // sstxTicketCommitmentEstimate = + // - version + amount + + // OP_SSTX OP_DUP OP_HASH160 OP_DATA_20 OP_EQUALVERIFY OP_CHECKSIG + sstxTicketCommitmentEstimate = 2 + 8 + 1 + 1 + 1 + 1 + 20 + 1 + 1 + + // sstxSubsidyCommitmentEstimate = + // version + amount + OP_RETURN OP_DATA_30 + sstxSubsidyCommitmentEstimate = 2 + 8 + 2 + 30 + + // sstxChangeOutputEstimate = + // version + amount + OP_SSTXCHANGE OP_DUP OP_HASH160 OP_DATA_20 + // OP_EQUALVERIFY OP_CHECKSIG + sstxChangeOutputEstimate = 2 + 8 + 1 + 1 + 1 + 1 + 20 + 1 + 1 + // A P2PKH pkScript contains the following bytes: // - OP_DUP // - OP_HASH160 @@ -92,8 +106,11 @@ func EstimateTxSize(numInputs, numOutputs int) int { return estimateTxSize(numInputs, numOutputs) } -func estimateSSTxSize(numInputs, numOutputs int) int { - return txOverheadEstimate + txInEstimate*numInputs + ssTxOutEsimate*numOutputs +func estimateSSTxSize(numInputs int) int { + return txOverheadEstimate + txInEstimate*numInputs + + sstxTicketCommitmentEstimate + + (sstxSubsidyCommitmentEstimate+ + sstxChangeOutputEstimate)*numInputs } func feeForSize(incr dcrutil.Amount, sz int) dcrutil.Amount { @@ -112,6 +129,14 @@ const FeeIncrementMainnet = 5e6 // measured in atoms) added to transactions requiring a fee for TestNet. const FeeIncrementTestnet = 1e3 +// TicketFeeIncrement is the default minimum stake transation fee (0.05 coin, +// measured in atoms). +const TicketFeeIncrement = 5e6 + +// EstMaxTicketFeeAmount is the estimated max ticket fee to be used for size +// calculation for eligible utxos for ticket purchasing +const EstMaxTicketFeeAmount = 0.1 * 1e8 + // -------------------------------------------------------------------------------- // Error Handling @@ -1301,9 +1326,15 @@ func (w *Wallet) purchaseTicket(req purchaseTicketRequest) (interface{}, pair := make(map[string]dcrutil.Amount, 1) pair[ticketAddr.String()] = ticketPrice + // TODO Currently we are using an estimated max ticket size + // to get estimate fees to make sure we have enough eligible + // utxos + var estFee dcrutil.Amount + estFee = EstMaxTicketFeeAmount + // Instead of taking reward addresses by arg, just create them now and // automatically find all eligible outputs from all current utxos. - amountNeeded := req.minBalance + ticketPrice + amountNeeded := req.minBalance + ticketPrice + estFee eligible, err := w.findEligibleOutputsAmount(account, req.minConf, amountNeeded, bs) if err != nil { @@ -1361,18 +1392,11 @@ func (w *Wallet) purchaseTicket(req purchaseTicketRequest) (interface{}, // so we'll have to change to pop in the // last output. - // Calculate the amount of fees needed. - s := estimateSSTxSize(i, i) + estSize := estimateSSTxSize(i) var feeIncrement dcrutil.Amount - switch { - case w.chainParams == &chaincfg.MainNetParams: - feeIncrement = FeeIncrementMainnet - case w.chainParams == &chaincfg.TestNetParams: - feeIncrement = FeeIncrementTestnet - default: - feeIncrement = FeeIncrementTestnet - } - fee := feeForSize(feeIncrement, s) + feeIncrement = w.TicketFeeIncrement() + + fee := feeForSize(feeIncrement, estSize) // Not enough funds after taking fee into account. // Should retry instead of failing, Decred TODO diff --git a/wallet/wallet.go b/wallet/wallet.go index 09f1615dd..6e32501bf 100644 --- a/wallet/wallet.go +++ b/wallet/wallet.go @@ -121,9 +121,11 @@ type Wallet struct { lockedOutpoints map[wire.OutPoint]struct{} - feeIncrementLock sync.Mutex - feeIncrement dcrutil.Amount - DisallowFree bool + feeIncrementLock sync.Mutex + feeIncrement dcrutil.Amount + ticketFeeIncrementLock sync.Mutex + ticketFeeIncrement dcrutil.Amount + DisallowFree bool // Channels for rescan processing. Requests are added and merged with // any waiting requests, before being sent to another goroutine to @@ -212,6 +214,9 @@ func newWallet(vb uint16, esm bool, btm dcrutil.Amount, addressReuse bool, feeIncrement = FeeIncrementTestnet } + var ticketFeeIncrement dcrutil.Amount + ticketFeeIncrement = TicketFeeIncrement + internalPool := NewAddressPool() externalPool := NewAddressPool() @@ -226,6 +231,7 @@ func newWallet(vb uint16, esm bool, btm dcrutil.Amount, addressReuse bool, CurrentStakeDiff: &StakeDifficultyInfo{nil, -1, -1}, lockedOutpoints: map[wire.OutPoint]struct{}{}, feeIncrement: feeIncrement, + ticketFeeIncrement: ticketFeeIncrement, rescanAddJob: make(chan *RescanJob), rescanBatch: make(chan *rescanBatch), rescanNotifications: make(chan interface{}), @@ -296,13 +302,28 @@ func (w *Wallet) FeeIncrement() dcrutil.Amount { } // SetFeeIncrement is used to set the current w.FeeIncrement for the wallet. -// Uses non-exported mutex safe setFeeIncrement func func (w *Wallet) SetFeeIncrement(fee dcrutil.Amount) { w.feeIncrementLock.Lock() w.feeIncrement = fee w.feeIncrementLock.Unlock() } +// TicketFeeIncrement is used to get the current feeIncrement for the wallet. +func (w *Wallet) TicketFeeIncrement() dcrutil.Amount { + w.ticketFeeIncrementLock.Lock() + fee := w.ticketFeeIncrement + w.ticketFeeIncrementLock.Unlock() + + return fee +} + +// SetTicketFeeIncrement is used to set the current w.ticketFeeIncrement for the wallet. +func (w *Wallet) SetTicketFeeIncrement(fee dcrutil.Amount) { + w.ticketFeeIncrementLock.Lock() + w.ticketFeeIncrement = fee + w.ticketFeeIncrementLock.Unlock() +} + // SetGenerate is used to enable or disable stake mining in the // wallet. func (w *Wallet) SetGenerate(flag bool) error {