diff --git a/go.mod b/go.mod index 1960a6a49..e6216be48 100644 --- a/go.mod +++ b/go.mod @@ -27,6 +27,7 @@ require ( github.com/decred/dcrwallet/spv/v2 v2.0.0 github.com/decred/dcrwallet/ticketbuyer/v3 v3.0.0 github.com/decred/dcrwallet/version v1.0.1 + github.com/decred/dcrwallet/wallet v1.3.0 // indirect github.com/decred/dcrwallet/wallet/v2 v2.1.0 github.com/decred/dcrwallet/walletseed v1.0.1 github.com/decred/slog v1.0.0 diff --git a/go.sum b/go.sum index a4e30e7f1..e17a1f4d5 100644 --- a/go.sum +++ b/go.sum @@ -3,6 +3,8 @@ github.com/aead/siphash v0.0.0-20170329201724-e404fcfc8885 h1:bBmEG9/q1xH07CqeeF github.com/aead/siphash v0.0.0-20170329201724-e404fcfc8885/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII= github.com/agl/ed25519 v0.0.0-20170116200512-5312a6153412 h1:w1UutsfOrms1J05zt7ISrnJIXKzwaspym5BTKGx93EI= github.com/agl/ed25519 v0.0.0-20170116200512-5312a6153412/go.mod h1:WPjqKcmVOxf0XSf3YxCJs6N6AOSrOx3obionmG7T0y0= +github.com/boltdb/bolt v1.3.1 h1:JQmyP4ZBrce+ZQu0dY660FMfatumYDLun9hBCUVIkF4= +github.com/boltdb/bolt v1.3.1/go.mod h1:clJnj/oiGkjum5o1McbSZDSLxVThjynRyGBgiAx27Ps= github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd h1:R/opQEbFEy9JGkIguV40SvRY1uliPX8ifOvi6ICsFCw= github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd/go.mod h1:HHNXQzUsZCxOoE+CPiyCTO6x34Zs86zZUiwtpXoGdtg= github.com/btcsuite/goleveldb v1.0.0 h1:Tvd0BfvqX9o823q1j2UZ/epQo09eJh6dTcRp79ilIN4= @@ -58,6 +60,7 @@ github.com/decred/dcrd/dcrec v0.0.0-20180721005914-d26200ec716b/go.mod h1:cRAH1S github.com/decred/dcrd/dcrec v0.0.0-20180721031028-5369a485acf6/go.mod h1:cRAH1SNk8Mi9hKBc/DHbeiWz/fyO8KWZR3H7okrIuOA= github.com/decred/dcrd/dcrec v0.0.0-20180801202239-0761de129164 h1:N5s3yVfjBNW6XNG3gLxYpvt0IUjUsp/FRfC75QpSI+E= github.com/decred/dcrd/dcrec v0.0.0-20180801202239-0761de129164/go.mod h1:cRAH1SNk8Mi9hKBc/DHbeiWz/fyO8KWZR3H7okrIuOA= +github.com/decred/dcrd/dcrec v0.0.0-20181212181811-1a370d38d671/go.mod h1:cRAH1SNk8Mi9hKBc/DHbeiWz/fyO8KWZR3H7okrIuOA= github.com/decred/dcrd/dcrec v0.0.0-20190130161649-59ed4247a1d5/go.mod h1:cRAH1SNk8Mi9hKBc/DHbeiWz/fyO8KWZR3H7okrIuOA= github.com/decred/dcrd/dcrec v1.0.0 h1:W+z6Es+Rai3MXYVoPAxYr5U1DGis0Co33scJ6uH2J6o= github.com/decred/dcrd/dcrec v1.0.0/go.mod h1:HIaqbEJQ+PDzQcORxnqen5/V1FR3B4VpIfmePklt8Q8= @@ -77,6 +80,8 @@ github.com/decred/dcrd/dcrec/secp256k1 v1.0.1 h1:EFWVd1p0t0Y5tnsm/dJujgV0ORogRJ6 github.com/decred/dcrd/dcrec/secp256k1 v1.0.1/go.mod h1:lhu4eZFSfTJWUnR3CFRcpD+Vta0KUAqnhTsTksHXgy0= github.com/decred/dcrd/dcrjson v1.0.0 h1:50DnA0XeV2JrQXoHh43TCKmH+kz2gHjZ1Mj/Pdk7Oz0= github.com/decred/dcrd/dcrjson v1.0.0/go.mod h1:ozddIaeF+EAvZZvFuB3zpfxhyxBGfvbt22crQh+PYuI= +github.com/decred/dcrd/dcrjson v1.1.0 h1:pFpbay3cWACkgloFxWjHBwlXWG2+S2QCJJzNxL40hwg= +github.com/decred/dcrd/dcrjson v1.1.0/go.mod h1:ozddIaeF+EAvZZvFuB3zpfxhyxBGfvbt22crQh+PYuI= github.com/decred/dcrd/dcrjson/v2 v2.0.0 h1:W0q4Alh36c5N318eUpfmU8kXoCNgImMLI87NIXni9Us= github.com/decred/dcrd/dcrjson/v2 v2.0.0/go.mod h1:FYueNy8BREAFq04YNEwcTsmGFcNqY+ehUUO81w2igi4= github.com/decred/dcrd/dcrutil v1.1.1 h1:zOkGiumN/JkobhAgpG/zfFgUoolGKVGYT5na1hbYUoE= @@ -91,10 +96,14 @@ github.com/decred/dcrd/hdkeychain v1.1.1 h1:6+BwOmPfEyw/Krm+91RXysc76F1jqCta3m45 github.com/decred/dcrd/hdkeychain v1.1.1/go.mod h1:CLBVXLoO63fIiqkv38KR23zXGSgrfiAWOybOKTneLhA= github.com/decred/dcrd/hdkeychain/v2 v2.0.0 h1:b6GklXT+LeDumc0bDqMHkss+p2Bu+mgiUDhjAX01LOc= github.com/decred/dcrd/hdkeychain/v2 v2.0.0/go.mod h1:tG+VpXfloIkNGHGd6NeoTElHWA68Wf1aP87zegXDGEw= +github.com/decred/dcrd/mempool v1.1.1 h1:ysFIS3HzEIJ88B1Y4OfL6wjzBurlChbKkzq54hPglGo= +github.com/decred/dcrd/mempool v1.1.1/go.mod h1:u1I2KRv9UHhx2crlbZXYoLDabWyQ8VnnHDSG53UdhCA= github.com/decred/dcrd/mempool/v2 v2.0.0 h1:QoQC5Lri311unqCr/PejBEwNERWMSWtnSa7bpBFZjbQ= github.com/decred/dcrd/mempool/v2 v2.0.0/go.mod h1:/AH0mFOKCglSdEDubF3oRDbWUmDj26gwnrIlFsr+lbM= github.com/decred/dcrd/mining v1.1.0 h1:9Wtla+i+pEjfYsNCfixsipmyyoB26DgL4LSXWAin/zw= github.com/decred/dcrd/mining v1.1.0/go.mod h1:NQEtX604XgNwKcPFId1hVTTiBqmVQDlnqV1yNqGl4oU= +github.com/decred/dcrd/rpcclient v1.1.0 h1:nQZ1qOJaLYoOTM1oQ2dLaqocb5TWI7gNBK+BTY7UVXk= +github.com/decred/dcrd/rpcclient v1.1.0/go.mod h1:SCwBs4d+aqRV2ChnriIZ1y/LgNVHG/2ieEC1vIop82s= github.com/decred/dcrd/rpcclient/v2 v2.0.0 h1:Zy9twdEaOGUdCj/89LAs/IrStm6FcabxzBve4UsA73A= github.com/decred/dcrd/rpcclient/v2 v2.0.0/go.mod h1:9XjbRHBSNqN+DXz8I47gUZszvVjvugqLGK8TZQ4c/u0= github.com/decred/dcrd/txscript v1.0.1 h1:IMgxZFCw3AyG4EbKwywE3SDNshOSHsoUK1Wk/5GqWJ0= @@ -113,6 +122,8 @@ github.com/decred/dcrwallet/spv/v2 v2.0.0 h1:JrU7rf9sL8hZwP3yPSv8bKVEXC99Glhf10Y github.com/decred/dcrwallet/spv/v2 v2.0.0/go.mod h1:2usXom73CRn2wjPJAEPtn+RiXLJPcIlb4aqgjn3+Dog= github.com/decred/dcrwallet/ticketbuyer/v3 v3.0.0 h1:xayh/Rb4vpD26NtwN8mZpnsumsol1leoorgVnUM0xTw= github.com/decred/dcrwallet/ticketbuyer/v3 v3.0.0/go.mod h1:gVrHqQSymQsOz4IkFv7Z8s+c3o7gyw2EBS5j+arrzlg= +github.com/decred/dcrwallet/wallet v1.3.0 h1:RQ4uHIfbDF0ppyeVDkTr7GapeABCddUUKOSexTN7X1M= +github.com/decred/dcrwallet/wallet v1.3.0/go.mod h1:ItOhnw3C4znuLQVWACSq8jCLy221v9X0Xo0b/j5WqgU= github.com/decred/dcrwallet/wallet/v2 v2.0.0 h1:ppSLjwu3DHmRWdnRbN53fu9AFjY0MqK40jzbZQAouG8= github.com/decred/dcrwallet/wallet/v2 v2.0.0/go.mod h1:uDSN0GRTH583eSnPyPiTXNcZ9DrQ2tBqdm/tjp5rjwY= github.com/decred/slog v1.0.0 h1:Dl+W8O6/JH6n2xIFN2p3DNjCmjYwvrXsjlSJTQQ4MhE= @@ -126,6 +137,7 @@ github.com/golang/protobuf v1.1.0 h1:0iH4Ffd/meGoXqF2lSAhZHt8X+cPgkfn/cb6Cce5Vpc github.com/golang/protobuf v1.1.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/gorilla/websocket v1.2.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= github.com/gorilla/websocket v1.4.0 h1:WDFjx/TMzVgy9VdMMQi2K2Emtwi2QcUQsztZ/zLaH/Q= github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= @@ -153,6 +165,7 @@ github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1Cpa go.etcd.io/bbolt v1.3.2 h1:Z/90sZLPOeCy2PwprqkFa25PdkusRzaj9P8zm/KNyvk= go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= golang.org/x/crypto v0.0.0-20180718160520-a2144134853f/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190131182504-b8fe1690c613/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190211182817-74369b46fc67 h1:ng3VDlRp5/DHpSWl02R4rM9I+8M2rhmsuLwAMmkLQWE= golang.org/x/crypto v0.0.0-20190211182817-74369b46fc67/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= diff --git a/internal/rpchelp/helpdescs_en_US.go b/internal/rpchelp/helpdescs_en_US.go index 2b787e706..dd18be7e9 100644 --- a/internal/rpchelp/helpdescs_en_US.go +++ b/internal/rpchelp/helpdescs_en_US.go @@ -414,11 +414,12 @@ var helpDescsEnUS = map[string]string{ "sendtoaddress--synopsis": "Authors, signs, and sends a transaction that outputs some amount to a payment address.\n" + "Unlike sendfrom, outputs are always chosen from the default account.\n" + "A change output is automatically included to send extra output value back to the original account.", - "sendtoaddress-address": "Address to pay", - "sendtoaddress-amount": "Amount to send to the payment address valued in decred", - "sendtoaddress-comment": "Unused", - "sendtoaddress-commentto": "Unused", - "sendtoaddress--result0": "The transaction hash of the sent transaction", + "sendtoaddress-address": "Address to pay", + "sendtoaddress-amount": "Amount to send to the payment address valued in decred", + "sendtoaddress-comment": "Unused", + "sendtoaddress-commentto": "Unused", + "sendtoaddress-subtractfeefromamount": "Toggles whether the tx fee is subtracted from the payment rather than the change", + "sendtoaddress--result0": "The transaction hash of the sent transaction", // SendToMultisigCmd help. "sendtomultisig--synopsis": "Authors, signs, and sends a transaction that outputs some amount to a multisig address.\n" + diff --git a/rpc/jsonrpc/methods.go b/rpc/jsonrpc/methods.go index a034d662d..ea055d264 100644 --- a/rpc/jsonrpc/methods.go +++ b/rpc/jsonrpc/methods.go @@ -2237,12 +2237,13 @@ func makeOutputs(pairs map[string]dcrutil.Amount, chainParams *chaincfg.Params) // sendPairs creates and sends payment transactions. // It returns the transaction hash in string format upon success // All errors are returned in dcrjson.RPCError format -func sendPairs(w *wallet.Wallet, amounts map[string]dcrutil.Amount, account uint32, minconf int32) (string, error) { +func sendPairs(w *wallet.Wallet, amounts map[string]dcrutil.Amount, + options *wallet.CreateTxOptions) (string, error) { outputs, err := makeOutputs(amounts, w.ChainParams()) if err != nil { return "", err } - txSha, err := w.SendOutputs(outputs, account, minconf) + txSha, err := w.SendOutputsWithOptions(outputs, options) if err != nil { if errors.Is(errors.Locked, err) { return "", errWalletUnlockNeeded @@ -2593,7 +2594,13 @@ func (s *Server) sendFrom(ctx context.Context, icmd interface{}) (interface{}, e cmd.ToAddress: amt, } - return sendPairs(w, pairs, account, minConf) + opt := &wallet.CreateTxOptions{ + RecipientPaysFee: dcrjson.Bool(false), + MinimumConfirmations: dcrjson.Int32(minConf), + Account: dcrjson.Uint32(account), + } + + return sendPairs(w, pairs, opt) } // sendMany handles a sendmany RPC request by creating a new transaction @@ -2635,7 +2642,13 @@ func (s *Server) sendMany(ctx context.Context, icmd interface{}) (interface{}, e pairs[k] = amt } - return sendPairs(w, pairs, account, minConf) + opt := &wallet.CreateTxOptions{ + RecipientPaysFee: dcrjson.Bool(false), + MinimumConfirmations: dcrjson.Int32(minConf), + Account: dcrjson.Uint32(account), + } + + return sendPairs(w, pairs, opt) } // sendToAddress handles a sendtoaddress RPC request by creating a new @@ -2671,8 +2684,14 @@ func (s *Server) sendToAddress(ctx context.Context, icmd interface{}) (interface cmd.Address: amt, } + opt := &wallet.CreateTxOptions{ + RecipientPaysFee: cmd.SubtractFeeFromAmount, + MinimumConfirmations: dcrjson.Int32(1), + Account: dcrjson.Uint32(udb.DefaultAccountNum), + } + // sendtoaddress always spends from the default account, this matches bitcoind - return sendPairs(w, pairs, udb.DefaultAccountNum, 1) + return sendPairs(w, pairs, opt) } // sendToMultiSig handles a sendtomultisig RPC request by creating a new diff --git a/rpc/jsonrpc/rpcserverhelp.go b/rpc/jsonrpc/rpcserverhelp.go index c62bfee61..308e67e5e 100644 --- a/rpc/jsonrpc/rpcserverhelp.go +++ b/rpc/jsonrpc/rpcserverhelp.go @@ -59,7 +59,7 @@ func helpDescsEnUS() map[string]string { "revoketickets": "revoketickets\n\nRequests the wallet create revovactions for any previously missed tickets. Wallet must be unlocked.\n\nArguments:\nNone\n\nResult:\nNothing\n", "sendfrom": "sendfrom \"fromaccount\" \"toaddress\" amount (minconf=1 \"comment\" \"commentto\")\n\nDEPRECATED -- Authors, signs, and sends a transaction that outputs some amount to a payment address.\nA change output is automatically included to send extra output value back to the original account.\n\nArguments:\n1. fromaccount (string, required) Account to pick unspent outputs from\n2. toaddress (string, required) Address to pay\n3. amount (numeric, required) Amount to send to the payment address valued in decred\n4. minconf (numeric, optional, default=1) Minimum number of block confirmations required before a transaction output is eligible to be spent\n5. comment (string, optional) Unused\n6. commentto (string, optional) Unused\n\nResult:\n\"value\" (string) The transaction hash of the sent transaction\n", "sendmany": "sendmany \"fromaccount\" {\"address\":amount,...} (minconf=1 \"comment\")\n\nAuthors, signs, and sends a transaction that outputs to many payment addresses.\nA change output is automatically included to send extra output value back to the original account.\n\nArguments:\n1. fromaccount (string, required) DEPRECATED -- Account to pick unspent outputs from\n2. amounts (object, required) Pairs of payment addresses and the output amount to pay each\n{\n \"Address to pay\": Amount to send to the payment address valued in decred, (object) JSON object using payment addresses as keys and output amounts valued in decred to send to each address\n ...\n}\n3. minconf (numeric, optional, default=1) Minimum number of block confirmations required before a transaction output is eligible to be spent\n4. comment (string, optional) Unused\n\nResult:\n\"value\" (string) The transaction hash of the sent transaction\n", - "sendtoaddress": "sendtoaddress \"address\" amount (\"comment\" \"commentto\")\n\nAuthors, signs, and sends a transaction that outputs some amount to a payment address.\nUnlike sendfrom, outputs are always chosen from the default account.\nA change output is automatically included to send extra output value back to the original account.\n\nArguments:\n1. address (string, required) Address to pay\n2. amount (numeric, required) Amount to send to the payment address valued in decred\n3. comment (string, optional) Unused\n4. commentto (string, optional) Unused\n\nResult:\n\"value\" (string) The transaction hash of the sent transaction\n", + "sendtoaddress": "sendtoaddress \"address\" amount (\"comment\" \"commentto\" subtractfeefromamount)\n\nAuthors, signs, and sends a transaction that outputs some amount to a payment address.\nUnlike sendfrom, outputs are always chosen from the default account.\nA change output is automatically included to send extra output value back to the original account.\n\nArguments:\n1. address (string, required) Address to pay\n2. amount (numeric, required) Amount to send to the payment address valued in decred\n3. comment (string, optional) Unused\n4. commentto (string, optional) Unused\n5. subtractfeefromamount (boolean, optional) Toggles whether the tx fee is subtracted from the payment rather than the change\n\nResult:\n\"value\" (string) The transaction hash of the sent transaction\n", "sendtomultisig": "sendtomultisig \"fromaccount\" amount [\"pubkey\",...] (nrequired=1 minconf=1 \"comment\")\n\nAuthors, signs, and sends a transaction that outputs some amount to a multisig address.\nUnlike sendfrom, outputs are always chosen from the default account.\nA change output is automatically included to send extra output value back to the original account.\n\nArguments:\n1. fromaccount (string, required) Unused\n2. amount (numeric, required) Amount to send to the payment address valued in decred\n3. pubkeys (array of string, required) Pubkey to send to.\n4. nrequired (numeric, optional, default=1) The number of signatures required to redeem outputs paid to this address\n5. minconf (numeric, optional, default=1) Minimum number of block confirmations required\n6. comment (string, optional) Unused\n\nResult:\n\"value\" (string) The transaction hash of the sent transaction\n", "setticketfee": "setticketfee fee\n\nModify the fee per kB of the serialized tx size used each time more fee is required for an authored stake transaction.\n\nArguments:\n1. fee (numeric, required) The new fee per kB of the serialized tx size valued in decred\n\nResult:\ntrue|false (boolean) The boolean 'true'\n", "settxfee": "settxfee amount\n\nModify the fee per kB of the serialized tx size used each time more fee is required for an authored transaction.\n\nArguments:\n1. amount (numeric, required) The new fee per kB of the serialized tx size valued in decred\n\nResult:\ntrue|false (boolean) The boolean 'true'\n", @@ -87,4 +87,4 @@ var localeHelpDescs = map[string]func() map[string]string{ "en_US": helpDescsEnUS, } -var requestUsages = "accountaddressindex \"account\" branch\naccountsyncaddressindex \"account\" branch index\naddmultisigaddress nrequired [\"key\",...] (\"account\")\naddticket \"tickethex\"\nconsolidate inputs (\"account\" \"address\")\ncreatemultisig nrequired [\"key\",...]\ncreatenewaccount \"account\"\ndumpprivkey \"address\"\nexportwatchingwallet (\"account\" download=false)\ngeneratevote \"blockhash\" height \"tickethash\" votebits \"votebitsext\"\ngetaccountaddress \"account\"\ngetaccount \"address\"\ngetaddressesbyaccount \"account\"\ngetbalance (\"account\" minconf=1)\ngetbestblockhash\ngetbestblock\ngetblockcount\ngetblockhash index\ngetinfo\ngetmasterpubkey (\"account\")\ngetmultisigoutinfo \"hash\" index\ngetnewaddress (\"account\" \"gappolicy\")\ngetrawchangeaddress (\"account\")\ngetreceivedbyaccount \"account\" (minconf=1)\ngetreceivedbyaddress \"address\" (minconf=1)\ngetstakeinfo\ngetticketfee\ngettickets includeimmature\ngettransaction \"txid\" (includewatchonly=false)\ngetunconfirmedbalance (\"account\")\ngetvotechoices\ngetwalletfee\nhelp (\"command\")\nimportprivkey \"privkey\" (\"label\" rescan=true scanfrom)\nimportscript \"hex\" (rescan=true scanfrom)\nkeypoolrefill (newsize=100)\nlistaccounts (minconf=1)\nlistaddresstransactions [\"address\",...] (\"account\")\nlistalltransactions (\"account\")\nlistlockunspent\nlistreceivedbyaccount (minconf=1 includeempty=false includewatchonly=false)\nlistreceivedbyaddress (minconf=1 includeempty=false includewatchonly=false)\nlistscripts\nlistsinceblock (\"blockhash\" targetconfirmations=1 includewatchonly=false)\nlisttransactions (\"account\" count=10 from=0 includewatchonly=false)\nlistunspent (minconf=1 maxconf=9999999 [\"address\",...])\nlockunspent unlock [{\"amount\":n.nnn,\"txid\":\"value\",\"vout\":n,\"tree\":n},...]\npurchaseticket \"fromaccount\" spendlimit (minconf=1 \"ticketaddress\" numtickets \"pooladdress\" poolfees expiry \"comment\" ticketfee)\nredeemmultisigout \"hash\" index tree (\"address\")\nredeemmultisigouts \"fromscraddress\" (\"toaddress\" number)\nrenameaccount \"oldaccount\" \"newaccount\"\nrescanwallet (beginheight=0)\nrevoketickets\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\")\nsetticketfee fee\nsettxfee amount\nsetvotechoice \"agendaid\" \"choiceid\"\nsignmessage \"address\" \"message\"\nsignrawtransaction \"rawtx\" ([{\"txid\":\"value\",\"vout\":n,\"tree\":n,\"scriptpubkey\":\"value\",\"redeemscript\":\"value\"},...] [\"privkey\",...] flags=\"ALL\")\nsignrawtransactions [\"rawtx\",...] (send=true)\nstakepooluserinfo \"user\"\nstartautobuyer \"account\" \"passphrase\" (balancetomaintain maxfeeperkb maxpricerelative maxpriceabsolute \"votingaddress\" \"pooladdress\" poolfees maxperblock)\nstopautobuyer\nsweepaccount \"sourceaccount\" \"destinationaddress\" (requiredconfirmations feeperkb)\nticketsforaddress \"address\"\nvalidateaddress \"address\"\nverifymessage \"address\" \"signature\" \"message\"\nversion\nwalletinfo\nwalletislocked\nwalletlock\nwalletpassphrasechange \"oldpassphrase\" \"newpassphrase\"\nwalletpassphrase \"passphrase\" timeout" +var requestUsages = "accountaddressindex \"account\" branch\naccountsyncaddressindex \"account\" branch index\naddmultisigaddress nrequired [\"key\",...] (\"account\")\naddticket \"tickethex\"\nconsolidate inputs (\"account\" \"address\")\ncreatemultisig nrequired [\"key\",...]\ncreatenewaccount \"account\"\ndumpprivkey \"address\"\nexportwatchingwallet (\"account\" download=false)\ngeneratevote \"blockhash\" height \"tickethash\" votebits \"votebitsext\"\ngetaccountaddress \"account\"\ngetaccount \"address\"\ngetaddressesbyaccount \"account\"\ngetbalance (\"account\" minconf=1)\ngetbestblockhash\ngetbestblock\ngetblockcount\ngetblockhash index\ngetinfo\ngetmasterpubkey (\"account\")\ngetmultisigoutinfo \"hash\" index\ngetnewaddress (\"account\" \"gappolicy\")\ngetrawchangeaddress (\"account\")\ngetreceivedbyaccount \"account\" (minconf=1)\ngetreceivedbyaddress \"address\" (minconf=1)\ngetstakeinfo\ngetticketfee\ngettickets includeimmature\ngettransaction \"txid\" (includewatchonly=false)\ngetunconfirmedbalance (\"account\")\ngetvotechoices\ngetwalletfee\nhelp (\"command\")\nimportprivkey \"privkey\" (\"label\" rescan=true scanfrom)\nimportscript \"hex\" (rescan=true scanfrom)\nkeypoolrefill (newsize=100)\nlistaccounts (minconf=1)\nlistaddresstransactions [\"address\",...] (\"account\")\nlistalltransactions (\"account\")\nlistlockunspent\nlistreceivedbyaccount (minconf=1 includeempty=false includewatchonly=false)\nlistreceivedbyaddress (minconf=1 includeempty=false includewatchonly=false)\nlistscripts\nlistsinceblock (\"blockhash\" targetconfirmations=1 includewatchonly=false)\nlisttransactions (\"account\" count=10 from=0 includewatchonly=false)\nlistunspent (minconf=1 maxconf=9999999 [\"address\",...])\nlockunspent unlock [{\"amount\":n.nnn,\"txid\":\"value\",\"vout\":n,\"tree\":n},...]\npurchaseticket \"fromaccount\" spendlimit (minconf=1 \"ticketaddress\" numtickets \"pooladdress\" poolfees expiry \"comment\" ticketfee)\nredeemmultisigout \"hash\" index tree (\"address\")\nredeemmultisigouts \"fromscraddress\" (\"toaddress\" number)\nrenameaccount \"oldaccount\" \"newaccount\"\nrescanwallet (beginheight=0)\nrevoketickets\nsendfrom \"fromaccount\" \"toaddress\" amount (minconf=1 \"comment\" \"commentto\")\nsendmany \"fromaccount\" {\"address\":amount,...} (minconf=1 \"comment\")\nsendtoaddress \"address\" amount (\"comment\" \"commentto\" subtractfeefromamount)\nsendtomultisig \"fromaccount\" amount [\"pubkey\",...] (nrequired=1 minconf=1 \"comment\")\nsetticketfee fee\nsettxfee amount\nsetvotechoice \"agendaid\" \"choiceid\"\nsignmessage \"address\" \"message\"\nsignrawtransaction \"rawtx\" ([{\"txid\":\"value\",\"vout\":n,\"tree\":n,\"scriptpubkey\":\"value\",\"redeemscript\":\"value\"},...] [\"privkey\",...] flags=\"ALL\")\nsignrawtransactions [\"rawtx\",...] (send=true)\nstakepooluserinfo \"user\"\nstartautobuyer \"account\" \"passphrase\" (balancetomaintain maxfeeperkb maxpricerelative maxpriceabsolute \"votingaddress\" \"pooladdress\" poolfees maxperblock)\nstopautobuyer\nsweepaccount \"sourceaccount\" \"destinationaddress\" (requiredconfirmations feeperkb)\nticketsforaddress \"address\"\nvalidateaddress \"address\"\nverifymessage \"address\" \"signature\" \"message\"\nversion\nwalletinfo\nwalletislocked\nwalletlock\nwalletpassphrasechange \"oldpassphrase\" \"newpassphrase\"\nwalletpassphrase \"passphrase\" timeout" diff --git a/rpc/jsonrpc/types/walletsvrcmds.go b/rpc/jsonrpc/types/walletsvrcmds.go index 563cc401c..7d7fc36fa 100644 --- a/rpc/jsonrpc/types/walletsvrcmds.go +++ b/rpc/jsonrpc/types/walletsvrcmds.go @@ -834,10 +834,11 @@ func NewSendManyCmd(fromAccount string, amounts map[string]float64, minConf *int // SendToAddressCmd defines the sendtoaddress JSON-RPC command. type SendToAddressCmd struct { - Address string - Amount float64 - Comment *string - CommentTo *string + Address string + Amount float64 + Comment *string + CommentTo *string + SubtractFeeFromAmount *bool } // NewSendToAddressCmd returns a new instance which can be used to issue a diff --git a/rpc/jsonrpc/types/walletsvrcmds_test.go b/rpc/jsonrpc/types/walletsvrcmds_test.go index aafbf471d..8921bfeae 100644 --- a/rpc/jsonrpc/types/walletsvrcmds_test.go +++ b/rpc/jsonrpc/types/walletsvrcmds_test.go @@ -93,14 +93,14 @@ func TestWalletSvrCmds(t *testing.T) { { name: "dumpprivkey", newCmd: func() (interface{}, error) { - return dcrjson.NewCmd("dumpprivkey", "1Address") + return dcrjson.NewCmd("dumpprivkey", "TsAddress") }, staticCmd: func() interface{} { - return NewDumpPrivKeyCmd("1Address") + return NewDumpPrivKeyCmd("TsAddress") }, - marshalled: `{"jsonrpc":"1.0","method":"dumpprivkey","params":["1Address"],"id":1}`, + marshalled: `{"jsonrpc":"1.0","method":"dumpprivkey","params":["TsAddress"],"id":1}`, unmarshalled: &DumpPrivKeyCmd{ - Address: "1Address", + Address: "TsAddress", }, }, { @@ -119,14 +119,14 @@ func TestWalletSvrCmds(t *testing.T) { { name: "getaccount", newCmd: func() (interface{}, error) { - return dcrjson.NewCmd("getaccount", "1Address") + return dcrjson.NewCmd("getaccount", "TsAddress") }, staticCmd: func() interface{} { - return NewGetAccountCmd("1Address") + return NewGetAccountCmd("TsAddress") }, - marshalled: `{"jsonrpc":"1.0","method":"getaccount","params":["1Address"],"id":1}`, + marshalled: `{"jsonrpc":"1.0","method":"getaccount","params":["TsAddress"],"id":1}`, unmarshalled: &GetAccountCmd{ - Address: "1Address", + Address: "TsAddress", }, }, { @@ -282,28 +282,28 @@ func TestWalletSvrCmds(t *testing.T) { { name: "getreceivedbyaddress", newCmd: func() (interface{}, error) { - return dcrjson.NewCmd("getreceivedbyaddress", "1Address") + return dcrjson.NewCmd("getreceivedbyaddress", "TsAddress") }, staticCmd: func() interface{} { - return NewGetReceivedByAddressCmd("1Address", nil) + return NewGetReceivedByAddressCmd("TsAddress", nil) }, - marshalled: `{"jsonrpc":"1.0","method":"getreceivedbyaddress","params":["1Address"],"id":1}`, + marshalled: `{"jsonrpc":"1.0","method":"getreceivedbyaddress","params":["TsAddress"],"id":1}`, unmarshalled: &GetReceivedByAddressCmd{ - Address: "1Address", + Address: "TsAddress", MinConf: dcrjson.Int(1), }, }, { name: "getreceivedbyaddress optional", newCmd: func() (interface{}, error) { - return dcrjson.NewCmd("getreceivedbyaddress", "1Address", 6) + return dcrjson.NewCmd("getreceivedbyaddress", "TsAddress", 6) }, staticCmd: func() interface{} { - return NewGetReceivedByAddressCmd("1Address", dcrjson.Int(6)) + return NewGetReceivedByAddressCmd("TsAddress", dcrjson.Int(6)) }, - marshalled: `{"jsonrpc":"1.0","method":"getreceivedbyaddress","params":["1Address",6],"id":1}`, + marshalled: `{"jsonrpc":"1.0","method":"getreceivedbyaddress","params":["TsAddress",6],"id":1}`, unmarshalled: &GetReceivedByAddressCmd{ - Address: "1Address", + Address: "TsAddress", MinConf: dcrjson.Int(6), }, }, @@ -769,17 +769,17 @@ func TestWalletSvrCmds(t *testing.T) { { name: "listunspent optional3", newCmd: func() (interface{}, error) { - return dcrjson.NewCmd("listunspent", 6, 100, []string{"1Address", "1Address2"}) + return dcrjson.NewCmd("listunspent", 6, 100, []string{"TsAddress", "TsAddress2"}) }, staticCmd: func() interface{} { return NewListUnspentCmd(dcrjson.Int(6), dcrjson.Int(100), - &[]string{"1Address", "1Address2"}) + &[]string{"TsAddress", "TsAddress2"}) }, - marshalled: `{"jsonrpc":"1.0","method":"listunspent","params":[6,100,["1Address","1Address2"]],"id":1}`, + marshalled: `{"jsonrpc":"1.0","method":"listunspent","params":[6,100,["TsAddress","TsAddress2"]],"id":1}`, unmarshalled: &ListUnspentCmd{ MinConf: dcrjson.Int(6), MaxConf: dcrjson.Int(100), - Addresses: &[]string{"1Address", "1Address2"}, + Addresses: &[]string{"TsAddress", "TsAddress2"}, }, }, { @@ -818,15 +818,15 @@ func TestWalletSvrCmds(t *testing.T) { { name: "sendfrom", newCmd: func() (interface{}, error) { - return dcrjson.NewCmd("sendfrom", "from", "1Address", 0.5) + return dcrjson.NewCmd("sendfrom", "from", "TsAddress", 0.5) }, staticCmd: func() interface{} { - return NewSendFromCmd("from", "1Address", 0.5, nil, nil, nil) + return NewSendFromCmd("from", "TsAddress", 0.5, nil, nil, nil) }, - marshalled: `{"jsonrpc":"1.0","method":"sendfrom","params":["from","1Address",0.5],"id":1}`, + marshalled: `{"jsonrpc":"1.0","method":"sendfrom","params":["from","TsAddress",0.5],"id":1}`, unmarshalled: &SendFromCmd{ FromAccount: "from", - ToAddress: "1Address", + ToAddress: "TsAddress", Amount: 0.5, MinConf: dcrjson.Int(1), Comment: nil, @@ -836,15 +836,15 @@ func TestWalletSvrCmds(t *testing.T) { { name: "sendfrom optional1", newCmd: func() (interface{}, error) { - return dcrjson.NewCmd("sendfrom", "from", "1Address", 0.5, 6) + return dcrjson.NewCmd("sendfrom", "from", "TsAddress", 0.5, 6) }, staticCmd: func() interface{} { - return NewSendFromCmd("from", "1Address", 0.5, dcrjson.Int(6), nil, nil) + return NewSendFromCmd("from", "TsAddress", 0.5, dcrjson.Int(6), nil, nil) }, - marshalled: `{"jsonrpc":"1.0","method":"sendfrom","params":["from","1Address",0.5,6],"id":1}`, + marshalled: `{"jsonrpc":"1.0","method":"sendfrom","params":["from","TsAddress",0.5,6],"id":1}`, unmarshalled: &SendFromCmd{ FromAccount: "from", - ToAddress: "1Address", + ToAddress: "TsAddress", Amount: 0.5, MinConf: dcrjson.Int(6), Comment: nil, @@ -854,16 +854,16 @@ func TestWalletSvrCmds(t *testing.T) { { name: "sendfrom optional2", newCmd: func() (interface{}, error) { - return dcrjson.NewCmd("sendfrom", "from", "1Address", 0.5, 6, "comment") + return dcrjson.NewCmd("sendfrom", "from", "TsAddress", 0.5, 6, "comment") }, staticCmd: func() interface{} { - return NewSendFromCmd("from", "1Address", 0.5, dcrjson.Int(6), + return NewSendFromCmd("from", "TsAddress", 0.5, dcrjson.Int(6), dcrjson.String("comment"), nil) }, - marshalled: `{"jsonrpc":"1.0","method":"sendfrom","params":["from","1Address",0.5,6,"comment"],"id":1}`, + marshalled: `{"jsonrpc":"1.0","method":"sendfrom","params":["from","TsAddress",0.5,6,"comment"],"id":1}`, unmarshalled: &SendFromCmd{ FromAccount: "from", - ToAddress: "1Address", + ToAddress: "TsAddress", Amount: 0.5, MinConf: dcrjson.Int(6), Comment: dcrjson.String("comment"), @@ -873,16 +873,16 @@ func TestWalletSvrCmds(t *testing.T) { { name: "sendfrom optional3", newCmd: func() (interface{}, error) { - return dcrjson.NewCmd("sendfrom", "from", "1Address", 0.5, 6, "comment", "commentto") + return dcrjson.NewCmd("sendfrom", "from", "TsAddress", 0.5, 6, "comment", "commentto") }, staticCmd: func() interface{} { - return NewSendFromCmd("from", "1Address", 0.5, dcrjson.Int(6), + return NewSendFromCmd("from", "TsAddress", 0.5, dcrjson.Int(6), dcrjson.String("comment"), dcrjson.String("commentto")) }, - marshalled: `{"jsonrpc":"1.0","method":"sendfrom","params":["from","1Address",0.5,6,"comment","commentto"],"id":1}`, + marshalled: `{"jsonrpc":"1.0","method":"sendfrom","params":["from","TsAddress",0.5,6,"comment","commentto"],"id":1}`, unmarshalled: &SendFromCmd{ FromAccount: "from", - ToAddress: "1Address", + ToAddress: "TsAddress", Amount: 0.5, MinConf: dcrjson.Int(6), Comment: dcrjson.String("comment"), @@ -892,16 +892,16 @@ func TestWalletSvrCmds(t *testing.T) { { name: "sendmany", newCmd: func() (interface{}, error) { - return dcrjson.NewCmd("sendmany", "from", `{"1Address":0.5}`) + return dcrjson.NewCmd("sendmany", "from", `{"TsAddress":0.5}`) }, staticCmd: func() interface{} { - amounts := map[string]float64{"1Address": 0.5} + amounts := map[string]float64{"TsAddress": 0.5} return NewSendManyCmd("from", amounts, nil, nil) }, - marshalled: `{"jsonrpc":"1.0","method":"sendmany","params":["from",{"1Address":0.5}],"id":1}`, + marshalled: `{"jsonrpc":"1.0","method":"sendmany","params":["from",{"TsAddress":0.5}],"id":1}`, unmarshalled: &SendManyCmd{ FromAccount: "from", - Amounts: map[string]float64{"1Address": 0.5}, + Amounts: map[string]float64{"TsAddress": 0.5}, MinConf: dcrjson.Int(1), Comment: nil, }, @@ -909,16 +909,16 @@ func TestWalletSvrCmds(t *testing.T) { { name: "sendmany optional1", newCmd: func() (interface{}, error) { - return dcrjson.NewCmd("sendmany", "from", `{"1Address":0.5}`, 6) + return dcrjson.NewCmd("sendmany", "from", `{"TsAddress":0.5}`, 6) }, staticCmd: func() interface{} { - amounts := map[string]float64{"1Address": 0.5} + amounts := map[string]float64{"TsAddress": 0.5} return NewSendManyCmd("from", amounts, dcrjson.Int(6), nil) }, - marshalled: `{"jsonrpc":"1.0","method":"sendmany","params":["from",{"1Address":0.5},6],"id":1}`, + marshalled: `{"jsonrpc":"1.0","method":"sendmany","params":["from",{"TsAddress":0.5},6],"id":1}`, unmarshalled: &SendManyCmd{ FromAccount: "from", - Amounts: map[string]float64{"1Address": 0.5}, + Amounts: map[string]float64{"TsAddress": 0.5}, MinConf: dcrjson.Int(6), Comment: nil, }, @@ -926,16 +926,16 @@ func TestWalletSvrCmds(t *testing.T) { { name: "sendmany optional2", newCmd: func() (interface{}, error) { - return dcrjson.NewCmd("sendmany", "from", `{"1Address":0.5}`, 6, "comment") + return dcrjson.NewCmd("sendmany", "from", `{"TsAddress":0.5}`, 6, "comment") }, staticCmd: func() interface{} { - amounts := map[string]float64{"1Address": 0.5} + amounts := map[string]float64{"TsAddress": 0.5} return NewSendManyCmd("from", amounts, dcrjson.Int(6), dcrjson.String("comment")) }, - marshalled: `{"jsonrpc":"1.0","method":"sendmany","params":["from",{"1Address":0.5},6,"comment"],"id":1}`, + marshalled: `{"jsonrpc":"1.0","method":"sendmany","params":["from",{"TsAddress":0.5},6,"comment"],"id":1}`, unmarshalled: &SendManyCmd{ FromAccount: "from", - Amounts: map[string]float64{"1Address": 0.5}, + Amounts: map[string]float64{"TsAddress": 0.5}, MinConf: dcrjson.Int(6), Comment: dcrjson.String("comment"), }, @@ -943,14 +943,14 @@ func TestWalletSvrCmds(t *testing.T) { { name: "sendtoaddress", newCmd: func() (interface{}, error) { - return dcrjson.NewCmd("sendtoaddress", "1Address", 0.5) + return dcrjson.NewCmd("sendtoaddress", "TsAddress", 0.5) }, staticCmd: func() interface{} { - return NewSendToAddressCmd("1Address", 0.5, nil, nil) + return NewSendToAddressCmd("TsAddress", 0.5, nil, nil) }, - marshalled: `{"jsonrpc":"1.0","method":"sendtoaddress","params":["1Address",0.5],"id":1}`, + marshalled: `{"jsonrpc":"1.0","method":"sendtoaddress","params":["TsAddress",0.5],"id":1}`, unmarshalled: &SendToAddressCmd{ - Address: "1Address", + Address: "TsAddress", Amount: 0.5, Comment: nil, CommentTo: nil, @@ -959,20 +959,40 @@ func TestWalletSvrCmds(t *testing.T) { { name: "sendtoaddress optional1", newCmd: func() (interface{}, error) { - return dcrjson.NewCmd("sendtoaddress", "1Address", 0.5, "comment", "commentto") + return dcrjson.NewCmd("sendtoaddress", "TsAddress", 0.5, "comment", "commentto") }, staticCmd: func() interface{} { - return NewSendToAddressCmd("1Address", 0.5, dcrjson.String("comment"), + return NewSendToAddressCmd("TsAddress", 0.5, dcrjson.String("comment"), dcrjson.String("commentto")) }, - marshalled: `{"jsonrpc":"1.0","method":"sendtoaddress","params":["1Address",0.5,"comment","commentto"],"id":1}`, + marshalled: `{"jsonrpc":"1.0","method":"sendtoaddress","params":["TsAddress",0.5,"comment","commentto"],"id":1}`, unmarshalled: &SendToAddressCmd{ - Address: "1Address", + Address: "TsAddress", Amount: 0.5, Comment: dcrjson.String("comment"), CommentTo: dcrjson.String("commentto"), }, }, + { + name: "sendtoaddress optional2", + newCmd: func() (interface{}, error) { + return dcrjson.NewCmd("sendtoaddress", "TsAddress", 0.5, "comment", "commentto", dcrjson.Bool(true)) + }, + staticCmd: func() interface{} { + val := NewSendToAddressCmd("TsAddress", 0.5, dcrjson.String("comment"), + dcrjson.String("commentto")) + val.SubtractFeeFromAmount = dcrjson.Bool(true) + return val + }, + marshalled: `{"jsonrpc":"1.0","method":"sendtoaddress","params":["TsAddress",0.5,"comment","commentto",true],"id":1}`, + unmarshalled: &SendToAddressCmd{ + Address: "TsAddress", + Amount: 0.5, + Comment: dcrjson.String("comment"), + CommentTo: dcrjson.String("commentto"), + SubtractFeeFromAmount: dcrjson.Bool(true), + }, + }, { name: "settxfee", newCmd: func() (interface{}, error) { @@ -989,14 +1009,14 @@ func TestWalletSvrCmds(t *testing.T) { { name: "signmessage", newCmd: func() (interface{}, error) { - return dcrjson.NewCmd("signmessage", "1Address", "message") + return dcrjson.NewCmd("signmessage", "TsAddress", "message") }, staticCmd: func() interface{} { - return NewSignMessageCmd("1Address", "message") + return NewSignMessageCmd("TsAddress", "message") }, - marshalled: `{"jsonrpc":"1.0","method":"signmessage","params":["1Address","message"],"id":1}`, + marshalled: `{"jsonrpc":"1.0","method":"signmessage","params":["TsAddress","message"],"id":1}`, unmarshalled: &SignMessageCmd{ - Address: "1Address", + Address: "TsAddress", Message: "message", }, }, diff --git a/rpc/jsonrpc/types/walletsvrwscmds_test.go b/rpc/jsonrpc/types/walletsvrwscmds_test.go index db7d67122..785c64c44 100644 --- a/rpc/jsonrpc/types/walletsvrwscmds_test.go +++ b/rpc/jsonrpc/types/walletsvrwscmds_test.go @@ -113,29 +113,29 @@ func TestWalletSvrWsCmds(t *testing.T) { { name: "listaddresstransactions", newCmd: func() (interface{}, error) { - return dcrjson.NewCmd("listaddresstransactions", `["1Address"]`) + return dcrjson.NewCmd("listaddresstransactions", `["TsAddress"]`) }, staticCmd: func() interface{} { - return NewListAddressTransactionsCmd([]string{"1Address"}, nil) + return NewListAddressTransactionsCmd([]string{"TsAddress"}, nil) }, - marshalled: `{"jsonrpc":"1.0","method":"listaddresstransactions","params":[["1Address"]],"id":1}`, + marshalled: `{"jsonrpc":"1.0","method":"listaddresstransactions","params":[["TsAddress"]],"id":1}`, unmarshalled: &ListAddressTransactionsCmd{ - Addresses: []string{"1Address"}, + Addresses: []string{"TsAddress"}, Account: nil, }, }, { name: "listaddresstransactions optional1", newCmd: func() (interface{}, error) { - return dcrjson.NewCmd("listaddresstransactions", `["1Address"]`, "acct") + return dcrjson.NewCmd("listaddresstransactions", `["TsAddress"]`, "acct") }, staticCmd: func() interface{} { - return NewListAddressTransactionsCmd([]string{"1Address"}, + return NewListAddressTransactionsCmd([]string{"TsAddress"}, dcrjson.String("acct")) }, - marshalled: `{"jsonrpc":"1.0","method":"listaddresstransactions","params":[["1Address"],"acct"],"id":1}`, + marshalled: `{"jsonrpc":"1.0","method":"listaddresstransactions","params":[["TsAddress"],"acct"],"id":1}`, unmarshalled: &ListAddressTransactionsCmd{ - Addresses: []string{"1Address"}, + Addresses: []string{"TsAddress"}, Account: dcrjson.String("acct"), }, }, diff --git a/rpc/jsonrpc/types/walletsvrwsntfns_test.go b/rpc/jsonrpc/types/walletsvrwsntfns_test.go index 9c651d7d6..c62e91b10 100644 --- a/rpc/jsonrpc/types/walletsvrwsntfns_test.go +++ b/rpc/jsonrpc/types/walletsvrwsntfns_test.go @@ -60,12 +60,12 @@ func TestWalletSvrWsNtfns(t *testing.T) { { name: "newtx", newNtfn: func() (interface{}, error) { - return dcrjson.NewCmd("newtx", "acct", `{"account":"acct","address":"1Address","category":"send","amount":1.5,"fee":0.0001,"confirmations":1,"txid":"456","walletconflicts":[],"time":12345678,"timereceived":12345876,"vout":789,"otheraccount":"otheracct"}`) + return dcrjson.NewCmd("newtx", "acct", `{"account":"acct","address":"TsAddress","category":"send","amount":1.5,"fee":0.0001,"confirmations":1,"txid":"456","walletconflicts":[],"time":12345678,"timereceived":12345876,"vout":789,"otheraccount":"otheracct"}`) }, staticNtfn: func() interface{} { result := ListTransactionsResult{ Account: "acct", - Address: "1Address", + Address: "TsAddress", Category: "send", Amount: 1.5, Fee: dcrjson.Float64(0.0001), @@ -79,12 +79,12 @@ func TestWalletSvrWsNtfns(t *testing.T) { } return NewNewTxNtfn("acct", result) }, - marshalled: `{"jsonrpc":"1.0","method":"newtx","params":["acct",{"account":"acct","address":"1Address","amount":1.5,"category":"send","confirmations":1,"fee":0.0001,"time":12345678,"timereceived":12345876,"txid":"456","vout":789,"walletconflicts":[],"otheraccount":"otheracct"}],"id":null}`, + marshalled: `{"jsonrpc":"1.0","method":"newtx","params":["acct",{"account":"acct","address":"TsAddress","amount":1.5,"category":"send","confirmations":1,"fee":0.0001,"time":12345678,"timereceived":12345876,"txid":"456","vout":789,"walletconflicts":[],"otheraccount":"otheracct"}],"id":null}`, unmarshalled: &NewTxNtfn{ Account: "acct", Details: ListTransactionsResult{ Account: "acct", - Address: "1Address", + Address: "TsAddress", Category: "send", Amount: 1.5, Fee: dcrjson.Float64(0.0001), diff --git a/wallet/createtx.go b/wallet/createtx.go index 0e2f176ff..4d7e4328d 100644 --- a/wallet/createtx.go +++ b/wallet/createtx.go @@ -17,6 +17,7 @@ import ( "github.com/decred/dcrd/chaincfg/chainec" "github.com/decred/dcrd/chaincfg/chainhash" "github.com/decred/dcrd/dcrec" + "github.com/decred/dcrd/dcrjson/v2" "github.com/decred/dcrd/dcrutil" "github.com/decred/dcrd/mempool/v2" "github.com/decred/dcrd/txscript" @@ -307,22 +308,7 @@ func (w *Wallet) checkHighFees(totalInput dcrutil.Amount, tx *wire.MsgTx) error return nil } -// txToOutputs creates a transaction, selecting previous outputs from an account -// with no less than minconf confirmations, and creates a signed transaction -// that pays to each of the outputs. -func (w *Wallet) txToOutputs(op errors.Op, outputs []*wire.TxOut, account uint32, - minconf int32, randomizeChangeIdx bool) (*txauthor.AuthoredTx, error) { - - n, err := w.NetworkBackend() - if err != nil { - return nil, errors.E(op, err) - } - - return w.txToOutputsInternal(op, outputs, account, minconf, n, - randomizeChangeIdx, w.RelayFee()) -} - -// txToOutputsInternal creates a signed transaction which includes each output +// txToOutputs creates a signed transaction which includes each output // from outputs. Previous outputs to reedeem are chosen from the passed // account's UTXO set and minconf policy. An additional output may be added to // return change to the wallet. An appropriate fee is included based on the @@ -333,8 +319,8 @@ func (w *Wallet) txToOutputs(op errors.Op, outputs []*wire.TxOut, account uint32 // Decred: This func also sends the transaction, and if successful, inserts it // into the database, rather than delegating this work to the caller as // btcwallet does. -func (w *Wallet) txToOutputsInternal(op errors.Op, outputs []*wire.TxOut, account uint32, minconf int32, - n NetworkBackend, randomizeChangeIdx bool, txFee dcrutil.Amount) (*txauthor.AuthoredTx, error) { +func (w *Wallet) txToOutputs(op errors.Op, outputs []*wire.TxOut, + n NetworkBackend, options *CreateTxOptions) (*txauthor.AuthoredTx, error) { var unlockOutpoints []*wire.OutPoint defer func() { @@ -353,6 +339,7 @@ func (w *Wallet) txToOutputsInternal(op errors.Op, outputs []*wire.TxOut, accoun var atx *txauthor.AuthoredTx var changeSourceUpdates []func(walletdb.ReadWriteTx) error + err := walletdb.View(w.db, func(dbtx walletdb.ReadTx) error { addrmgrNs := dbtx.ReadBucket(waddrmgrNamespaceKey) txmgrNs := dbtx.ReadBucket(wtxmgrNamespaceKey) @@ -363,16 +350,30 @@ func (w *Wallet) txToOutputsInternal(op errors.Op, outputs []*wire.TxOut, accoun // Create the unsigned transaction. _, tipHeight := w.TxStore.MainChainTip(txmgrNs) - inputSource := w.TxStore.MakeIgnoredInputSource(txmgrNs, addrmgrNs, account, - minconf, tipHeight, ignoreInput) + inputSource := w.TxStore.MakeIgnoredInputSource(txmgrNs, addrmgrNs, + options.account(), options.minConf(), + tipHeight, ignoreInput) changeSource := &p2PKHChangeSource{ persist: w.deferPersistReturnedChild(&changeSourceUpdates), - account: account, + account: options.account(), wallet: w, } var err error - atx, err = txauthor.NewUnsignedTransaction(outputs, txFee, - inputSource.SelectInputs, changeSource) + + if options.recipientPaysFee() { + if len(outputs) != 1 { + return errors.E(errors.Invalid, "expected exactly one output for "+ + "transaction where recipient pays the fee") + } + + output := outputs[0] + atx, err = newUnsignedTransactionMinusFee(op, output, + options.relayFee(w), inputSource.SelectInputs, changeSource) + } else { + atx, err = txauthor.NewUnsignedTransaction(outputs, + options.relayFee(w), inputSource.SelectInputs, changeSource) + } + if err != nil { return err } @@ -385,7 +386,7 @@ func (w *Wallet) txToOutputsInternal(op errors.Op, outputs []*wire.TxOut, accoun // Randomize change position, if change exists, before signing. This // doesn't affect the serialize size, so the change amount will still be // valid. - if atx.ChangeIndex >= 0 && randomizeChangeIdx { + if atx.ChangeIndex >= 0 && options.randomizeChangeIndex() { atx.RandomizeChangePosition() } @@ -409,7 +410,7 @@ func (w *Wallet) txToOutputsInternal(op errors.Op, outputs []*wire.TxOut, accoun // Warn when spending UTXOs controlled by imported keys created change for // the default account. - if atx.ChangeIndex >= 0 && account == udb.ImportedAddrAccount { + if atx.ChangeIndex >= 0 && options.account() == udb.ImportedAddrAccount { changeAmount := dcrutil.Amount(atx.Tx.TxOut[atx.ChangeIndex].Value) log.Warnf("Spend from imported account produced change: moving"+ " %v from imported account into default account.", changeAmount) @@ -468,6 +469,47 @@ func (w *Wallet) txToOutputsInternal(op errors.Op, outputs []*wire.TxOut, accoun return atx, nil } +// newUnsignedTransactionMinusFee creates an unsigned transaction paying to one +// non-change output. An appropriate transaction fee is included based on the +// transaction size, and is subtracted from the provided `output` parameter. +// +// The behavior of this method mirrors a call to `NewUnsignedTransaction` with the differences being that: +// * this method takes a single `output` argument +// * all fees are subtracted from the provided output rather than from change. +// * the output in the return value is a shallow copy of the output argument and references the original output script. +func newUnsignedTransactionMinusFee(op errors.Op, output *wire.TxOut, relayFeePerKb dcrutil.Amount, + fetchInputs txauthor.InputSource, fetchChange txauthor.ChangeSource) (*txauthor.AuthoredTx, error) { + + // Create shallow copy of `output` to include in return value since the `Value` property will be mutated + // Still retains a reference to the output script array. + outputCopy := &wire.TxOut{ + PkScript: output.PkScript, + Version: output.Version, + Value: output.Value, + } + + // Since the fee will come directly from the full output amount, + // defer fee calculation until enough inputs have been consumed. + authoredTx, err := txauthor.NewUnsignedTransaction([]*wire.TxOut{outputCopy}, + 0, fetchInputs, fetchChange) + if err != nil { + return nil, err + } + + // At this point, all inputs have been determined and the estimated size of the transaction is fixed. + // Calculate the fee and subtract it from the copied instance of the provided output. + feeAmount := txrules.FeeForSerializeSize(relayFeePerKb, + authoredTx.EstimatedSignedSerializeSize) + outputCopy.Value -= int64(feeAmount) + + // Mirror the behavior of txauthor.NewUnsignedTransaction when the fee exceeds the amount to send. + if outputCopy.Value < 0 { + return nil, errors.E(op, errors.InsufficientBalance) + } + + return authoredTx, nil +} + // txToMultisig spends funds to a multisig output, partially signs the // transaction, then returns fund func (w *Wallet) txToMultisig(op errors.Op, account uint32, amount dcrutil.Amount, pubkeys []*dcrutil.AddressSecpPubKey, @@ -1160,8 +1202,16 @@ func (w *Wallet) purchaseTickets(ctx context.Context, op errors.Op, n NetworkBac if txFeeIncrement == 0 { txFeeIncrement = w.RelayFee() } - splitTx, err := w.txToOutputsInternal(op, splitOuts, account, req.MinConf, - n, false, txFeeIncrement) + + options := &CreateTxOptions{ + RandomizeChangeIndex: dcrjson.Bool(false), + RelayFeePerKb: dcrjson.Int64(int64(txFeeIncrement)), + MinimumConfirmations: dcrjson.Int32(req.MinConf), + Account: dcrjson.Uint32(account), + } + + splitTx, err := w.txToOutputs(op, splitOuts, n, options) + if err != nil { return nil, err } diff --git a/wallet/createtx_test.go b/wallet/createtx_test.go new file mode 100644 index 000000000..5bfd7fe39 --- /dev/null +++ b/wallet/createtx_test.go @@ -0,0 +1,187 @@ +package wallet + +import ( + "testing" + + "github.com/decred/dcrd/dcrutil" + "github.com/decred/dcrd/wire" + "github.com/decred/dcrwallet/errors" + "github.com/decred/dcrwallet/wallet/v2/txauthor" + . "github.com/decred/dcrwallet/wallet/v2/txauthor" + "github.com/decred/dcrwallet/wallet/v2/txrules" + + "github.com/decred/dcrwallet/wallet/v2/internal/txsizes" +) + + +type AuthorTestChangeSource struct{} + +func (src AuthorTestChangeSource) Script() ([]byte, uint16, error) { + // Only length matters for these tests. + return make([]byte, txsizes.P2PKHPkScriptSize), 0, nil +} + +func (src AuthorTestChangeSource) ScriptSize() int { + return txsizes.P2PKHPkScriptSize +} + +func p2pkhOutputs(amounts ...dcrutil.Amount) []*wire.TxOut { + v := make([]*wire.TxOut, 0, len(amounts)) + for _, a := range amounts { + outScript := make([]byte, txsizes.P2PKHOutputSize) + v = append(v, wire.NewTxOut(int64(a), outScript)) + } + return v +} + +func makeInputSource(unspents []*wire.TxOut) InputSource { + // Return outputs in order. + currentTotal := dcrutil.Amount(0) + currentInputs := make([]*wire.TxIn, 0, len(unspents)) + redeemScriptSizes := make([]int, 0, len(unspents)) + f := func(target dcrutil.Amount) (*InputDetail, error) { + for currentTotal < target && len(unspents) != 0 { + u := unspents[0] + unspents = unspents[1:] + nextInput := wire.NewTxIn(&wire.OutPoint{}, u.Value, nil) + currentTotal += dcrutil.Amount(u.Value) + currentInputs = append(currentInputs, nextInput) + redeemScriptSizes = append(redeemScriptSizes, txsizes.RedeemP2PKHSigScriptSize) + } + + inputDetail := txauthor.InputDetail{ + Amount: currentTotal, + Inputs: currentInputs, + Scripts: make([][]byte, len(currentInputs)), + RedeemScriptSizes: redeemScriptSizes, + } + return &inputDetail, nil + } + return InputSource(f) +} + +func TestNewUnsignedTransactionMinusFee(t *testing.T) { + const op errors.Op = "createtx.TestNewUnsignedTransactionMinusFee" + + tests := []struct { + UnspentOutputs []*wire.TxOut + Output *wire.TxOut + RelayFee dcrutil.Amount + ExpectedChange dcrutil.Amount + ShouldError bool + ExpectedError errors.Kind + }{ + 0: { + // Spend exactly what we have available, but would be negative since fee is + // +1 more than available + UnspentOutputs: p2pkhOutputs(227), + Output: p2pkhOutputs(227)[0], + RelayFee: 1e3, + ShouldError: true, + ExpectedError: errors.InsufficientBalance, + }, + 1: { + // Spend all inputs, and do not fail if dust + UnspentOutputs: p2pkhOutputs(228), + Output: p2pkhOutputs(228)[0], + RelayFee: 1e3, + ShouldError: false, + }, + 2: { + // Spend exactly what we have available but enough for fee + UnspentOutputs: p2pkhOutputs(1e8), + Output: p2pkhOutputs(1e8)[0], + RelayFee: 1e3, + ShouldError: false, + }, + 3: { + // Spend more than we have available + UnspentOutputs: p2pkhOutputs(1e6), + Output: p2pkhOutputs(1e6 + 1)[0], + RelayFee: 1e3, + ShouldError: true, + ExpectedError: errors.InsufficientBalance, + }, + 4: { + // Expect change exactly to equal input - (output without fee) + // Output should have fee subtracted. + UnspentOutputs: p2pkhOutputs(2e6), + Output: p2pkhOutputs(1e6)[0], + RelayFee: 1e3, + ExpectedChange: 1e6, + ShouldError: false, + }, + 5: { + // Make sure we get expected change + UnspentOutputs: p2pkhOutputs(2), + Output: p2pkhOutputs(1)[0], + RelayFee: 0, + ExpectedChange: 1, + ShouldError: false, + }, + } + + var changeSource AuthorTestChangeSource + + for i, test := range tests { + inputSource := makeInputSource(test.UnspentOutputs) + tx, err := newUnsignedTransactionMinusFee(op, test.Output, test.RelayFee, + inputSource, changeSource) + + if test.ShouldError { + if err == nil { + t.Errorf("Test %d: Expected error but one was not returned", i) + continue + } else if !errors.Is(test.ExpectedError, err) { + t.Errorf("Test %d: Error=%v expected %v", i, err, test.ExpectedError) + continue + } else { + // pass, got expected error + continue + } + } else { + if err != nil { + t.Errorf("Test %d: Unexpected error: %v", i, err) + continue + } else { + // no unexpected errors, carry on... + } + } + + if test.ExpectedChange > 0 && tx.ChangeIndex < 0 { + t.Errorf("Test %d: Expected change value (%v) but no change returned", i, test.ExpectedChange) + continue + } + + outputIndex := 0 + if tx.ChangeIndex >= 0 { + outputIndex = tx.ChangeIndex ^ 1 + } + + outputValue := tx.Tx.TxOut[outputIndex].Value + expectedFee := int64(txrules.FeeForSerializeSize(test.RelayFee, tx.EstimatedSignedSerializeSize)) + + // Make sure the resulting TxOut is not the same reference as the `output` argument + if test.Output == tx.Tx.TxOut[outputIndex] { + t.Errorf("Test %d: Expected returned TxOut reference to not equal test output reference", i) + continue + } + + // Adding the fee back to the output should give original value + if outputValue+expectedFee != test.Output.Value { + t.Errorf("Test %d: Expected output value (%v) plus fee (%v) to equal original output value (%v)", + i, outputValue, expectedFee, test.Output.Value) + continue + } + + // If there is change, make sure it's the expected value. + if tx.ChangeIndex >= 0 { + changeValue := tx.Tx.TxOut[tx.ChangeIndex].Value + if changeValue != int64(test.ExpectedChange) { + t.Errorf("Test %d: Got change amount %v, Expected %v", + i, changeValue, test.ExpectedChange) + continue + } + } + } +} diff --git a/wallet/wallet.go b/wallet/wallet.go index 81424ed26..e2e63abfb 100644 --- a/wallet/wallet.go +++ b/wallet/wallet.go @@ -3774,13 +3774,93 @@ func (w *Wallet) TotalReceivedForAddr(addr dcrutil.Address, minConf int32) (dcru return amount, nil } -// SendOutputs creates and sends payment transactions. It returns the -// transaction hash upon success +// CreateTxOptions allows changing the default behavior of how outputs are +// constructed. Nil values are set to default values. +type CreateTxOptions struct { + // Determines if the fee comes from the change output or from the recipient. + // Defaults to false. + RecipientPaysFee *bool + + // Indicates whether the change output should be randomized. + // Defaults to true. + RandomizeChangeIndex *bool + + // Defines the relay fee for the entire paid, per kb. + // Defaults to the wallet's relay fee + RelayFeePerKb *int64 + + // Minimum utxo age to consider as valid inputs + // Defaults to 1 block + MinimumConfirmations *int32 + + // Account number to use + // Defaults to the wallet's default account number + Account *uint32 +} + +func (o *CreateTxOptions) recipientPaysFee() bool { + if o == nil || o.RecipientPaysFee == nil { + return false + } + + return *o.RecipientPaysFee +} + +func (o *CreateTxOptions) randomizeChangeIndex() bool { + if o == nil || o.RandomizeChangeIndex == nil { + return true + } + + return *o.RandomizeChangeIndex +} + +func (o *CreateTxOptions) relayFee(w *Wallet) dcrutil.Amount { + if o == nil || o.RelayFeePerKb == nil { + return w.RelayFee() + } + + return dcrutil.Amount(*o.RelayFeePerKb) +} + +func (o *CreateTxOptions) minConf() int32 { + if o == nil || o.MinimumConfirmations == nil { + return 1 + } + + return *o.MinimumConfirmations +} + +func (o *CreateTxOptions) account() uint32 { + if o == nil || o.Account == nil { + return 1 + } + + return *o.Account +} + +// SendOutputs creates and sends payment transactions. +// It returns the transaction hash upon success. +// +// Deprecated: Use SendOutputsWithOptions +// When building the transaction, miner fees are subtracted from the change output. func (w *Wallet) SendOutputs(outputs []*wire.TxOut, account uint32, minconf int32) (*chainhash.Hash, error) { - const op errors.Op = "wallet.SendOutputs" - relayFee := w.RelayFee() + options := &CreateTxOptions{ + RandomizeChangeIndex: dcrjson.Bool(true), + MinimumConfirmations: dcrjson.Int32(minconf), + Account: dcrjson.Uint32(account), + } + + return w.SendOutputsWithOptions(outputs, options) +} + +// SendOutputsWithOptions creates and sends payment transactions with options. +// It returns the transaction hash upon success. +func (w *Wallet) SendOutputsWithOptions(outputs []*wire.TxOut, + options *CreateTxOptions) (*chainhash.Hash, error) { + const op errors.Op = "wallet.SendOutputsWithOptions" + for _, output := range outputs { - err := txrules.CheckOutput(output, relayFee) + err := txrules.CheckOutput(output, options.relayFee(w)) if err != nil { return nil, errors.E(op, err) } @@ -3791,7 +3871,13 @@ func (w *Wallet) SendOutputs(outputs []*wire.TxOut, account uint32, minconf int3 return nil, err } defer heldUnlock.release() - tx, err := w.txToOutputs("wallet.SendOutputs", outputs, account, minconf, true) + + n, err := w.NetworkBackend() + if err != nil { + return nil, errors.E(op, err) + } + + tx, err := w.txToOutputs(op, outputs, n, options) if err != nil { return nil, err }