diff --git a/src/omnicore/createpayload.h b/src/omnicore/createpayload.h --- a/src/omnicore/createpayload.h +++ b/src/omnicore/createpayload.h @@ -2,11 +2,13 @@ #define BITCOIN_OMNICORE_CREATEPAYLOAD_H #include +#include #include #include std::vector CreatePayload_SimpleSend(uint32_t propertyId, uint64_t amount); std::vector CreatePayload_SendAll(uint8_t ecosystem); +std::vector CreatePayload_SendToMany(uint32_t propertyId, std::vector> outputValues); std::vector CreatePayload_SendNonFungible(uint32_t propertyId, uint64_t tokenStart, uint64_t tokenEnd); std::vector CreatePayload_SetNonFungibleData(uint32_t propertyId, uint64_t tokenStart, uint64_t tokenEnd, uint8_t issuer, std::string& data); std::vector CreatePayload_DExSell(uint32_t propertyId, uint64_t amountForSale, uint64_t amountDesired, uint8_t timeLimit, uint64_t minFee, uint8_t subAction); diff --git a/src/omnicore/createpayload.cpp b/src/omnicore/createpayload.cpp --- a/src/omnicore/createpayload.cpp +++ b/src/omnicore/createpayload.cpp @@ -8,7 +8,9 @@ #include #include +#include #include +#include #include /** @@ -76,6 +78,39 @@ return payload; } + +std::vector CreatePayload_SendToMany(uint32_t propertyId, std::vector> outputValues) +{ + std::vector payload; + uint16_t messageType = 7; + uint16_t messageVer = 0; + SwapByteOrder16(messageType); + SwapByteOrder16(messageVer); + SwapByteOrder32(propertyId); + + PUSH_BACK_BYTES(payload, messageVer); + PUSH_BACK_BYTES(payload, messageType); + PUSH_BACK_BYTES(payload, propertyId); + + if (outputValues.size() > std::numeric_limits::max()) { + PrintToLog("WARNING: removing last elements from send-to-many transaction due to size limitations (%d > %d)\n", + outputValues.size(), std::numeric_limits::max()); + outputValues.erase(outputValues.begin() + std::numeric_limits::max(), outputValues.end()); + } + + uint8_t outputsCount = static_cast(outputValues.size()); + PUSH_BACK_BYTES(payload, outputsCount); + + for (const auto& outputValue : outputValues) { + uint8_t vout = std::get<0>(outputValue); + uint64_t amount = std::get<1>(outputValue); + SwapByteOrder64(amount); + PUSH_BACK_BYTES(payload, vout); + PUSH_BACK_BYTES(payload, amount); + } + + return payload; +} std::vector CreatePayload_SendNonFungible(uint32_t propertyId, uint64_t tokenStart, uint64_t tokenEnd) { diff --git a/src/omnicore/tx.h b/src/omnicore/tx.h --- a/src/omnicore/tx.h +++ b/src/omnicore/tx.h @@ -13,7 +13,9 @@ #include #include +#include #include +#include using mastercore::strTransactionType; @@ -76,6 +78,11 @@ uint64_t min_fee; unsigned char subaction; + // Send To Many + uint8_t numberOfSTMReceivers; + std::vector> outputValuesForSTM; + std::map validOutputAddressesForSTM; + // Unique Send uint64_t nonfungible_token_start; uint64_t nonfungible_token_end; @@ -105,6 +112,7 @@ bool interpret_SimpleSend(); bool interpret_SendToOwners(); bool interpret_SendAll(); + bool interpret_SendToMany(); bool interpret_SendNonFungible(); bool interpret_TradeOffer(); bool interpret_AcceptOfferBTC(); @@ -131,6 +139,7 @@ int logicMath_SimpleSend(uint256& blockHash); int logicMath_SendToOwners(); int logicMath_SendAll(); + int logicMath_SendToMany(); int logicMath_SendNonFungible(); int logicMath_TradeOffer(); int logicMath_AcceptOffer_BTC(); @@ -181,6 +190,14 @@ std::string getPayloadData() const { return HexStr(pkt + 4 /* skip version and type */, pkt + pkt_size); } uint64_t getAmount() const { return nValue; } uint64_t getNewAmount() const { return nNewValue; } + uint8_t getStmNumberOfReceivers() const { return numberOfSTMReceivers; } + std::vector> getStmOutputValues() const { return outputValuesForSTM; } + + /** Registers an output as potential Send-To-Many destination. */ + void addValidStmAddress(size_t output, const std::string& address); + + /** Returns an output address, if it's considered a valid Omni destination. */ + bool getValidStmAddressAt(uint8_t output, std::string& addressOut); uint8_t getEcosystem() const { return ecosystem; } uint32_t getPreviousId() const { return prev_prop_id; } std::string getSPCategory() const { return category; } @@ -256,6 +273,9 @@ distribution_property = 0; nonfungible_token_start = 0; nonfungible_token_end = 0; + numberOfSTMReceivers = 0; + outputValuesForSTM.clear(); + validOutputAddressesForSTM.clear(); memset(&nonfungible_data, 0, sizeof(nonfungible_data)); } diff --git a/src/omnicore/tx.cpp b/src/omnicore/tx.cpp --- a/src/omnicore/tx.cpp +++ b/src/omnicore/tx.cpp @@ -48,6 +48,7 @@ case MSC_TYPE_RESTRICTED_SEND: return "Restricted Send"; case MSC_TYPE_SEND_TO_OWNERS: return "Send To Owners"; case MSC_TYPE_SEND_ALL: return "Send All"; + case MSC_TYPE_SEND_TO_MANY: return "Send To Many"; case MSC_TYPE_SEND_NONFUNGIBLE: return "Unique Send"; case MSC_TYPE_SAVINGS_MARK: return "Savings"; case MSC_TYPE_SAVINGS_COMPROMISED: return "Savings COMPROMISED"; @@ -120,6 +121,9 @@ case MSC_TYPE_SEND_ALL: return interpret_SendAll(); + case MSC_TYPE_SEND_TO_MANY: + return interpret_SendToMany(); + case MSC_TYPE_SEND_NONFUNGIBLE: return interpret_SendNonFungible(); @@ -280,6 +284,69 @@ return true; } + +/** Tx 7 */ +bool CMPTransaction::interpret_SendToMany() +{ + // minimum case without any recipients, still needs the property id + if (pkt_size < 9) { + return false; + } + + memcpy(&property, &pkt[4], 4); + SwapByteOrder32(property); + memcpy(&numberOfSTMReceivers, &pkt[8], 1); + + // check the total size for all receivers + if (pkt_size < (9 + (numberOfSTMReceivers * (1 + 8)))) { + return false; + } + + size_t pos = 9; + for (uint8_t i = 0; i < numberOfSTMReceivers; ++i) { + uint8_t outputN = 0; + memcpy(&outputN, &pkt[pos], 1); + uint64_t valueN = 0; + memcpy(&valueN, &pkt[pos + 1], 8); + SwapByteOrder64(valueN); + outputValuesForSTM.push_back(std::make_tuple(outputN, valueN)); + nValue += valueN; + pos = pos + 9; + + if ((!rpcOnly && msc_debug_packets) || msc_debug_packets_readonly) { + PrintToLog("\t output %d: %s\n", outputN, FormatMP(property, valueN)); + } + } + nNewValue = nValue; + + if ((!rpcOnly && msc_debug_packets) || msc_debug_packets_readonly) { + PrintToLog("\t property: %d (%s)\n", property, strMPProperty(property)); + PrintToLog("\t receivers: %d\n", (int)numberOfSTMReceivers); + } + + return true; +} + +/** Registers an output as potential Send-To-Many destination. */ +void CMPTransaction::addValidStmAddress(size_t output, const std::string& address) +{ + if (output > std::numeric_limits::max()) { + return; + } + validOutputAddressesForSTM[static_cast(output)] = address; +} + +/** Returns an output address, if it's considered a valid Omni destination. */ +bool CMPTransaction::getValidStmAddressAt(uint8_t output, std::string& addressOut) +{ + if (validOutputAddressesForSTM.find(output) != validOutputAddressesForSTM.end()) { + addressOut = validOutputAddressesForSTM[output]; + return true; + } + addressOut.clear(); + return false; +} + /** Tx 5 */ bool CMPTransaction::interpret_SendNonFungible() { @@ -858,6 +925,9 @@ case MSC_TYPE_SEND_ALL: return logicMath_SendAll(); + case MSC_TYPE_SEND_TO_MANY: + return logicMath_SendToMany(); + case MSC_TYPE_SEND_NONFUNGIBLE: return logicMath_SendNonFungible(); @@ -1170,6 +1240,95 @@ } /** Tx 4 */ +/** Tx 7 */ +int CMPTransaction::logicMath_SendToMany() +{ + if (!IsTransactionTypeAllowed(block, property, type, version)) { + PrintToLog("%s(): rejected: type %d or version %d not permitted for property %d at block %d\n", + __func__, + type, + version, + property, + block); + return (PKT_ERROR_SEND_MANY -22); + } + + if (isPropertyNonFungible(property)) { + PrintToLog("%s(): rejected: property %d is of type non-fungible\n", __func__, property); + return (PKT_ERROR_SEND_MANY -27); + } + + if (nValue <= 0 || MAX_INT_8_BYTES < nValue) { + PrintToLog("%s(): rejected: value out of range or zero: %d\n", __func__, nValue); + return (PKT_ERROR_SEND_MANY -23); + } + + if (!IsPropertyIdValid(property)) { + PrintToLog("%s(): rejected: property %d does not exist\n", __func__, property); + return (PKT_ERROR_SEND_MANY -24); + } + + int64_t nBalance = GetTokenBalance(sender, property, BALANCE); + if (nBalance < (int64_t) nValue) { + PrintToLog("%s(): rejected: sender %s has insufficient balance of property %d [%s < %s]\n", + __func__, + sender, + property, + FormatMP(property, nBalance), + FormatMP(property, nValue)); + return (PKT_ERROR_SEND_MANY -25); + } + + // ------------------------------------------ + // First pass: validate every single receiver, before any money is moved. + + uint64_t totalAmount = 0; + uint8_t validReceivers = 0; + + for (const std::tuple& entry : outputValuesForSTM) { + uint8_t output = std::get<0>(entry); + uint64_t amount = std::get<1>(entry); + + std::string address; + if (!getValidStmAddressAt(output, address)) { + PrintToLog("%s(): rejected: output %d is not a valid destination\n", __func__, output); + return (PKT_ERROR_SEND_MANY -26); + } + if (address.empty()) { + PrintToLog("%s(): rejected: receiver at output %d is empty\n", __func__, output); + return (PKT_ERROR_SEND_MANY -27); + } + + totalAmount += amount; + validReceivers += 1; + } + + if (totalAmount != nValue) { + PrintToLog("%s(): rejected: sum of outputs %d does not match total amount %d\n", __func__, totalAmount, nValue); + return (PKT_ERROR_SEND_MANY -28); + } + + if (validReceivers != numberOfSTMReceivers) { + PrintToLog("%s(): rejected: number of valid receivers %d does not match %d\n", __func__, validReceivers, numberOfSTMReceivers); + return (PKT_ERROR_SEND_MANY -29); + } + + // ------------------------------------------ + // Second pass: move the money. + + for (const std::tuple& entry : outputValuesForSTM) { + uint8_t output = std::get<0>(entry); + uint64_t amount = std::get<1>(entry); + + std::string receiverAddress; + assert(getValidStmAddressAt(output, receiverAddress)); + assert(update_tally_map(sender, property, -amount, BALANCE)); + assert(update_tally_map(receiverAddress, property, amount, BALANCE)); + } + + return 0; +} + int CMPTransaction::logicMath_SendAll() { if (!IsTransactionTypeAllowed(block, ecosystem, type, version)) { diff --git a/src/omnicore/omnicore.h b/src/omnicore/omnicore.h --- a/src/omnicore/omnicore.h +++ b/src/omnicore/omnicore.h @@ -58,6 +58,7 @@ MSC_TYPE_RESTRICTED_SEND = 2, MSC_TYPE_SEND_TO_OWNERS = 3, MSC_TYPE_SEND_ALL = 4, + MSC_TYPE_SEND_TO_MANY = 7, MSC_TYPE_SEND_NONFUNGIBLE = 5, MSC_TYPE_SAVINGS_MARK = 10, MSC_TYPE_SAVINGS_COMPROMISED = 11, @@ -111,6 +112,7 @@ #define PKT_ERROR_SEND_ALL (-83000) #define PKT_ERROR_ANYDATA (-84000) #define PKT_ERROR_NFT (-85000) +#define PKT_ERROR_SEND_MANY (-86000) #define OMNI_PROPERTY_BTC 0 #define OMNI_PROPERTY_MSC 1 diff --git a/src/omnicore/omnicore.cpp b/src/omnicore/omnicore.cpp --- a/src/omnicore/omnicore.cpp +++ b/src/omnicore/omnicore.cpp @@ -932,6 +932,8 @@ GetScriptPushes(wtx.vout[n].scriptPubKey, script_data); address_data.push_back(EncodeDestination(dest)); value_data.push_back(wtx.vout[n].nValue); + // register the output as potential Send-To-Many destination (unconditional, cheap) + mp_tx.addValidStmAddress(n, EncodeDestination(dest)); if (msc_debug_parser_data) PrintToLog("saving address_data #%d: %s:%s\n", n, EncodeDestination(dest), ScriptToAsmStr(wtx.vout[n].scriptPubKey)); } } diff --git a/src/omnicore/rules.h b/src/omnicore/rules.h --- a/src/omnicore/rules.h +++ b/src/omnicore/rules.h @@ -19,6 +19,8 @@ const uint16_t FEATURE_NONFUNGIBLE = 16; //! Feature identifier to enable NFT issuer data update by issuers only const uint16_t FEATURE_NONFUNGIBLE_ISSUER = 18; +//! Feature identifier to activate send-to-many transactions +const uint16_t FEATURE_SEND_TO_MANY = 19; /** A structure to represent transaction restrictions. */ @@ -96,6 +98,8 @@ int MSC_ANYDATA_BLOCK; //! Block to enable non-fungible tokens int MSC_NONFUNGIBLE_BLOCK; + //! Block to enable send-to-many transactions + int MSC_SEND_TO_MANY_BLOCK; //! Block to activate the waiting period for enabling managed property address freezing int FREEZENOTICE_FEATURE_BLOCK; diff --git a/src/omnicore/rules.cpp b/src/omnicore/rules.cpp --- a/src/omnicore/rules.cpp +++ b/src/omnicore/rules.cpp @@ -65,6 +65,7 @@ { MSC_TYPE_SEND_TO_OWNERS, MP_TX_PKT_V1, false, MSC_STOV1_BLOCK }, { MSC_TYPE_SEND_ALL, MP_TX_PKT_V0, false, MSC_SEND_ALL_BLOCK }, + { MSC_TYPE_SEND_TO_MANY, MP_TX_PKT_V0, false, MSC_SEND_TO_MANY_BLOCK }, { MSC_TYPE_ANYDATA, MP_TX_PKT_V0, true, MSC_ANYDATA_BLOCK }, @@ -141,6 +142,7 @@ MSC_STOV1_BLOCK = 100000000; MSC_ANYDATA_BLOCK = 0; MSC_NONFUNGIBLE_BLOCK = 2262000; + MSC_SEND_TO_MANY_BLOCK = 999999999; // Other feature activations: FREEZENOTICE_FEATURE_BLOCK = 100000000; FREEDEX_FEATURE_BLOCK = GENESIS_BLOCK; @@ -175,6 +177,7 @@ MSC_STOV1_BLOCK = 0; MSC_ANYDATA_BLOCK = 0; MSC_NONFUNGIBLE_BLOCK = 0; + MSC_SEND_TO_MANY_BLOCK = 0; // Other feature activations: FREEZENOTICE_FEATURE_BLOCK = 0; FREEDEX_FEATURE_BLOCK = 0; @@ -209,6 +212,7 @@ MSC_STOV1_BLOCK = 100000000; MSC_ANYDATA_BLOCK = 0; MSC_NONFUNGIBLE_BLOCK = 0; + MSC_SEND_TO_MANY_BLOCK = 0; // Other feature activations: FREEZENOTICE_FEATURE_BLOCK = 100000000; FREEDEX_FEATURE_BLOCK = GENESIS_BLOCK; @@ -360,6 +364,9 @@ case FEATURE_NONFUNGIBLE_ISSUER: MutableConsensusParams().NONFUNGIBLETOKEN_ISSUER_DATA = activationBlock; break; + case FEATURE_SEND_TO_MANY: + MutableConsensusParams().MSC_SEND_TO_MANY_BLOCK = activationBlock; + break; default: supported = false; break; @@ -414,6 +421,9 @@ case FEATURE_NONFUNGIBLE_ISSUER: MutableConsensusParams().NONFUNGIBLETOKEN_ISSUER_DATA = 999999; break; + case FEATURE_SEND_TO_MANY: + MutableConsensusParams().MSC_SEND_TO_MANY_BLOCK = 999999999; + break; default: return false; break; @@ -441,6 +451,7 @@ case FEATURE_FREEDEX: return "Activate trading of any token on the distributed exchange"; case FEATURE_NONFUNGIBLE: return "Uniquely identifiable tokens"; case FEATURE_NONFUNGIBLE_ISSUER: return "NFT issuer data update by issuers only"; + case FEATURE_SEND_TO_MANY: return "Activate send-to-many transactions"; default: return "Unknown feature"; } @@ -472,6 +483,9 @@ case FEATURE_NONFUNGIBLE_ISSUER: activationBlock = params.NONFUNGIBLETOKEN_ISSUER_DATA; break; + case FEATURE_SEND_TO_MANY: + activationBlock = params.MSC_SEND_TO_MANY_BLOCK; + break; default: return false; } diff --git a/src/omnicore/rpcpayload.cpp b/src/omnicore/rpcpayload.cpp --- a/src/omnicore/rpcpayload.cpp +++ b/src/omnicore/rpcpayload.cpp @@ -42,6 +42,62 @@ return HexStr(payload.begin(), payload.end()); } +static UniValue omni_createpayload_sendtomany(const JSONRPCRequest& request) +{ + if (request.fHelp || request.params.size() != 2) + throw runtime_error( + RPCHelpMan{"omni_createpayload_sendtomany", + "\nCreate the payload for a send-to-many transaction.\n", + { + {"propertyid", RPCArg::Type::NUM, RPCArg::Optional::NO, "the identifier of the tokens to send\n"}, + {"mapping", RPCArg::Type::ARR, RPCArg::Optional::NO, "an array with the receiving outputs\n", + { + {"", RPCArg::Type::OBJ, RPCArg::Optional::OMITTED, "", + { + {"output", RPCArg::Type::NUM, RPCArg::Optional::NO, "the output number of the receiver\n"}, + {"amount", RPCArg::Type::STR, RPCArg::Optional::NO, "the amount to send\n"}, + }, + }, + }, + }, + }, + RPCResult{ + RPCResult::Type::STR_HEX, "", "The hex-encoded payload" + }, + RPCExamples{ + HelpExampleCli("omni_createpayload_sendtomany", "31 \"[{\\\"output\\\":2,\\\"amount\\\":\\\"10.5\\\"},{\\\"output\\\":3,\\\"amount\\\":\\\"0.5\\\"}]\"") + + HelpExampleRpc("omni_createpayload_sendtomany", "31, [{\"output\":2,\"amount\":\"10.5\"},{\"output\":3,\"amount\":\"0.5\"}]") + } + }.ToString()); + + uint32_t propertyId = ParsePropertyId(request.params[0]); + RequireExistingProperty(propertyId); + + bool isDivisible = isPropertyDivisible(propertyId); + + std::vector> outputValues; + + UniValue receiverList = request.params[1].get_array(); + for (unsigned int idx = 0; idx < receiverList.size(); idx++) { + const UniValue& input = receiverList[idx]; + const UniValue& o = input.get_obj(); + + const UniValue& uvOutput = find_value(o, "output"); + const UniValue& uvAmount = find_value(o, "amount"); + + uint8_t output = ParseStmOutputIndex(uvOutput); + uint64_t amount = static_cast(ParseAmount(uvAmount, isDivisible)); + + outputValues.push_back(std::make_tuple(output, amount)); + } + + RequireBoundedStmReceiverNumber(outputValues.size()); + + std::vector payload = CreatePayload_SendToMany(propertyId, outputValues); + + return HexStr(payload.begin(), payload.end()); +} + static UniValue omni_createpayload_sendall(const JSONRPCRequest& request) { if (request.fHelp || request.params.size() != 1) @@ -623,6 +679,7 @@ // -------------------------------- ----------------------------------------- ---------------------------------------- ---------- { "omni layer (payload creation)", "omni_createpayload_simplesend", &omni_createpayload_simplesend, {"propertyid", "amount"} }, { "omni layer (payload creation)", "omni_createpayload_sendall", &omni_createpayload_sendall, {"ecosystem"} }, + { "omni layer (payload creation)", "omni_createpayload_sendtomany", &omni_createpayload_sendtomany, {"propertyid", "mapping"} }, { "omni layer (payload creation)", "omni_createpayload_dexsell", &omni_createpayload_dexsell, {"propertyidforsale", "amountforsale", "amountdesired", "paymentwindow", "minacceptfee", "action"} }, { "omni layer (payload creation)", "omni_createpayload_dexaccept", &omni_createpayload_dexaccept, {"propertyid", "amount"} }, { "omni layer (payload creation)", "omni_createpayload_sto", &omni_createpayload_sto, {"propertyid", "amount", "distributionproperty"} }, diff --git a/src/omnicore/rpctx.cpp b/src/omnicore/rpctx.cpp --- a/src/omnicore/rpctx.cpp +++ b/src/omnicore/rpctx.cpp @@ -248,6 +248,101 @@ } } +static UniValue omni_sendtomany(const JSONRPCRequest& request) +{ + std::shared_ptr const wallet = GetWalletForJSONRPCRequest(request); + std::unique_ptr pwallet = interfaces::MakeWallet(wallet); + + if (request.fHelp || request.params.size() != 3) + throw runtime_error( + RPCHelpMan{"omni_sendtomany", + "\nCreate and broadcast a send-to-many transaction, which sends tokens from one address to multiple receivers.\n", + { + {"fromaddress", RPCArg::Type::STR, RPCArg::Optional::NO, "the address to send from\n"}, + {"propertyid", RPCArg::Type::NUM, RPCArg::Optional::NO, "the identifier of the tokens to send\n"}, + {"mapping", RPCArg::Type::ARR, RPCArg::Optional::NO, "an array with the receiving address and amount\n", + { + {"", RPCArg::Type::OBJ, RPCArg::Optional::OMITTED, "", + { + {"address", RPCArg::Type::STR, RPCArg::Optional::NO, "the address of the receiver\n"}, + {"amount", RPCArg::Type::STR, RPCArg::Optional::NO, "the amount to send\n"}, + }, + }, + }, + }, + }, + RPCResult{ + RPCResult::Type::STR_HEX, "", "The hex-encoded transaction hash." + }, + RPCExamples{ + HelpExampleCli("omni_sendtomany", "\"3M9qvHKtgARhqcMtM5cRT9VaiDJ5PSfQGY\" 1 \"[{\\\"address\\\":\\\"37FaKponF7zqoMLUjEiko25pDiuVH5YLEa\\\",\\\"amount\\\":\\\"10.5\\\"}]\"") + + HelpExampleRpc("omni_sendtomany", "\"3M9qvHKtgARhqcMtM5cRT9VaiDJ5PSfQGY\", 1, [{\"address\":\"37FaKponF7zqoMLUjEiko25pDiuVH5YLEa\",\"amount\":\"10.5\"}]") + } + }.ToString()); + + // obtain parameters & info + std::string fromAddress = ParseAddress(request.params[0]); + uint32_t propertyId = ParsePropertyId(request.params[1]); + RequireExistingProperty(propertyId); + + bool isDivisible = isPropertyDivisible(propertyId); + + std::vector receiverAddresses; + std::vector> outputValues; + int64_t amountToSend = 0; + + UniValue receiverList = request.params[2].get_array(); + for (unsigned int idx = 0; idx < receiverList.size(); idx++) { + const UniValue& input = receiverList[idx]; + const UniValue& o = input.get_obj(); + + std::string address = ParseAddress(find_value(o, "address")); + int64_t amount = ParseAmount(find_value(o, "amount"), isDivisible); + + receiverAddresses.push_back(address); + // placeholder index, corrected below once the payload size is known + outputValues.push_back(std::make_tuple(uint8_t(0), uint64_t(amount))); + amountToSend += amount; + } + + // First pass: build a throwaway payload to learn how many outputs the + // payload itself occupies, because that determines the first receiver vout. + std::vector testPayload = CreatePayload_SendToMany(propertyId, outputValues); + + int payloadOutputCount = GetDryPayloadOutputCount(fromAddress, "", testPayload, pwallet.get()); + if (payloadOutputCount < 0) { + throw JSONRPCError(payloadOutputCount, error_str(payloadOutputCount)); + } + + // perform checks + RequireBoundedStmReceiverNumber(payloadOutputCount + receiverAddresses.size()); + RequireBalance(fromAddress, propertyId, amountToSend); + + // Second pass: real output indices, starting right after the payload outputs + for (size_t i = 0; i < outputValues.size(); ++i) { + outputValues[i] = std::make_tuple(static_cast(payloadOutputCount + i), std::get<1>(outputValues[i])); + } + + std::vector payload = CreatePayload_SendToMany(propertyId, outputValues); + + // request the wallet build the transaction (and if needed commit it) + uint256 txid; + std::string rawHex; + int result = WalletTxBuilder(fromAddress, receiverAddresses, "", 0, payload, txid, rawHex, autoCommit, pwallet.get()); + + // check error and return the txid (or raw hex depending on autocommit) + if (result != 0) { + throw JSONRPCError(result, error_str(result)); + } else { + if (!autoCommit) { + return rawHex; + } else { + PendingAdd(txid, fromAddress, MSC_TYPE_SEND_TO_MANY, propertyId, amountToSend); + return txid.GetHex(); + } + } +} + static UniValue omni_sendall(const JSONRPCRequest& request) { std::shared_ptr const wallet = GetWalletForJSONRPCRequest(request); @@ -1837,6 +1932,7 @@ { "omni layer (transaction creation)", "omni_sendclosecrowdsale", &omni_sendclosecrowdsale, {"fromaddress", "propertyid"} }, { "omni layer (transaction creation)", "omni_sendchangeissuer", &omni_sendchangeissuer, {"fromaddress", "toaddress", "propertyid"} }, { "omni layer (transaction creation)", "omni_sendall", &omni_sendall, {"fromaddress", "toaddress", "ecosystem", "redeemaddress", "referenceamount"} }, + { "omni layer (transaction creation)", "omni_sendtomany", &omni_sendtomany, {"fromaddress", "propertyid", "mapping"} }, { "omni layer (transaction creation)", "omni_sendnonfungible", &omni_sendnonfungible, {"fromaddress", "toaddress", "propertyid", "tokenstart", "tokenend", "redeemaddress", "referenceamount"} }, { "omni layer (transaction creation)", "omni_setnonfungibledata", &omni_setnonfungibledata, {"propertyid", "tokenstart", "tokenend", "issuer", "data"} }, { "omni layer (transaction creation)", "omni_sendenablefreezing", &omni_sendenablefreezing, {"fromaddress", "propertyid"} }, diff --git a/src/omnicore/rpcvalues.h b/src/omnicore/rpcvalues.h --- a/src/omnicore/rpcvalues.h +++ b/src/omnicore/rpcvalues.h @@ -38,6 +38,7 @@ CMutableTransaction ParseMutableTransaction(const UniValue& value); CPubKey ParsePubKeyOrAddress(const interfaces::Wallet* iWallet, const UniValue& value); uint32_t ParseOutputIndex(const UniValue& value); +uint8_t ParseStmOutputIndex(const UniValue& value); /** Parses previous transaction outputs. */ std::vector ParsePrevTxs(const UniValue& value); diff --git a/src/omnicore/rpcvalues.cpp b/src/omnicore/rpcvalues.cpp --- a/src/omnicore/rpcvalues.cpp +++ b/src/omnicore/rpcvalues.cpp @@ -17,6 +17,7 @@ #include +#include #include #include @@ -197,6 +198,15 @@ return pubKey; } +uint8_t ParseStmOutputIndex(const UniValue& value) +{ + int64_t nOut = value.get_int64(); + if (nOut < 0 || nOut > std::numeric_limits::max()) { + throw JSONRPCError(RPC_DESERIALIZATION_ERROR, "Output index is out of range"); + } + return static_cast(nOut); +} + uint32_t ParseOutputIndex(const UniValue& value) { int nOut = value.get_int(); diff --git a/src/omnicore/rpcrequirements.h b/src/omnicore/rpcrequirements.h --- a/src/omnicore/rpcrequirements.h +++ b/src/omnicore/rpcrequirements.h @@ -19,6 +19,7 @@ void RequireNoOtherDExOffer(const std::string& address); void RequireMatchingDExAccept(const std::string& sellerAddress, uint32_t propertyId, const std::string& buyerAddress); void RequireSaneReferenceAmount(int64_t amount); +void RequireBoundedStmReceiverNumber(size_t numberOfOutputs); void RequireSaneDExPaymentWindow(const std::string& address, uint32_t propertyId); void RequireSaneDExFee(const std::string& address, uint32_t propertyId); void RequireSaneNonFungibleRange(int64_t tokenStart, int64_t tokenEnd); diff --git a/src/omnicore/rpcrequirements.cpp b/src/omnicore/rpcrequirements.cpp --- a/src/omnicore/rpcrequirements.cpp +++ b/src/omnicore/rpcrequirements.cpp @@ -30,6 +30,13 @@ } } +void RequireBoundedStmReceiverNumber(size_t numberOfOutputs) +{ + if (numberOfOutputs > 255) { + throw JSONRPCError(RPC_TYPE_ERROR, "Number of outputs must not exceed 255"); + } +} + void RequirePrimaryToken(uint32_t propertyId) { if (propertyId < 1 || 2 < propertyId) { diff --git a/src/omnicore/wallettxbuilder.h b/src/omnicore/wallettxbuilder.h --- a/src/omnicore/wallettxbuilder.h +++ b/src/omnicore/wallettxbuilder.h @@ -35,6 +35,35 @@ interfaces::Wallet* iWallet = nullptr, CAmount min_fee = 0); +/** + * Creates and sends a transaction with multiple receivers. + * + * The receiver outputs are appended in the order given, directly after the + * payload outputs. That ordering is what the Send-To-Many payload's output + * indices refer to, so it must not be changed. + */ +int WalletTxBuilder( + const std::string& senderAddress, + const std::vector& receiverAddresses, + const std::string& redemptionAddress, + int64_t referenceAmount, + const std::vector& payload, + uint256& retTxid, + std::string& retRawTx, + bool commit, + interfaces::Wallet* iWallet = nullptr, + CAmount min_fee = 0); + +/** + * Dry-runs the payload encoding and returns how many outputs the payload + * itself will occupy, so receiver output indices can be computed up front. + */ +int GetDryPayloadOutputCount( + const std::string& senderAddress, + const std::string& redemptionAddress, + const std::vector& payload, + interfaces::Wallet* iWallet = nullptr); + #ifdef ENABLE_WALLET /** * Creates and sends a raw transaction by selecting all coins from the sender diff --git a/src/omnicore/wallettxbuilder.cpp b/src/omnicore/wallettxbuilder.cpp --- a/src/omnicore/wallettxbuilder.cpp +++ b/src/omnicore/wallettxbuilder.cpp @@ -131,6 +131,135 @@ } + +/** Dry-runs the payload encoding and returns the number of payload outputs. */ +int GetDryPayloadOutputCount( + const std::string& senderAddress, + const std::string& redemptionAddress, + const std::vector& payload, + interfaces::Wallet* iWallet) +{ +#ifdef ENABLE_WALLET + if (!iWallet) return MP_ERR_WALLET_ACCESS; + + int omniTxClass = OMNI_CLASS_C; + if (!UseEncodingClassC(payload.size() + 1 /* OP_RETURN */ + 2 /* pushdata opcodes */)) omniTxClass = OMNI_CLASS_B; + + std::vector > vecSend; + + switch (omniTxClass) { + case OMNI_CLASS_B: { + CPubKey redeemingPubKey; + const std::string& sAddress = redemptionAddress.empty() ? senderAddress : redemptionAddress; + if (!AddressToPubKey(iWallet, sAddress, redeemingPubKey)) { + return MP_REDEMP_BAD_VALIDATION; + } + if (!OmniCore_Encode_ClassB(senderAddress, redeemingPubKey, payload, vecSend)) { return MP_ENCODING_ERROR; } + break; } + case OMNI_CLASS_C: + if (!OmniCore_Encode_ClassC(payload, vecSend)) { return MP_ENCODING_ERROR; } + break; + } + + return static_cast(vecSend.size()); +#else + return MP_ERR_WALLET_ACCESS; +#endif +} + +/** Creates and sends a transaction with multiple receivers. */ +int WalletTxBuilder( + const std::string& senderAddress, + const std::vector& receiverAddresses, + const std::string& redemptionAddress, + int64_t referenceAmount, + const std::vector& payload, + uint256& retTxid, + std::string& retRawTx, + bool commit, + interfaces::Wallet* iWallet, + CAmount minFee) +{ +#ifdef ENABLE_WALLET + if (!iWallet) return MP_ERR_WALLET_ACCESS; + + // Determine the class to send the transaction via - default is Class C + int omniTxClass = OMNI_CLASS_C; + if (!UseEncodingClassC(payload.size() + 1 /* OP_RETURN */ + 2 /* pushdata opcodes */)) omniTxClass = OMNI_CLASS_B; + + CCoinControl coinControl; + std::vector > vecSend; + + coinControl.destChange = DecodeDestination(senderAddress); + + if (0 > mastercore::SelectCoins(*iWallet, senderAddress, coinControl, referenceAmount * (int64_t)receiverAddresses.size())) { + return MP_INPUTS_INVALID; + } + + // Encode the data outputs first - they always come before the receivers + switch (omniTxClass) { + case OMNI_CLASS_B: { + CPubKey redeemingPubKey; + const std::string& sAddress = redemptionAddress.empty() ? senderAddress : redemptionAddress; + if (!AddressToPubKey(iWallet, sAddress, redeemingPubKey)) { + return MP_REDEMP_BAD_VALIDATION; + } + if (!OmniCore_Encode_ClassB(senderAddress, redeemingPubKey, payload, vecSend)) { return MP_ENCODING_ERROR; } + break; } + case OMNI_CLASS_C: + if (!OmniCore_Encode_ClassC(payload, vecSend)) { return MP_ENCODING_ERROR; } + break; + } + + // Append one dust output per receiver, IN LIST ORDER. The payload's output + // indices point at these positions, so the ordering is consensus critical. + for (const std::string& receiverAddress : receiverAddresses) { + if (receiverAddress.empty()) return MP_ENCODING_ERROR; + CScript scriptPubKey = GetScriptForDestination(DecodeDestination(receiverAddress)); + vecSend.push_back(std::make_pair(scriptPubKey, 0 < referenceAmount ? referenceAmount : OmniGetDustThreshold(scriptPubKey))); + } + + if (!coinControl.HasSelected()) return MP_ERR_INPUTSELECT_FAIL; + + std::vector vecRecipients; + for (size_t i = 0; i < vecSend.size(); ++i) { + const std::pair& vec = vecSend[i]; + CRecipient recipient = {vec.first, vec.second, false}; + vecRecipients.push_back(recipient); + } + + CAmount nFeeRet = 0; + int nChangePosInOut = -1; + bilingual_str strFailReason; + auto wtxNew = iWallet->createTransaction(vecRecipients, coinControl, true /* sign */, nChangePosInOut, nFeeRet, strFailReason, false, minFee); + + if (!wtxNew) { + PrintToLog("%s: ERROR: wallet transaction creation failed: %s\n", __func__, strFailReason.original); + return MP_ERR_CREATE_TX; + } + + // The change output must never be inserted before the receiver outputs, + // or every output index in the payload shifts. + if (nChangePosInOut >= 0 && static_cast(nChangePosInOut) < vecSend.size()) { + PrintToLog("%s: ERROR: change output at position %d would shift receiver outputs\n", __func__, nChangePosInOut); + return MP_ERR_CREATE_TX; + } + + if (!commit) { + retRawTx = EncodeHexTx(*wtxNew); + return 0; + } else { + PrintToLog("%s: %s; nFeeRet = %d\n", __func__, wtxNew->ToString(), nFeeRet); + std::vector reserved_keys; + iWallet->commitTransaction(wtxNew, {}, {}, reserved_keys); + retTxid = wtxNew->GetHash(); + return 0; + } +#else + return MP_ERR_WALLET_ACCESS; +#endif +} + #ifdef ENABLE_WALLET /** Locks all available coins that are not in the set of destinations. */ static void LockUnrelatedCoins( diff --git a/src/rpc/client.cpp b/src/rpc/client.cpp --- a/src/rpc/client.cpp +++ b/src/rpc/client.cpp @@ -300,6 +300,8 @@ /* Omni Core - payload creation */ { "omni_createpayload_simplesend", 0, "propertyid" }, + { "omni_createpayload_sendtomany", 0, "propertyid" }, + { "omni_createpayload_sendtomany", 1, "mapping" }, + { "omni_sendtomany", 1, "propertyid" }, + { "omni_sendtomany", 2, "mapping" }, { "omni_createpayload_sendall", 0, "ecosystem" }, { "omni_createpayload_dexsell", 0, "propertyidforsale" }, { "omni_createpayload_dexsell", 3, "paymentwindow" },