Skip to content

Commit ba25a5a

Browse files
committed
DIP-0026: accept the multi-payout object as a JSON string (dash-cli)
ParsePayoutParam only recognized the {"address": basisPoints} payout form when it arrived as a UniValue object, which is the case over JSON-RPC and named arguments. dash-cli passes the payout argument as a plain string (it is not in the rpc/client.cpp conversion table), so the documented object form sent via dash-cli arrived as a string, fell through to the single-address branch, and failed with "invalid payout address: {...}". Accept the object whether it arrives as a UniValue object or as a JSON string: if a string payout begins with '{', parse it and require a JSON object, otherwise treat it as a single address. A base58/bech32 address never begins with '{', so the two forms are unambiguous, and the legacy single-address CLI form is unchanged. The functional test now sends one conversion's shares as a JSON string (the dash-cli path) in addition to the dict form, so both arrival shapes are covered.
1 parent 3cbef82 commit ba25a5a

2 files changed

Lines changed: 32 additions & 5 deletions

File tree

src/rpc/evo.cpp

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -410,7 +410,26 @@ static uint16_t ParsePayoutParam(const UniValue& param, const uint16_t max_versi
410410
scriptPayoutRet = CScript();
411411
payoutSharesRet.clear();
412412

413-
if (param.isObject()) {
413+
// The DIP0026 multi-payout form is a {"address": basisPoints} object. It can arrive either as a
414+
// UniValue object (named arguments / JSON-RPC callers) or as a JSON string, because dash-cli
415+
// passes this argument as a string (the payout arg is not in the rpc/client.cpp conversion
416+
// table). Accept both. A single payout address is base58/bech32 and never begins with '{', so
417+
// the single-address and object forms are unambiguous.
418+
UniValue parsed_obj;
419+
if (param.isStr()) {
420+
const std::string& s{param.get_str()};
421+
const size_t first{s.find_first_not_of(" \t\n\r")};
422+
if (first != std::string::npos && s[first] == '{') {
423+
if (!parsed_obj.read(s) || !parsed_obj.isObject()) {
424+
throw JSONRPCError(RPC_INVALID_PARAMETER,
425+
"payout must be a single address or a JSON object like "
426+
"{\"address\": basisPoints}");
427+
}
428+
}
429+
}
430+
const UniValue& shares{param.isObject() ? param : parsed_obj};
431+
432+
if (shares.isObject()) {
414433
if (max_version < ProTxVersion::MultiPayout) {
415434
throw JSONRPCError(RPC_INVALID_PARAMETER,
416435
"multi-party payouts (DIP0026) are not available yet (v25 is not active)");
@@ -423,12 +442,12 @@ static uint16_t ParsePayoutParam(const UniValue& param, const uint16_t max_versi
423442
// duplicate scripts to mirror consensus. CheckPayoutShares remains the authoritative gate.
424443
int64_t total_bps{0};
425444
std::set<CScript> seen_scripts;
426-
for (const std::string& addr : param.getKeys()) {
445+
for (const std::string& addr : shares.getKeys()) {
427446
const CTxDestination dest = DecodeDestination(addr);
428447
if (!IsValidDestination(dest)) {
429448
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, strprintf("invalid payout address: %s", addr));
430449
}
431-
const int64_t bps = param[addr].getInt<int64_t>();
450+
const int64_t bps = shares[addr].getInt<int64_t>();
432451
if (bps <= 0 || bps > PayoutShare::TOTAL_BASIS_POINTS) {
433452
throw JSONRPCError(RPC_INVALID_PARAMETER,
434453
strprintf("payout share for %s must be between 1 and %d basis points", addr,

test/functional/feature_dip0026_multipayout.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@
4444
payees, cross-version mixing) are covered by the unit reject-matrix instead.
4545
"""
4646

47+
import json
48+
4749
from test_framework.test_framework import DashTestFramework, MasternodeInfo
4850
from test_framework.util import (
4951
assert_equal,
@@ -304,14 +306,20 @@ def reject_payout(shares, msg):
304306
# coinbase additionally has an operator output. The split check below ignores
305307
# that output (different address) and validates only the owner-share split of
306308
# the already-operator-reduced owner reward reported by getblocktemplate.
307-
self.log.info("convert mninfo[1] to a 2-way multi-payout via update_registrar")
309+
#
310+
# The shares here are passed as a JSON STRING rather than a dict, on purpose: dash-cli
311+
# delivers the object form as a string (the payout arg is not in the rpc/client.cpp JSON
312+
# conversion table), so this exercises ParsePayoutParam's string-to-object path that the dict
313+
# form (real JSON-RPC object) does not. mninfo[0] above used the dict form, so both arrival
314+
# shapes are covered.
315+
self.log.info("convert mninfo[1] to a 2-way multi-payout via update_registrar (JSON-string form)")
308316
mn_b = self.mninfo[1]
309317
self.upgrade_mn_to_extaddr(node, mn_b)
310318
b1, b2 = node.getnewaddress(), node.getnewaddress()
311319
shares_b = {b1: 3333, b2: 6667} # ordered; sums to 10000
312320
assert_equal(sum(shares_b.values()), TOTAL_BASIS_POINTS)
313321
node.sendtoaddress(mn_b.fundsAddr, 0.001) # ensure a spendable fee output for the ProUpRegTx
314-
txid = mn_b.update_registrar(node, submit=True, rewards_address=shares_b,
322+
txid = mn_b.update_registrar(node, submit=True, rewards_address=json.dumps(shares_b),
315323
fundsAddr=mn_b.fundsAddr)
316324
assert txid is not None
317325
self.bump_mocktime(10 * 60 + 1)

0 commit comments

Comments
 (0)