mirror of
https://github.com/XRPLF/rippled.git
synced 2025-12-06 17:27:55 +00:00
Update Base58 codec and remove Bitcoin support:
Use C++17 constant expressions to calculate the inverse alphabet map at compile time instead of at runtime. Remove support for encoding & decoding tokens using the Bitcoin alphabet.
This commit is contained in:
committed by
Nik Bougalis
parent
ab9f3fa42a
commit
831e03ad2a
@@ -57,12 +57,6 @@ template <>
|
|||||||
boost::optional<AccountID>
|
boost::optional<AccountID>
|
||||||
parseBase58(std::string const& s);
|
parseBase58(std::string const& s);
|
||||||
|
|
||||||
// Parses AccountID using Bitcoin's alphabet
|
|
||||||
// This is to catch user error. Likely not needed
|
|
||||||
// DEPRECATED
|
|
||||||
boost::optional<AccountID>
|
|
||||||
deprecatedParseBitcoinAccountID(std::string const& s);
|
|
||||||
|
|
||||||
// Compatibility with legacy code
|
// Compatibility with legacy code
|
||||||
bool
|
bool
|
||||||
deprecatedParseBase58(AccountID& account, Json::Value const& jv);
|
deprecatedParseBase58(AccountID& account, Json::Value const& jv);
|
||||||
|
|||||||
@@ -86,7 +86,7 @@ enum error_code_i {
|
|||||||
rpcNO_PF_REQUEST = 33,
|
rpcNO_PF_REQUEST = 33,
|
||||||
|
|
||||||
// Bad parameter
|
// Bad parameter
|
||||||
rpcACT_BITCOIN = 34,
|
// NOT USED DO NOT USE AGAIN rpcACT_BITCOIN = 34,
|
||||||
rpcACT_MALFORMED = 35,
|
rpcACT_MALFORMED = 35,
|
||||||
rpcALREADY_MULTISIG = 36,
|
rpcALREADY_MULTISIG = 36,
|
||||||
rpcALREADY_SINGLE_SIG = 37,
|
rpcALREADY_SINGLE_SIG = 37,
|
||||||
|
|||||||
@@ -185,7 +185,7 @@ struct STExchange<STBlob, PublicKey>
|
|||||||
inline std::string
|
inline std::string
|
||||||
toBase58(TokenType type, PublicKey const& pk)
|
toBase58(TokenType type, PublicKey const& pk)
|
||||||
{
|
{
|
||||||
return base58EncodeToken(type, pk.data(), pk.size());
|
return encodeBase58Token(type, pk.data(), pk.size());
|
||||||
}
|
}
|
||||||
|
|
||||||
template <>
|
template <>
|
||||||
|
|||||||
@@ -119,7 +119,7 @@ parseBase58(TokenType type, std::string const& s);
|
|||||||
inline std::string
|
inline std::string
|
||||||
toBase58(TokenType type, SecretKey const& sk)
|
toBase58(TokenType type, SecretKey const& sk)
|
||||||
{
|
{
|
||||||
return base58EncodeToken(type, sk.data(), sk.size());
|
return encodeBase58Token(type, sk.data(), sk.size());
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Create a secret key using secure random numbers. */
|
/** Create a secret key using secure random numbers. */
|
||||||
|
|||||||
@@ -128,7 +128,7 @@ seedAs1751(Seed const& seed);
|
|||||||
inline std::string
|
inline std::string
|
||||||
toBase58(Seed const& seed)
|
toBase58(Seed const& seed)
|
||||||
{
|
{
|
||||||
return base58EncodeToken(TokenType::FamilySeed, seed.data(), seed.size());
|
return encodeBase58Token(TokenType::FamilySeed, seed.data(), seed.size());
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace ripple
|
} // namespace ripple
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ namespace ripple {
|
|||||||
std::string
|
std::string
|
||||||
toBase58(AccountID const& v)
|
toBase58(AccountID const& v)
|
||||||
{
|
{
|
||||||
return base58EncodeToken(TokenType::AccountID, v.data(), v.size());
|
return encodeBase58Token(TokenType::AccountID, v.data(), v.size());
|
||||||
}
|
}
|
||||||
|
|
||||||
template <>
|
template <>
|
||||||
@@ -45,19 +45,6 @@ parseBase58(std::string const& s)
|
|||||||
return id;
|
return id;
|
||||||
}
|
}
|
||||||
|
|
||||||
boost::optional<AccountID>
|
|
||||||
deprecatedParseBitcoinAccountID(std::string const& s)
|
|
||||||
{
|
|
||||||
auto const result = decodeBase58TokenBitcoin(s, TokenType::AccountID);
|
|
||||||
if (result.empty())
|
|
||||||
return boost::none;
|
|
||||||
AccountID id;
|
|
||||||
if (result.size() != id.size())
|
|
||||||
return boost::none;
|
|
||||||
std::memcpy(id.data(), result.data(), result.size());
|
|
||||||
return id;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool
|
bool
|
||||||
deprecatedParseBase58(AccountID& account, Json::Value const& jv)
|
deprecatedParseBase58(AccountID& account, Json::Value const& jv)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -32,7 +32,6 @@ namespace detail {
|
|||||||
// This array will be omitted from the object file; only the sorted version
|
// This array will be omitted from the object file; only the sorted version
|
||||||
// will remain in the object file. But the string literals will remain.
|
// will remain in the object file. But the string literals will remain.
|
||||||
constexpr static ErrorInfo unorderedErrorInfos[]{
|
constexpr static ErrorInfo unorderedErrorInfos[]{
|
||||||
{rpcACT_BITCOIN, "actBitcoin", "Account is bitcoin address."},
|
|
||||||
{rpcACT_MALFORMED, "actMalformed", "Account malformed."},
|
{rpcACT_MALFORMED, "actMalformed", "Account malformed."},
|
||||||
{rpcACT_NOT_FOUND, "actNotFound", "Account not found."},
|
{rpcACT_NOT_FOUND, "actNotFound", "Account not found."},
|
||||||
{rpcALREADY_MULTISIG, "alreadyMultisig", "Already multisigned."},
|
{rpcALREADY_MULTISIG, "alreadyMultisig", "Already multisigned."},
|
||||||
|
|||||||
@@ -30,13 +30,17 @@
|
|||||||
|
|
||||||
namespace ripple {
|
namespace ripple {
|
||||||
|
|
||||||
static char rippleAlphabet[] =
|
static constexpr char const* alphabetForward =
|
||||||
"rpshnaf39wBUDNEGHJKLM4PQRST7VWXYZ2bcdeCg65jkm8oFqi1tuvAxyz";
|
"rpshnaf39wBUDNEGHJKLM4PQRST7VWXYZ2bcdeCg65jkm8oFqi1tuvAxyz";
|
||||||
|
|
||||||
static char bitcoinAlphabet[] =
|
static constexpr std::array<int, 256> const alphabetReverse = []() {
|
||||||
"123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
|
std::array<int, 256> map{};
|
||||||
|
for (auto& m : map)
|
||||||
//------------------------------------------------------------------------------
|
m = -1;
|
||||||
|
for (int i = 0, j = 0; alphabetForward[i] != 0; ++i)
|
||||||
|
map[static_cast<unsigned char>(alphabetForward[i])] = j++;
|
||||||
|
return map;
|
||||||
|
}();
|
||||||
|
|
||||||
template <class Hasher>
|
template <class Hasher>
|
||||||
static typename Hasher::result_type
|
static typename Hasher::result_type
|
||||||
@@ -66,7 +70,7 @@ digest2(Args const&... args)
|
|||||||
return digest<Hasher>(digest<Hasher>(args...));
|
return digest<Hasher>(digest<Hasher>(args...));
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Calculate a 4-byte checksum of the data
|
/** Calculate a 4-byte checksum of the data
|
||||||
|
|
||||||
The checksum is calculated as the first 4 bytes
|
The checksum is calculated as the first 4 bytes
|
||||||
of the SHA256 digest of the message. This is added
|
of the SHA256 digest of the message. This is added
|
||||||
@@ -75,32 +79,28 @@ digest2(Args const&... args)
|
|||||||
|
|
||||||
@note This checksum algorithm is part of the client API
|
@note This checksum algorithm is part of the client API
|
||||||
*/
|
*/
|
||||||
void
|
static void
|
||||||
checksum(void* out, void const* message, std::size_t size)
|
checksum(void* out, void const* message, std::size_t size)
|
||||||
{
|
{
|
||||||
auto const h = digest2<sha256_hasher>(message, size);
|
auto const h = digest2<sha256_hasher>(message, size);
|
||||||
std::memcpy(out, h.data(), 4);
|
std::memcpy(out, h.data(), 4);
|
||||||
}
|
}
|
||||||
|
|
||||||
//------------------------------------------------------------------------------
|
namespace detail {
|
||||||
|
|
||||||
// Code from Bitcoin: https://github.com/bitcoin/bitcoin
|
/* The base58 encoding & decoding routines in this namespace are taken from
|
||||||
// Copyright (c) 2014 The Bitcoin Core developers
|
* Bitcoin but have been modified from the original.
|
||||||
// Distributed under the MIT software license, see the accompanying
|
*
|
||||||
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
|
* Copyright (c) 2014 The Bitcoin Core developers
|
||||||
//
|
* Distributed under the MIT software license, see the accompanying
|
||||||
// Modified from the original
|
* file COPYING or http://www.opensource.org/licenses/mit-license.php.
|
||||||
//
|
*/
|
||||||
// WARNING Do not call this directly, use
|
|
||||||
// encodeBase58Token instead since it
|
|
||||||
// calculates the size of buffer needed.
|
|
||||||
static std::string
|
static std::string
|
||||||
encodeBase58(
|
encodeBase58(
|
||||||
void const* message,
|
void const* message,
|
||||||
std::size_t size,
|
std::size_t size,
|
||||||
void* temp,
|
void* temp,
|
||||||
std::size_t temp_size,
|
std::size_t temp_size)
|
||||||
char const* const alphabet)
|
|
||||||
{
|
{
|
||||||
auto pbegin = reinterpret_cast<unsigned char const*>(message);
|
auto pbegin = reinterpret_cast<unsigned char const*>(message);
|
||||||
auto const pend = pbegin + size;
|
auto const pend = pbegin + size;
|
||||||
@@ -140,73 +140,20 @@ encodeBase58(
|
|||||||
// Translate the result into a string.
|
// Translate the result into a string.
|
||||||
std::string str;
|
std::string str;
|
||||||
str.reserve(zeroes + (b58end - iter));
|
str.reserve(zeroes + (b58end - iter));
|
||||||
str.assign(zeroes, alphabet[0]);
|
str.assign(zeroes, alphabetForward[0]);
|
||||||
while (iter != b58end)
|
while (iter != b58end)
|
||||||
str += alphabet[*(iter++)];
|
str += alphabetForward[*(iter++)];
|
||||||
return str;
|
return str;
|
||||||
}
|
}
|
||||||
|
|
||||||
static std::string
|
static std::string
|
||||||
encodeToken(
|
decodeBase58(std::string const& s)
|
||||||
TokenType type,
|
|
||||||
void const* token,
|
|
||||||
std::size_t size,
|
|
||||||
char const* const alphabet)
|
|
||||||
{
|
|
||||||
// expanded token includes type + 4 byte checksum
|
|
||||||
auto const expanded = 1 + size + 4;
|
|
||||||
|
|
||||||
// We need expanded + expanded * (log(256) / log(58)) which is
|
|
||||||
// bounded by expanded + expanded * (138 / 100 + 1) which works
|
|
||||||
// out to expanded * 3:
|
|
||||||
auto const bufsize = expanded * 3;
|
|
||||||
|
|
||||||
boost::container::small_vector<std::uint8_t, 1024> buf(bufsize);
|
|
||||||
|
|
||||||
// Lay the data out as
|
|
||||||
// <type><token><checksum>
|
|
||||||
buf[0] = safe_cast<std::underlying_type_t<TokenType>>(type);
|
|
||||||
if (size)
|
|
||||||
std::memcpy(buf.data() + 1, token, size);
|
|
||||||
checksum(buf.data() + 1 + size, buf.data(), 1 + size);
|
|
||||||
|
|
||||||
return encodeBase58(
|
|
||||||
buf.data(),
|
|
||||||
expanded,
|
|
||||||
buf.data() + expanded,
|
|
||||||
bufsize - expanded,
|
|
||||||
alphabet);
|
|
||||||
}
|
|
||||||
|
|
||||||
std::string
|
|
||||||
base58EncodeToken(TokenType type, void const* token, std::size_t size)
|
|
||||||
{
|
|
||||||
return encodeToken(type, token, size, rippleAlphabet);
|
|
||||||
}
|
|
||||||
|
|
||||||
std::string
|
|
||||||
base58EncodeTokenBitcoin(TokenType type, void const* token, std::size_t size)
|
|
||||||
{
|
|
||||||
return encodeToken(type, token, size, bitcoinAlphabet);
|
|
||||||
}
|
|
||||||
|
|
||||||
//------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
// Code from Bitcoin: https://github.com/bitcoin/bitcoin
|
|
||||||
// Copyright (c) 2014 The Bitcoin Core developers
|
|
||||||
// Distributed under the MIT software license, see the accompanying
|
|
||||||
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
|
|
||||||
//
|
|
||||||
// Modified from the original
|
|
||||||
template <class InverseArray>
|
|
||||||
static std::string
|
|
||||||
decodeBase58(std::string const& s, InverseArray const& inv)
|
|
||||||
{
|
{
|
||||||
auto psz = s.c_str();
|
auto psz = s.c_str();
|
||||||
auto remain = s.size();
|
auto remain = s.size();
|
||||||
// Skip and count leading zeroes
|
// Skip and count leading zeroes
|
||||||
int zeroes = 0;
|
int zeroes = 0;
|
||||||
while (remain > 0 && inv[*psz] == 0)
|
while (remain > 0 && alphabetReverse[*psz] == 0)
|
||||||
{
|
{
|
||||||
++zeroes;
|
++zeroes;
|
||||||
++psz;
|
++psz;
|
||||||
@@ -221,7 +168,7 @@ decodeBase58(std::string const& s, InverseArray const& inv)
|
|||||||
std::vector<unsigned char> b256(remain * 733 / 1000 + 1);
|
std::vector<unsigned char> b256(remain * 733 / 1000 + 1);
|
||||||
while (remain > 0)
|
while (remain > 0)
|
||||||
{
|
{
|
||||||
auto carry = inv[*psz];
|
auto carry = alphabetReverse[*psz];
|
||||||
if (carry == -1)
|
if (carry == -1)
|
||||||
return {};
|
return {};
|
||||||
// Apply "b256 = b256 * 58 + carry".
|
// Apply "b256 = b256 * 58 + carry".
|
||||||
@@ -246,16 +193,36 @@ decodeBase58(std::string const& s, InverseArray const& inv)
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Base58 decode a Ripple token
|
} // namespace detail
|
||||||
|
|
||||||
The type and checksum are are checked
|
std::string
|
||||||
and removed from the returned result.
|
encodeBase58Token(TokenType type, void const* token, std::size_t size)
|
||||||
*/
|
|
||||||
template <class InverseArray>
|
|
||||||
static std::string
|
|
||||||
decodeBase58Token(std::string const& s, TokenType type, InverseArray const& inv)
|
|
||||||
{
|
{
|
||||||
std::string const ret = decodeBase58(s, inv);
|
// expanded token includes type + 4 byte checksum
|
||||||
|
auto const expanded = 1 + size + 4;
|
||||||
|
|
||||||
|
// We need expanded + expanded * (log(256) / log(58)) which is
|
||||||
|
// bounded by expanded + expanded * (138 / 100 + 1) which works
|
||||||
|
// out to expanded * 3:
|
||||||
|
auto const bufsize = expanded * 3;
|
||||||
|
|
||||||
|
boost::container::small_vector<std::uint8_t, 1024> buf(bufsize);
|
||||||
|
|
||||||
|
// Lay the data out as
|
||||||
|
// <type><token><checksum>
|
||||||
|
buf[0] = safe_cast<std::underlying_type_t<TokenType>>(type);
|
||||||
|
if (size)
|
||||||
|
std::memcpy(buf.data() + 1, token, size);
|
||||||
|
checksum(buf.data() + 1 + size, buf.data(), 1 + size);
|
||||||
|
|
||||||
|
return detail::encodeBase58(
|
||||||
|
buf.data(), expanded, buf.data() + expanded, bufsize - expanded);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string
|
||||||
|
decodeBase58Token(std::string const& s, TokenType type)
|
||||||
|
{
|
||||||
|
std::string const ret = detail::decodeBase58(s);
|
||||||
|
|
||||||
// Reject zero length tokens
|
// Reject zero length tokens
|
||||||
if (ret.size() < 6)
|
if (ret.size() < 6)
|
||||||
@@ -275,44 +242,4 @@ decodeBase58Token(std::string const& s, TokenType type, InverseArray const& inv)
|
|||||||
return ret.substr(1, ret.size() - 1 - guard.size());
|
return ret.substr(1, ret.size() - 1 - guard.size());
|
||||||
}
|
}
|
||||||
|
|
||||||
//------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
// Maps characters to their base58 digit
|
|
||||||
class InverseAlphabet
|
|
||||||
{
|
|
||||||
private:
|
|
||||||
std::array<int, 256> map_;
|
|
||||||
|
|
||||||
public:
|
|
||||||
explicit InverseAlphabet(std::string const& digits)
|
|
||||||
{
|
|
||||||
map_.fill(-1);
|
|
||||||
int i = 0;
|
|
||||||
for (auto const c : digits)
|
|
||||||
map_[static_cast<unsigned char>(c)] = i++;
|
|
||||||
}
|
|
||||||
|
|
||||||
int
|
|
||||||
operator[](char c) const
|
|
||||||
{
|
|
||||||
return map_[static_cast<unsigned char>(c)];
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
static InverseAlphabet rippleInverse(rippleAlphabet);
|
|
||||||
|
|
||||||
static InverseAlphabet bitcoinInverse(bitcoinAlphabet);
|
|
||||||
|
|
||||||
std::string
|
|
||||||
decodeBase58Token(std::string const& s, TokenType type)
|
|
||||||
{
|
|
||||||
return decodeBase58Token(s, type, rippleInverse);
|
|
||||||
}
|
|
||||||
|
|
||||||
std::string
|
|
||||||
decodeBase58TokenBitcoin(std::string const& s, TokenType type)
|
|
||||||
{
|
|
||||||
return decodeBase58Token(s, type, bitcoinInverse);
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace ripple
|
} // namespace ripple
|
||||||
|
|||||||
@@ -53,61 +53,31 @@ template <class T>
|
|||||||
boost::optional<T>
|
boost::optional<T>
|
||||||
parseHexOrBase58(std::string const& s);
|
parseHexOrBase58(std::string const& s);
|
||||||
|
|
||||||
// Facilities for converting Ripple tokens
|
/** Encode data in Base58Check format using XRPL alphabet
|
||||||
// to and from their human readable strings
|
|
||||||
|
|
||||||
/* Base-58 encode a Ripple Token
|
For details on the format see
|
||||||
|
https://xrpl.org/base58-encodings.html#base58-encodings
|
||||||
|
|
||||||
Ripple Tokens have a one-byte prefx indicating
|
@param type The type of token to encode.
|
||||||
the type of token, followed by the data for the
|
@param token Pointer to the data to encode.
|
||||||
token, and finally a 4-byte checksum.
|
@param size The size of the data to encode.
|
||||||
|
|
||||||
Tokens include the following:
|
@return the encoded token.
|
||||||
|
|
||||||
Wallet Seed
|
|
||||||
Account Public Key
|
|
||||||
Account ID
|
|
||||||
|
|
||||||
@param type A single byte representing the TokenType
|
|
||||||
@param token A pointer to storage of not
|
|
||||||
less than 2*(size+6) bytes
|
|
||||||
@param size the size of the token buffer in bytes
|
|
||||||
*/
|
*/
|
||||||
std::string
|
std::string
|
||||||
base58EncodeToken(TokenType type, void const* token, std::size_t size);
|
encodeBase58Token(TokenType type, void const* token, std::size_t size);
|
||||||
|
|
||||||
/* Base-58 encode a Bitcoin Token
|
/** Decode a token of given type encoded using Base58Check and the XRPL alphabet
|
||||||
*
|
|
||||||
* provided here for symmetry, but should never be needed
|
|
||||||
* except for testing.
|
|
||||||
*
|
|
||||||
* @see base58EncodeToken for format description.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
std::string
|
|
||||||
base58EncodeTokenBitcoin(TokenType type, void const* token, std::size_t size);
|
|
||||||
|
|
||||||
/** Decode a Base58 token
|
@param s The encoded token
|
||||||
|
@param type The type expected for this token.
|
||||||
|
|
||||||
The type and checksum must match or an
|
@return If the encoded token decodes correctly, the token data without
|
||||||
empty string is returned.
|
the type or checksum. And empty string otherwise.
|
||||||
*/
|
*/
|
||||||
std::string
|
std::string
|
||||||
decodeBase58Token(std::string const& s, TokenType type);
|
decodeBase58Token(std::string const& s, TokenType type);
|
||||||
|
|
||||||
/** Decode a Base58 token using Bitcoin alphabet
|
|
||||||
|
|
||||||
The type and checksum must match or an
|
|
||||||
empty string is returned.
|
|
||||||
|
|
||||||
This is used to detect user error. Specifically,
|
|
||||||
when an AccountID is specified using the wrong
|
|
||||||
base58 alphabet, so that a better error message
|
|
||||||
may be returned.
|
|
||||||
*/
|
|
||||||
std::string
|
|
||||||
decodeBase58TokenBitcoin(std::string const& s, TokenType type);
|
|
||||||
|
|
||||||
} // namespace ripple
|
} // namespace ripple
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -63,10 +63,7 @@ accountFromStringWithCode(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (bStrict)
|
if (bStrict)
|
||||||
{
|
return rpcACT_MALFORMED;
|
||||||
auto id = deprecatedParseBitcoinAccountID(strIdent);
|
|
||||||
return id ? rpcACT_BITCOIN : rpcACT_MALFORMED;
|
|
||||||
}
|
|
||||||
|
|
||||||
// We allow the use of the seeds which is poor practice
|
// We allow the use of the seeds which is poor practice
|
||||||
// and merely for debugging convenience.
|
// and merely for debugging convenience.
|
||||||
|
|||||||
@@ -69,20 +69,6 @@ class AccountCurrencies_test : public beast::unit_test::suite
|
|||||||
BEAST_EXPECT(result[jss::error_message] == "Account malformed.");
|
BEAST_EXPECT(result[jss::error_message] == "Account malformed.");
|
||||||
}
|
}
|
||||||
|
|
||||||
{ // strict mode, using properly formatted bitcoin token
|
|
||||||
Json::Value params;
|
|
||||||
params[jss::account] = base58EncodeTokenBitcoin(
|
|
||||||
TokenType::AccountID, alice.id().data(), alice.id().size());
|
|
||||||
params[jss::strict] = true;
|
|
||||||
auto const result = env.rpc(
|
|
||||||
"json",
|
|
||||||
"account_currencies",
|
|
||||||
boost::lexical_cast<std::string>(params))[jss::result];
|
|
||||||
BEAST_EXPECT(result[jss::error] == "actBitcoin");
|
|
||||||
BEAST_EXPECT(
|
|
||||||
result[jss::error_message] == "Account is bitcoin address.");
|
|
||||||
}
|
|
||||||
|
|
||||||
{ // ask for nonexistent account
|
{ // ask for nonexistent account
|
||||||
Json::Value params;
|
Json::Value params;
|
||||||
params[jss::account] = Account{"bob"}.human();
|
params[jss::account] = Account{"bob"}.human();
|
||||||
|
|||||||
Reference in New Issue
Block a user