Skip to content

Commit 82bb32c

Browse files
committed
Add "sendCoins" option for name operations.
The new option "sendCoins" allows to send also coins together with name operations (name_new, name_firstupdate, name_update), similar to sendmany (but combined with a name operation).
1 parent 0151bbd commit 82bb32c

File tree

4 files changed

+224
-15
lines changed

4 files changed

+224
-15
lines changed

src/wallet/rpcnames.cpp

Lines changed: 123 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,23 +4,27 @@
44

55
#include <base58.h>
66
#include <coins.h>
7+
#include <consensus/validation.h>
78
#include <init.h>
89
#include <key_io.h>
910
#include <names/common.h>
1011
#include <names/main.h>
12+
#include <net.h>
1113
#include <primitives/transaction.h>
1214
#include <random.h>
1315
#include <rpc/mining.h>
1416
#include <rpc/server.h>
1517
#include <script/names.h>
1618
#include <txmempool.h>
1719
#include <util.h>
20+
#include <utilmoneystr.h>
1821
#include <validation.h>
1922
#include <wallet/coincontrol.h>
2023
#include <wallet/wallet.h>
2124

2225
#include <univalue.h>
2326

27+
#include <algorithm>
2428
#include <memory>
2529

2630
/* ************************************************************************** */
@@ -116,9 +120,110 @@ std::string getNameOpOptionsHelp ()
116120
{
117121
return " {\n"
118122
" \"destAddress\" (string, optional) The address to send the name output to\n"
123+
" \"sendCoins\" (object, optional) Addresses to which coins should be sent additionally\n"
124+
" {\n"
125+
" \"addr1\": x,\n"
126+
" \"addr2\": y,\n"
127+
" ...\n"
128+
" }\n"
119129
" }\n";
120130
}
121131

132+
/**
133+
* Sends a name output to the given name script. This is the "final" step that
134+
* is common between name_new, name_firstupdate and name_update. This method
135+
* also implements the "sendCoins" option, if included.
136+
*/
137+
CTransactionRef
138+
SendNameOutput (CWallet& wallet, const CScript& nameOutScript,
139+
const CTxIn* nameInput, const UniValue& opt)
140+
{
141+
RPCTypeCheckObj (opt,
142+
{
143+
{"sendCoins", UniValueType (UniValue::VOBJ)},
144+
},
145+
true, false);
146+
147+
if (wallet.GetBroadcastTransactions () && !g_connman)
148+
throw JSONRPCError (RPC_CLIENT_P2P_DISABLED,
149+
"Error: Peer-to-peer functionality missing"
150+
" or disabled");
151+
152+
std::vector<CRecipient> vecSend;
153+
vecSend.push_back ({nameOutScript, NAME_LOCKED_AMOUNT, false});
154+
155+
if (opt.exists ("sendCoins"))
156+
for (const std::string& addr : opt["sendCoins"].getKeys ())
157+
{
158+
const CTxDestination dest = DecodeDestination (addr);
159+
if (!IsValidDestination (dest))
160+
throw JSONRPCError (RPC_INVALID_ADDRESS_OR_KEY,
161+
"Invalid address: " + addr);
162+
163+
const CAmount nAmount = AmountFromValue (opt["sendCoins"][addr]);
164+
if (nAmount <= 0)
165+
throw JSONRPCError (RPC_TYPE_ERROR, "Invalid amount for send");
166+
167+
vecSend.push_back ({GetScriptForDestination (dest), nAmount, false});
168+
}
169+
170+
/* Shuffle the recipient list for privacy. */
171+
std::shuffle (vecSend.begin (), vecSend.end (), FastRandomContext ());
172+
173+
/* Check balance against total amount sent. If we have a name input, we have
174+
to take its value into account. */
175+
176+
const CAmount curBalance = wallet.GetBalance ();
177+
178+
CAmount totalSpend = 0;
179+
for (const auto& recv : vecSend)
180+
totalSpend += recv.nAmount;
181+
182+
CAmount lockedValue = 0;
183+
std::string strError;
184+
if (nameInput != nullptr)
185+
{
186+
const CWalletTx* dummyWalletTx;
187+
if (!wallet.FindValueInNameInput (*nameInput, lockedValue,
188+
dummyWalletTx, strError))
189+
throw JSONRPCError(RPC_WALLET_ERROR, strError);
190+
}
191+
192+
if (totalSpend > curBalance + lockedValue)
193+
throw JSONRPCError (RPC_WALLET_INSUFFICIENT_FUNDS, "Insufficient funds");
194+
195+
/* Create and send the transaction. This code is based on the corresponding
196+
part of SendMoneyToScript and should stay in sync. */
197+
198+
CCoinControl coinControl;
199+
CReserveKey keyChange(&wallet);
200+
CAmount nFeeRequired;
201+
int nChangePosRet = -1;
202+
203+
CTransactionRef tx;
204+
if (!wallet.CreateTransaction (vecSend, nameInput, tx, keyChange,
205+
nFeeRequired, nChangePosRet, strError,
206+
coinControl))
207+
{
208+
if (totalSpend + nFeeRequired > curBalance)
209+
strError = strprintf ("Error: This transaction requires a transaction"
210+
" fee of at least %s",
211+
FormatMoney (nFeeRequired));
212+
throw JSONRPCError (RPC_WALLET_ERROR, strError);
213+
}
214+
215+
CValidationState state;
216+
if (!wallet.CommitTransaction (tx, {}, {}, {}, keyChange, g_connman.get (),
217+
state))
218+
{
219+
strError = strprintf ("Error: The transaction was rejected!"
220+
" Reason given: %s", FormatStateMessage (state));
221+
throw JSONRPCError (RPC_WALLET_ERROR, strError);
222+
}
223+
224+
return tx;
225+
}
226+
122227
} // anonymous namespace
123228
/* ************************************************************************** */
124229

@@ -263,6 +368,10 @@ name_new (const JSONRPCRequest& request)
263368
if (name.size () > MAX_NAME_LENGTH)
264369
throw JSONRPCError (RPC_INVALID_PARAMETER, "the name is too long");
265370

371+
UniValue options(UniValue::VOBJ);
372+
if (request.params.size () >= 2)
373+
options = request.params[1].get_obj ();
374+
266375
valtype rand(20);
267376
GetRandBytes (&rand[0], rand.size ());
268377

@@ -277,16 +386,13 @@ name_new (const JSONRPCRequest& request)
277386
EnsureWalletIsUnlocked (pwallet);
278387

279388
DestinationAddressHelper destHelper(*pwallet);
280-
if (request.params.size () >= 2)
281-
destHelper.setOptions (request.params[1].get_obj ());
389+
destHelper.setOptions (options);
282390

283391
const CScript newScript
284392
= CNameScript::buildNameNew (destHelper.getScript (), hash);
285393

286394
CCoinControl coinControl;
287-
CTransactionRef tx = SendMoneyToScript (pwallet, newScript, nullptr,
288-
NAME_LOCKED_AMOUNT, false,
289-
coinControl, {}, {});
395+
CTransactionRef tx = SendNameOutput (*pwallet, newScript, nullptr, options);
290396
destHelper.finalise ();
291397

292398
const std::string randStr = HexStr (rand);
@@ -405,6 +511,10 @@ name_firstupdate (const JSONRPCRequest& request)
405511
if (value.size () > MAX_VALUE_LENGTH_UI)
406512
throw JSONRPCError (RPC_INVALID_PARAMETER, "the value is too long");
407513

514+
UniValue options(UniValue::VOBJ);
515+
if (request.params.size () >= 5)
516+
options = request.params[4].get_obj ();
517+
408518
{
409519
LOCK (mempool.cs);
410520
if (mempool.registersName (name))
@@ -444,17 +554,14 @@ name_firstupdate (const JSONRPCRequest& request)
444554
EnsureWalletIsUnlocked (pwallet);
445555

446556
DestinationAddressHelper destHelper(*pwallet);
447-
if (request.params.size () >= 5)
448-
destHelper.setOptions (request.params[4].get_obj ());
557+
destHelper.setOptions (options);
449558

450559
const CScript nameScript
451560
= CNameScript::buildNameFirstupdate (destHelper.getScript (), name, value,
452561
rand);
453562

454563
CCoinControl coinControl;
455-
CTransactionRef tx = SendMoneyToScript (pwallet, nameScript, &txIn,
456-
NAME_LOCKED_AMOUNT, false,
457-
coinControl, {}, {});
564+
CTransactionRef tx = SendNameOutput (*pwallet, nameScript, &txIn, options);
458565
destHelper.finalise ();
459566

460567
return tx->GetHash ().GetHex ();
@@ -501,6 +608,10 @@ name_update (const JSONRPCRequest& request)
501608
if (value.size () > MAX_VALUE_LENGTH_UI)
502609
throw JSONRPCError (RPC_INVALID_PARAMETER, "the value is too long");
503610

611+
UniValue options(UniValue::VOBJ);
612+
if (request.params.size () >= 3)
613+
options = request.params[2].get_obj ();
614+
504615
/* Reject updates to a name for which the mempool already has
505616
a pending update. This is not a hard rule enforced by network
506617
rules, but it is necessary with the current mempool implementation. */
@@ -527,16 +638,13 @@ name_update (const JSONRPCRequest& request)
527638
EnsureWalletIsUnlocked (pwallet);
528639

529640
DestinationAddressHelper destHelper(*pwallet);
530-
if (request.params.size () >= 3)
531-
destHelper.setOptions (request.params[2].get_obj ());
641+
destHelper.setOptions (options);
532642

533643
const CScript nameScript
534644
= CNameScript::buildNameUpdate (destHelper.getScript (), name, value);
535645

536646
CCoinControl coinControl;
537-
CTransactionRef tx = SendMoneyToScript (pwallet, nameScript, &txIn,
538-
NAME_LOCKED_AMOUNT, false,
539-
coinControl, {}, {});
647+
CTransactionRef tx = SendNameOutput (*pwallet, nameScript, &txIn, options);
540648
destHelper.finalise ();
541649

542650
return tx->GetHash ().GetHex ();

test/functional/name_sendcoins.py

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
#!/usr/bin/env python3
2+
# Copyright (c) 2018 Daniel Kraft
3+
# Distributed under the MIT/X11 software license, see the accompanying
4+
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
5+
6+
# RPC test for the "sendCoins" option with name operations.
7+
8+
from test_framework.names import NameTestFramework
9+
from test_framework.util import *
10+
11+
class NameSendCoinsTest (NameTestFramework):
12+
13+
def set_test_params (self):
14+
self.setup_name_test ([[]] * 1)
15+
16+
def verifyTx (self, txid, expected):
17+
"""Verify that the given tx sends currency to the expected recipients."""
18+
19+
tx = self.nodes[0].getrawtransaction (txid, 1)
20+
vout = tx['vout']
21+
22+
# There should be two additional outputs: name and change
23+
assert_equal (len (vout), len (expected) + 2)
24+
25+
actual = {}
26+
for out in vout:
27+
if 'nameOp' in out['scriptPubKey']:
28+
continue
29+
addr = out['scriptPubKey']['addresses']
30+
assert_equal (len (addr), 1)
31+
addr = addr[0]
32+
if not addr in expected:
33+
# This must be the change address. Through the assertion above about
34+
# the expected sizes, we make sure that the test fails if there is
35+
# not exactly one key with this property.
36+
continue
37+
actual[addr] = out['value']
38+
assert_equal (actual, expected)
39+
40+
def run_test (self):
41+
42+
# Send name_new with sendCoins option and verify it worked as expected.
43+
addr1 = self.nodes[0].getnewaddress ()
44+
addr2 = self.nodes[0].getnewaddress ()
45+
sendCoins = {addr1: 1, addr2: 2}
46+
new = self.nodes[0].name_new ("testname", {"sendCoins": sendCoins})
47+
self.generate (0, 10)
48+
self.verifyTx (new[0], sendCoins)
49+
50+
# Check that it also works with first_update.
51+
txid = self.firstupdateName (0, "testname", new, "value",
52+
{"sendCoins": sendCoins})
53+
self.generate (0, 5)
54+
self.verifyTx (txid, sendCoins)
55+
56+
# Test different variantions (numbers of target addresses) with name_update.
57+
for n in range (5):
58+
sendCoins = {}
59+
for i in range (n):
60+
sendCoins[self.nodes[0].getnewaddress ()] = 42 + i
61+
txid = self.nodes[0].name_update ("testname", "value",
62+
{"sendCoins": sendCoins})
63+
self.generate (0, 1)
64+
self.verifyTx (txid, sendCoins)
65+
66+
# Verify the range check for amount and the address validation.
67+
assert_raises_rpc_error (-3, 'Invalid amount for send',
68+
self.nodes[0].name_update,
69+
"testname", "value", {"sendCoins": {addr1: 0}})
70+
assert_raises_rpc_error (-5, 'Invalid address',
71+
self.nodes[0].name_update,
72+
"testname", "value", {"sendCoins": {"x": 1}})
73+
74+
# Verify the insufficient funds check, both where it fails a priori
75+
# and where we just don't have enough for the fee.
76+
balance = self.nodes[0].getbalance ()
77+
assert_raises_rpc_error (-6, 'Insufficient funds',
78+
self.nodes[0].name_update,
79+
"testname", "value",
80+
{"sendCoins": {addr1: balance + 1}})
81+
assert_raises_rpc_error (-4, 'requires a transaction fee',
82+
self.nodes[0].name_update,
83+
"testname", "value",
84+
{"sendCoins": {addr1: balance}})
85+
86+
# Check that we can send a name_update that spends almost all funds in
87+
# the wallet. We only need to keep a tiny amount to pay for the fee
88+
# (but less than the locked amount of 0.01).
89+
keep = Decimal ("0.009")
90+
sendCoins = {addr1: balance - keep}
91+
txid = self.nodes[0].name_update ("testname", "value",
92+
{"sendCoins": sendCoins})
93+
self.generate (0, 1)
94+
self.verifyTx (txid, sendCoins)
95+
96+
if __name__ == '__main__':
97+
NameSendCoinsTest ().main ()

test/functional/run_name_tests.sh

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,5 +24,8 @@ echo "\nName reorgs..."
2424
echo "\nName scanning..."
2525
./name_scanning.py
2626

27+
echo "\nName operation with sendCoins..."
28+
./name_sendcoins.py
29+
2730
echo "\nName wallet..."
2831
./name_wallet.py

test/functional/test_runner.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,7 @@
166166
'name_registration.py',
167167
'name_reorg.py',
168168
'name_scanning.py',
169+
'name_sendcoins.py',
169170
'name_wallet.py',
170171
]
171172

0 commit comments

Comments
 (0)