mirror of
https://github.com/dashpay/dash.git
synced 2024-12-27 04:52:59 +01:00
[rpc] createrawtransaction: Accept sorted outputs
This commit is contained in:
parent
8acd25d854
commit
fa06dfce0f
@ -61,7 +61,8 @@ RPC changes
|
|||||||
|
|
||||||
### Low-level changes
|
### Low-level changes
|
||||||
|
|
||||||
- The `fundrawtransaction` rpc will reject the previously deprecated `reserveChangeKey` option.
|
- The `createrawtransaction` RPC will now accept an array or dictionary (kept for compatibility) for the `outputs` parameter. This means the order of transaction outputs can be specified by the client.
|
||||||
|
- The `fundrawtransaction` RPC will reject the previously deprecated `reserveChangeKey` option.
|
||||||
|
|
||||||
Credits
|
Credits
|
||||||
=======
|
=======
|
||||||
|
@ -316,9 +316,10 @@ UniValue verifytxoutproof(const JSONRPCRequest& request)
|
|||||||
|
|
||||||
UniValue createrawtransaction(const JSONRPCRequest& request)
|
UniValue createrawtransaction(const JSONRPCRequest& request)
|
||||||
{
|
{
|
||||||
if (request.fHelp || request.params.size() < 2 || request.params.size() > 4)
|
if (request.fHelp || request.params.size() < 2 || request.params.size() > 4) {
|
||||||
throw std::runtime_error(
|
throw std::runtime_error(
|
||||||
"createrawtransaction [{\"txid\":\"id\",\"vout\":n},...] {\"address\":amount,\"data\":\"hex\",...} ( locktime ) ( replaceable )\n"
|
// clang-format off
|
||||||
|
"createrawtransaction [{\"txid\":\"id\",\"vout\":n},...] [{\"address\":amount},{\"data\":\"hex\"},...] ( locktime ) ( replaceable )\n"
|
||||||
"\nCreate a transaction spending the given inputs and creating new outputs.\n"
|
"\nCreate a transaction spending the given inputs and creating new outputs.\n"
|
||||||
"Outputs can be addresses or data.\n"
|
"Outputs can be addresses or data.\n"
|
||||||
"Returns hex-encoded raw transaction.\n"
|
"Returns hex-encoded raw transaction.\n"
|
||||||
@ -335,12 +336,17 @@ UniValue createrawtransaction(const JSONRPCRequest& request)
|
|||||||
" } \n"
|
" } \n"
|
||||||
" ,...\n"
|
" ,...\n"
|
||||||
" ]\n"
|
" ]\n"
|
||||||
"2. \"outputs\" (object, required) a json object with outputs\n"
|
"2. \"outputs\" (array, required) a json array with outputs (key-value pairs)\n"
|
||||||
|
" [\n"
|
||||||
" {\n"
|
" {\n"
|
||||||
" \"address\": x.xxx, (numeric or string, required) The key is the bitcoin address, the numeric value (can be string) is the " + CURRENCY_UNIT + " amount\n"
|
" \"address\": x.xxx, (obj, optional) A key-value pair. The key (string) is the bitcoin address, the value (float or string) is the amount in " + CURRENCY_UNIT + "\n"
|
||||||
" \"data\": \"hex\" (string, required) The key is \"data\", the value is hex encoded data\n"
|
" },\n"
|
||||||
" ,...\n"
|
" {\n"
|
||||||
|
" \"data\": \"hex\" (obj, optional) A key-value pair. The key must be \"data\", the value is hex encoded data\n"
|
||||||
" }\n"
|
" }\n"
|
||||||
|
" ,... More key-value pairs of the above form. For compatibility reasons, a dictionary, which holds the key-value pairs directly, is also\n"
|
||||||
|
" accepted as second parameter.\n"
|
||||||
|
" ]\n"
|
||||||
"3. locktime (numeric, optional, default=0) Raw locktime. Non-0 value also locktime-activates inputs\n"
|
"3. locktime (numeric, optional, default=0) Raw locktime. Non-0 value also locktime-activates inputs\n"
|
||||||
"4. replaceable (boolean, optional, default=false) Marks this transaction as BIP125 replaceable.\n"
|
"4. replaceable (boolean, optional, default=false) Marks this transaction as BIP125 replaceable.\n"
|
||||||
" Allows this transaction to be replaced by a transaction with higher fees. If provided, it is an error if explicit sequence numbers are incompatible.\n"
|
" Allows this transaction to be replaced by a transaction with higher fees. If provided, it is an error if explicit sequence numbers are incompatible.\n"
|
||||||
@ -352,14 +358,25 @@ UniValue createrawtransaction(const JSONRPCRequest& request)
|
|||||||
+ HelpExampleCli("createrawtransaction", "\"[{\\\"txid\\\":\\\"myid\\\",\\\"vout\\\":0}]\" \"{\\\"data\\\":\\\"00010203\\\"}\"")
|
+ HelpExampleCli("createrawtransaction", "\"[{\\\"txid\\\":\\\"myid\\\",\\\"vout\\\":0}]\" \"{\\\"data\\\":\\\"00010203\\\"}\"")
|
||||||
+ HelpExampleRpc("createrawtransaction", "\"[{\\\"txid\\\":\\\"myid\\\",\\\"vout\\\":0}]\", \"{\\\"address\\\":0.01}\"")
|
+ HelpExampleRpc("createrawtransaction", "\"[{\\\"txid\\\":\\\"myid\\\",\\\"vout\\\":0}]\", \"{\\\"address\\\":0.01}\"")
|
||||||
+ HelpExampleRpc("createrawtransaction", "\"[{\\\"txid\\\":\\\"myid\\\",\\\"vout\\\":0}]\", \"{\\\"data\\\":\\\"00010203\\\"}\"")
|
+ HelpExampleRpc("createrawtransaction", "\"[{\\\"txid\\\":\\\"myid\\\",\\\"vout\\\":0}]\", \"{\\\"data\\\":\\\"00010203\\\"}\"")
|
||||||
|
// clang-format on
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
RPCTypeCheck(request.params, {UniValue::VARR, UniValue::VOBJ, UniValue::VNUM, UniValue::VBOOL}, true);
|
RPCTypeCheck(request.params, {
|
||||||
|
UniValue::VARR,
|
||||||
|
UniValueType(), // ARR or OBJ, checked later
|
||||||
|
UniValue::VNUM,
|
||||||
|
UniValue::VBOOL
|
||||||
|
}, true
|
||||||
|
);
|
||||||
if (request.params[0].isNull() || request.params[1].isNull())
|
if (request.params[0].isNull() || request.params[1].isNull())
|
||||||
throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid parameter, arguments 1 and 2 must be non-null");
|
throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid parameter, arguments 1 and 2 must be non-null");
|
||||||
|
|
||||||
UniValue inputs = request.params[0].get_array();
|
UniValue inputs = request.params[0].get_array();
|
||||||
UniValue sendTo = request.params[1].get_obj();
|
const bool outputs_is_obj = request.params[1].isObject();
|
||||||
|
UniValue outputs = outputs_is_obj ?
|
||||||
|
request.params[1].get_obj() :
|
||||||
|
request.params[1].get_array();
|
||||||
|
|
||||||
CMutableTransaction rawTx;
|
CMutableTransaction rawTx;
|
||||||
|
|
||||||
@ -411,11 +428,24 @@ UniValue createrawtransaction(const JSONRPCRequest& request)
|
|||||||
}
|
}
|
||||||
|
|
||||||
std::set<CTxDestination> destinations;
|
std::set<CTxDestination> destinations;
|
||||||
std::vector<std::string> addrList = sendTo.getKeys();
|
if (!outputs_is_obj) {
|
||||||
for (const std::string& name_ : addrList) {
|
// Translate array of key-value pairs into dict
|
||||||
|
UniValue outputs_dict = UniValue(UniValue::VOBJ);
|
||||||
|
for (size_t i = 0; i < outputs.size(); ++i) {
|
||||||
|
const UniValue& output = outputs[i];
|
||||||
|
if (!output.isObject()) {
|
||||||
|
throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid parameter, key-value pair not an object as expected");
|
||||||
|
}
|
||||||
|
if (output.size() != 1) {
|
||||||
|
throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid parameter, key-value pair must contain exactly one key");
|
||||||
|
}
|
||||||
|
outputs_dict.pushKVs(output);
|
||||||
|
}
|
||||||
|
outputs = std::move(outputs_dict);
|
||||||
|
}
|
||||||
|
for (const std::string& name_ : outputs.getKeys()) {
|
||||||
if (name_ == "data") {
|
if (name_ == "data") {
|
||||||
std::vector<unsigned char> data = ParseHexV(sendTo[name_].getValStr(),"Data");
|
std::vector<unsigned char> data = ParseHexV(outputs[name_].getValStr(), "Data");
|
||||||
|
|
||||||
CTxOut out(0, CScript() << OP_RETURN << data);
|
CTxOut out(0, CScript() << OP_RETURN << data);
|
||||||
rawTx.vout.push_back(out);
|
rawTx.vout.push_back(out);
|
||||||
@ -430,7 +460,7 @@ UniValue createrawtransaction(const JSONRPCRequest& request)
|
|||||||
}
|
}
|
||||||
|
|
||||||
CScript scriptPubKey = GetScriptForDestination(destination);
|
CScript scriptPubKey = GetScriptForDestination(destination);
|
||||||
CAmount nAmount = AmountFromValue(sendTo[name_]);
|
CAmount nAmount = AmountFromValue(outputs[name_]);
|
||||||
|
|
||||||
CTxOut out(nAmount, scriptPubKey);
|
CTxOut out(nAmount, scriptPubKey);
|
||||||
rawTx.vout.push_back(out);
|
rawTx.vout.push_back(out);
|
||||||
|
@ -28,7 +28,7 @@ namespace RPCServer
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Wrapper for UniValue::VType, which includes typeAny:
|
/** Wrapper for UniValue::VType, which includes typeAny:
|
||||||
* Used to denote don't care type. Only used by RPCTypeCheckObj */
|
* Used to denote don't care type. */
|
||||||
struct UniValueType {
|
struct UniValueType {
|
||||||
UniValueType(UniValue::VType _type) : typeAny(false), type(_type) {}
|
UniValueType(UniValue::VType _type) : typeAny(false), type(_type) {}
|
||||||
UniValueType() : typeAny(true) {}
|
UniValueType() : typeAny(true) {}
|
||||||
|
@ -52,7 +52,6 @@ BOOST_AUTO_TEST_CASE(rpc_rawparams)
|
|||||||
BOOST_CHECK_THROW(CallRPC("createrawtransaction"), std::runtime_error);
|
BOOST_CHECK_THROW(CallRPC("createrawtransaction"), std::runtime_error);
|
||||||
BOOST_CHECK_THROW(CallRPC("createrawtransaction null null"), std::runtime_error);
|
BOOST_CHECK_THROW(CallRPC("createrawtransaction null null"), std::runtime_error);
|
||||||
BOOST_CHECK_THROW(CallRPC("createrawtransaction not_array"), std::runtime_error);
|
BOOST_CHECK_THROW(CallRPC("createrawtransaction not_array"), std::runtime_error);
|
||||||
BOOST_CHECK_THROW(CallRPC("createrawtransaction [] []"), std::runtime_error);
|
|
||||||
BOOST_CHECK_THROW(CallRPC("createrawtransaction {} {}"), std::runtime_error);
|
BOOST_CHECK_THROW(CallRPC("createrawtransaction {} {}"), std::runtime_error);
|
||||||
BOOST_CHECK_NO_THROW(CallRPC("createrawtransaction [] {}"));
|
BOOST_CHECK_NO_THROW(CallRPC("createrawtransaction [] {}"));
|
||||||
BOOST_CHECK_THROW(CallRPC("createrawtransaction [] {} extra"), std::runtime_error);
|
BOOST_CHECK_THROW(CallRPC("createrawtransaction [] {} extra"), std::runtime_error);
|
||||||
|
@ -12,7 +12,12 @@ Test the following RPCs:
|
|||||||
- getrawtransaction
|
- getrawtransaction
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
from collections import OrderedDict
|
||||||
|
from io import BytesIO
|
||||||
from test_framework.test_framework import BitcoinTestFramework
|
from test_framework.test_framework import BitcoinTestFramework
|
||||||
|
from test_framework.messages import (
|
||||||
|
CTransaction,
|
||||||
|
)
|
||||||
from test_framework.util import *
|
from test_framework.util import *
|
||||||
|
|
||||||
|
|
||||||
@ -46,8 +51,7 @@ class RawTransactionsTest(BitcoinTestFramework):
|
|||||||
connect_nodes_bi(self.nodes, 0, 2)
|
connect_nodes_bi(self.nodes, 0, 2)
|
||||||
|
|
||||||
def run_test(self):
|
def run_test(self):
|
||||||
|
self.log.info('prepare some coins for multiple *rawtransaction commands')
|
||||||
#prepare some coins for multiple *rawtransaction commands
|
|
||||||
self.nodes[2].generate(1)
|
self.nodes[2].generate(1)
|
||||||
self.sync_all()
|
self.sync_all()
|
||||||
self.nodes[0].generate(101)
|
self.nodes[0].generate(101)
|
||||||
@ -59,10 +63,11 @@ class RawTransactionsTest(BitcoinTestFramework):
|
|||||||
self.nodes[0].generate(5)
|
self.nodes[0].generate(5)
|
||||||
self.sync_all()
|
self.sync_all()
|
||||||
|
|
||||||
# Test getrawtransaction on genesis block coinbase returns an error
|
self.log.info('Test getrawtransaction on genesis block coinbase returns an error')
|
||||||
block = self.nodes[0].getblock(self.nodes[0].getblockhash(0))
|
block = self.nodes[0].getblock(self.nodes[0].getblockhash(0))
|
||||||
assert_raises_rpc_error(-5, "The genesis block coinbase is not considered an ordinary transaction", self.nodes[0].getrawtransaction, block['merkleroot'])
|
assert_raises_rpc_error(-5, "The genesis block coinbase is not considered an ordinary transaction", self.nodes[0].getrawtransaction, block['merkleroot'])
|
||||||
|
|
||||||
|
self.log.info('Check parameter types and required parameters of createrawtransaction')
|
||||||
# Test `createrawtransaction` required parameters
|
# Test `createrawtransaction` required parameters
|
||||||
assert_raises_rpc_error(-1, "createrawtransaction", self.nodes[0].createrawtransaction)
|
assert_raises_rpc_error(-1, "createrawtransaction", self.nodes[0].createrawtransaction)
|
||||||
assert_raises_rpc_error(-1, "createrawtransaction", self.nodes[0].createrawtransaction, [])
|
assert_raises_rpc_error(-1, "createrawtransaction", self.nodes[0].createrawtransaction, [])
|
||||||
@ -83,12 +88,18 @@ class RawTransactionsTest(BitcoinTestFramework):
|
|||||||
|
|
||||||
# Test `createrawtransaction` invalid `outputs`
|
# Test `createrawtransaction` invalid `outputs`
|
||||||
address = self.nodes[0].getnewaddress()
|
address = self.nodes[0].getnewaddress()
|
||||||
assert_raises_rpc_error(-3, "Expected type object", self.nodes[0].createrawtransaction, [], 'foo')
|
address2 = self.nodes[0].getnewaddress()
|
||||||
|
assert_raises_rpc_error(-1, "JSON value is not an array as expected", self.nodes[0].createrawtransaction, [], 'foo')
|
||||||
|
self.nodes[0].createrawtransaction(inputs=[], outputs={}) # Should not throw for backwards compatibility
|
||||||
|
self.nodes[0].createrawtransaction(inputs=[], outputs=[])
|
||||||
assert_raises_rpc_error(-8, "Data must be hexadecimal string", self.nodes[0].createrawtransaction, [], {'data': 'foo'})
|
assert_raises_rpc_error(-8, "Data must be hexadecimal string", self.nodes[0].createrawtransaction, [], {'data': 'foo'})
|
||||||
assert_raises_rpc_error(-5, "Invalid Bitcoin address", self.nodes[0].createrawtransaction, [], {'foo': 0})
|
assert_raises_rpc_error(-5, "Invalid Bitcoin address", self.nodes[0].createrawtransaction, [], {'foo': 0})
|
||||||
assert_raises_rpc_error(-3, "Invalid amount", self.nodes[0].createrawtransaction, [], {address: 'foo'})
|
assert_raises_rpc_error(-3, "Invalid amount", self.nodes[0].createrawtransaction, [], {address: 'foo'})
|
||||||
assert_raises_rpc_error(-3, "Amount out of range", self.nodes[0].createrawtransaction, [], {address: -1})
|
assert_raises_rpc_error(-3, "Amount out of range", self.nodes[0].createrawtransaction, [], {address: -1})
|
||||||
assert_raises_rpc_error(-8, "Invalid parameter, duplicated address: %s" % address, self.nodes[0].createrawtransaction, [], multidict([(address, 1), (address, 1)]))
|
assert_raises_rpc_error(-8, "Invalid parameter, duplicated address: %s" % address, self.nodes[0].createrawtransaction, [], multidict([(address, 1), (address, 1)]))
|
||||||
|
assert_raises_rpc_error(-8, "Invalid parameter, duplicated address: %s" % address, self.nodes[0].createrawtransaction, [], [{address: 1}, {address: 1}])
|
||||||
|
assert_raises_rpc_error(-8, "Invalid parameter, key-value pair must contain exactly one key", self.nodes[0].createrawtransaction, [], [{'a': 1, 'b': 2}])
|
||||||
|
assert_raises_rpc_error(-8, "Invalid parameter, key-value pair not an object as expected", self.nodes[0].createrawtransaction, [], [['key-value pair1'], ['2']])
|
||||||
|
|
||||||
# Test `createrawtransaction` invalid `locktime`
|
# Test `createrawtransaction` invalid `locktime`
|
||||||
assert_raises_rpc_error(-3, "Expected type number", self.nodes[0].createrawtransaction, [], {}, 'foo')
|
assert_raises_rpc_error(-3, "Expected type number", self.nodes[0].createrawtransaction, [], {}, 'foo')
|
||||||
@ -98,9 +109,38 @@ class RawTransactionsTest(BitcoinTestFramework):
|
|||||||
# Test `createrawtransaction` invalid `replaceable`
|
# Test `createrawtransaction` invalid `replaceable`
|
||||||
assert_raises_rpc_error(-3, "Expected type bool", self.nodes[0].createrawtransaction, [], {}, 0, 'foo')
|
assert_raises_rpc_error(-3, "Expected type bool", self.nodes[0].createrawtransaction, [], {}, 0, 'foo')
|
||||||
|
|
||||||
#########################################
|
self.log.info('Check that createrawtransaction accepts an array and object as outputs')
|
||||||
# sendrawtransaction with missing input #
|
tx = CTransaction()
|
||||||
#########################################
|
# One output
|
||||||
|
tx.deserialize(BytesIO(hex_str_to_bytes(self.nodes[2].createrawtransaction(inputs=[{'txid': txid, 'vout': 9}], outputs={address: 99}))))
|
||||||
|
assert_equal(len(tx.vout), 1)
|
||||||
|
assert_equal(
|
||||||
|
bytes_to_hex_str(tx.serialize()),
|
||||||
|
self.nodes[2].createrawtransaction(inputs=[{'txid': txid, 'vout': 9}], outputs=[{address: 99}]),
|
||||||
|
)
|
||||||
|
# Two outputs
|
||||||
|
tx.deserialize(BytesIO(hex_str_to_bytes(self.nodes[2].createrawtransaction(inputs=[{'txid': txid, 'vout': 9}], outputs=OrderedDict([(address, 99), (address2, 99)])))))
|
||||||
|
assert_equal(len(tx.vout), 2)
|
||||||
|
assert_equal(
|
||||||
|
bytes_to_hex_str(tx.serialize()),
|
||||||
|
self.nodes[2].createrawtransaction(inputs=[{'txid': txid, 'vout': 9}], outputs=[{address: 99}, {address2: 99}]),
|
||||||
|
)
|
||||||
|
# Two data outputs
|
||||||
|
tx.deserialize(BytesIO(hex_str_to_bytes(self.nodes[2].createrawtransaction(inputs=[{'txid': txid, 'vout': 9}], outputs=multidict([('data', '99'), ('data', '99')])))))
|
||||||
|
assert_equal(len(tx.vout), 2)
|
||||||
|
assert_equal(
|
||||||
|
bytes_to_hex_str(tx.serialize()),
|
||||||
|
self.nodes[2].createrawtransaction(inputs=[{'txid': txid, 'vout': 9}], outputs=[{'data': '99'}, {'data': '99'}]),
|
||||||
|
)
|
||||||
|
# Multiple mixed outputs
|
||||||
|
tx.deserialize(BytesIO(hex_str_to_bytes(self.nodes[2].createrawtransaction(inputs=[{'txid': txid, 'vout': 9}], outputs=multidict([(address, 99), ('data', '99'), ('data', '99')])))))
|
||||||
|
assert_equal(len(tx.vout), 3)
|
||||||
|
assert_equal(
|
||||||
|
bytes_to_hex_str(tx.serialize()),
|
||||||
|
self.nodes[2].createrawtransaction(inputs=[{'txid': txid, 'vout': 9}], outputs=[{address: 99}, {'data': '99'}, {'data': '99'}]),
|
||||||
|
)
|
||||||
|
|
||||||
|
self.log.info('sendrawtransaction with missing input')
|
||||||
inputs = [ {'txid' : "1d1d4e24ed99057e84c3f80fd8fbec79ed9e1acee37da269356ecea000000000", 'vout' : 1}] #won't exists
|
inputs = [ {'txid' : "1d1d4e24ed99057e84c3f80fd8fbec79ed9e1acee37da269356ecea000000000", 'vout' : 1}] #won't exists
|
||||||
outputs = { self.nodes[0].getnewaddress() : 4.998 }
|
outputs = { self.nodes[0].getnewaddress() : 4.998 }
|
||||||
rawtx = self.nodes[2].createrawtransaction(inputs, outputs)
|
rawtx = self.nodes[2].createrawtransaction(inputs, outputs)
|
||||||
@ -248,14 +288,14 @@ class RawTransactionsTest(BitcoinTestFramework):
|
|||||||
outputs = { self.nodes[0].getnewaddress() : 2.19 }
|
outputs = { self.nodes[0].getnewaddress() : 2.19 }
|
||||||
rawTx2 = self.nodes[2].createrawtransaction(inputs, outputs)
|
rawTx2 = self.nodes[2].createrawtransaction(inputs, outputs)
|
||||||
rawTxPartialSigned1 = self.nodes[1].signrawtransactionwithwallet(rawTx2, inputs)
|
rawTxPartialSigned1 = self.nodes[1].signrawtransactionwithwallet(rawTx2, inputs)
|
||||||
self.log.info(rawTxPartialSigned1)
|
self.log.debug(rawTxPartialSigned1)
|
||||||
assert_equal(rawTxPartialSigned['complete'], False) #node1 only has one key, can't comp. sign the tx
|
assert_equal(rawTxPartialSigned['complete'], False) #node1 only has one key, can't comp. sign the tx
|
||||||
|
|
||||||
rawTxPartialSigned2 = self.nodes[2].signrawtransactionwithwallet(rawTx2, inputs)
|
rawTxPartialSigned2 = self.nodes[2].signrawtransactionwithwallet(rawTx2, inputs)
|
||||||
self.log.info(rawTxPartialSigned2)
|
self.log.debug(rawTxPartialSigned2)
|
||||||
assert_equal(rawTxPartialSigned2['complete'], False) #node2 only has one key, can't comp. sign the tx
|
assert_equal(rawTxPartialSigned2['complete'], False) #node2 only has one key, can't comp. sign the tx
|
||||||
rawTxComb = self.nodes[2].combinerawtransaction([rawTxPartialSigned1['hex'], rawTxPartialSigned2['hex']])
|
rawTxComb = self.nodes[2].combinerawtransaction([rawTxPartialSigned1['hex'], rawTxPartialSigned2['hex']])
|
||||||
self.log.info(rawTxComb)
|
self.log.debug(rawTxComb)
|
||||||
self.nodes[2].sendrawtransaction(rawTxComb)
|
self.nodes[2].sendrawtransaction(rawTxComb)
|
||||||
rawTx2 = self.nodes[0].decoderawtransaction(rawTxComb)
|
rawTx2 = self.nodes[0].decoderawtransaction(rawTxComb)
|
||||||
self.sync_all()
|
self.sync_all()
|
||||||
|
Loading…
Reference in New Issue
Block a user