diff --git a/src/assets/assets.cpp b/src/assets/assets.cpp index b37f7f218b..49268f91ff 100644 --- a/src/assets/assets.cpp +++ b/src/assets/assets.cpp @@ -207,6 +207,17 @@ std::string GetParentName(const std::string& name) return name; } +std::string GetUniqueAssetName(const std::string& parent, const std::string& tag) +{ + if (!IsRootNameValid(parent)) + return ""; + + if (!IsUniqueTagValid(tag)) + return ""; + + return parent + "#" + tag; +} + bool CNewAsset::IsNull() const { return strName == ""; @@ -542,6 +553,10 @@ bool CTransaction::IsNewAsset() const if (!CheckIssueDataTx(vout[vout.size() - 1])) return false; + // Don't overlap with IsNewUniqueAsset() + if (IsScriptNewUniqueAsset(vout[vout.size() - 1].scriptPubKey)) + return false; + return true; } @@ -584,6 +599,95 @@ bool CTransaction::VerifyNewAsset() const return false; } +bool CTransaction::IsNewUniqueAsset() const +{ + // Check trailing outpoint for issue data with unique asset name + if (!CheckIssueDataTx(vout[vout.size() - 1])) + return false; + + if (!IsScriptNewUniqueAsset(vout[vout.size() - 1].scriptPubKey)) + return false; + + return true; +} + +bool CTransaction::VerifyNewUniqueAsset(CCoinsViewCache& view) const +{ + // Must contain at least 3 outpoints (RVN burn, owner change and one or more new unique assets that share a root (should be in trailing position)) + if (vout.size() < 3) + return false; + + // check for (and count) new unique asset outpoints. make sure they share a root. + std::string assetRoot = ""; + int assetOutpointCount = 0; + for (auto out : vout) { + if (IsScriptNewUniqueAsset(out.scriptPubKey)) { + CNewAsset asset; + std::string address; + if (!AssetFromScript(out.scriptPubKey, asset, address)) + return false; + std::string root = GetParentName(asset.strName); + if (assetRoot.compare("") == 0) + assetRoot = root; + if (assetRoot.compare(root) != 0) + return false; + assetOutpointCount += 1; + } + } + if (assetOutpointCount == 0) + return false; + + // check for burn outpoint (must account for each new asset) + bool fBurnOutpointFound = false; + for (auto out : vout) + if (CheckIssueBurnTx(out, AssetType::UNIQUE, assetOutpointCount)) { + fBurnOutpointFound = true; + break; + } + if (!fBurnOutpointFound) + return false; + + // check for owner change outpoint that matches root + bool fOwnerOutFound = false; + for (auto out : vout) { + if (CheckTransferOwnerTx(out)) { + fOwnerOutFound = true; + break; + } + } + + if (!fOwnerOutFound) + return false; + + // The owner change output must match a corresponding owner input + bool fFoundCorrectInput = false; + for (unsigned int i = 0; i < vin.size(); ++i) { + const COutPoint &prevout = vin[i].prevout; + const Coin& coin = view.AccessCoin(prevout); + assert(!coin.IsSpent()); + + int nType = -1; + bool fOwner = false; + if (coin.out.scriptPubKey.IsAssetScript(nType, fOwner)) { + std::string strAssetName; + CAmount nAssetAmount; + if (!GetAssetInfoFromCoin(coin, strAssetName, nAssetAmount)) + continue; + if (IsAssetNameAnOwner(strAssetName)) { + if (strAssetName == assetRoot + OWNER_TAG) { + fFoundCorrectInput = true; + break; + } + } + } + } + + if (!fFoundCorrectInput) + return false; + + return true; +} + bool CTransaction::IsReissueAsset() const { // Check for the reissue asset data CTxOut. This will always be the last output in the transaction @@ -1591,7 +1695,7 @@ bool IsAssetUnitsValid(const CAmount& units) return false; } -bool CheckIssueBurnTx(const CTxOut& txOut, const AssetType& type) +bool CheckIssueBurnTx(const CTxOut& txOut, const AssetType& type, const int numberIssued) { CAmount burnAmount = 0; std::string burnAddress = ""; @@ -1609,6 +1713,9 @@ bool CheckIssueBurnTx(const CTxOut& txOut, const AssetType& type) return false; } + // If issuing multiple (unique) assets need to burn for each + burnAmount *= numberIssued; + // Check the first transaction for the required Burn Amount for the asset type if (!(txOut.nValue == burnAmount)) return false; @@ -1630,6 +1737,11 @@ bool CheckIssueBurnTx(const CTxOut& txOut, const AssetType& type) return true; } +bool CheckIssueBurnTx(const CTxOut& txOut, const AssetType& type) +{ + return CheckIssueBurnTx(txOut, type, 1); +} + bool CheckReissueBurnTx(const CTxOut& txOut) { // Check the first transaction and verify that the correct RVN Amount @@ -1702,6 +1814,31 @@ bool IsScriptNewAsset(const CScript& scriptPubKey, int& nStartingIndex) return false; } +bool IsScriptNewUniqueAsset(const CScript& scriptPubKey) +{ + int index = 0; + return IsScriptNewUniqueAsset(scriptPubKey, index); +} + +bool IsScriptNewUniqueAsset(const CScript& scriptPubKey, int& nStartingIndex) +{ + int nType = 0; + bool fIsOwner = false; + if (!scriptPubKey.IsAssetScript(nType, fIsOwner, nStartingIndex)) + return false; + + CNewAsset asset; + std::string address; + if (!AssetFromScript(scriptPubKey, asset, address)) + return false; + + AssetType assetType; + if (!IsAssetNameValid(asset.strName, assetType)) + return false; + + return AssetType::UNIQUE == assetType; +} + bool IsScriptOwnerAsset(const CScript& scriptPubKey) { @@ -2165,14 +2302,22 @@ std::string EncodeIPFS(std::string decoded){ bool CreateAssetTransaction(CWallet* pwallet, const CNewAsset& asset, const std::string& address, std::pair& error, std::string& rvnChangeAddress, CWalletTx& wtxNew, CReserveKey& reservekey, CAmount& nFeeRequired) { + std::vector assets; + assets.push_back(asset); + return CreateAssetTransaction(pwallet, assets, address, error, rvnChangeAddress, wtxNew, reservekey, nFeeRequired); +} +bool CreateAssetTransaction(CWallet* pwallet, const std::vector assets, const std::string& address, std::pair& error, std::string& rvnChangeAddress, CWalletTx& wtxNew, CReserveKey& reservekey, CAmount& nFeeRequired) +{ std::string change_address = rvnChangeAddress; // Validate the assets data std::string strError; - if (!asset.IsValid(strError, *passets)) { - error = std::make_pair(RPC_INVALID_PARAMETER, strError); - return false; + for (auto asset : assets) { + if (!asset.IsValid(strError, *passets)) { + error = std::make_pair(RPC_INVALID_PARAMETER, strError); + return false; + } } if (!change_address.empty()) { @@ -2202,15 +2347,28 @@ bool CreateAssetTransaction(CWallet* pwallet, const CNewAsset& asset, const std: change_address = EncodeDestination(keyID); } - AssetType assetType; - if (!IsAssetNameValid(asset.strName, assetType)) { - error = std::make_pair(RPC_INVALID_PARAMETER, "Asset name not valid"); - return false; + std::string parentName; + for (auto asset : assets) { + if (!IsAssetNameValid(asset.strName, assetType)) { + error = std::make_pair(RPC_INVALID_PARAMETER, "Asset name not valid"); + return false; + } + if (assets.size() > 1 && assetType != AssetType::UNIQUE) { + error = std::make_pair(RPC_INVALID_PARAMETER, "Only unique assets can be issued in bulk."); + return false; + } + std::string parent = GetParentName(asset.strName); + if (parentName.empty()) + parentName = parent; + if (parentName != parent) { + error = std::make_pair(RPC_INVALID_PARAMETER, "All assets must have the same parent."); + return false; + } } // Assign the correct burn amount and the correct burn address depending on the type of asset issuance that is happening - CAmount burnAmount = GetBurnAmount(assetType); + CAmount burnAmount = GetBurnAmount(assetType) * assets.size(); CScript scriptPubKey = GetScriptForDestination(DecodeDestination(GetBurnAddress(assetType))); CAmount curBalance = pwallet->GetBalance(); @@ -2242,27 +2400,28 @@ bool CreateAssetTransaction(CWallet* pwallet, const CNewAsset& asset, const std: CRecipient recipient = {scriptPubKey, burnAmount, fSubtractFeeFromAmount}; vecSend.push_back(recipient); - // If the asset is a subasset. We need to send the ownertoken change back to ourselfs - if (assetType == AssetType::SUB) { + // If the asset is a subasset or unique asset. We need to send the ownertoken change back to ourselfs + if (assetType == AssetType::SUB || assetType == AssetType::UNIQUE) { // Get the script for the destination address for the assets CScript scriptTransferOwnerAsset = GetScriptForDestination(DecodeDestination(change_address)); - std::string parent_name = GetParentName(asset.strName); - CAssetTransfer assetTransfer(parent_name + OWNER_TAG, OWNER_ASSET_AMOUNT); + CAssetTransfer assetTransfer(parentName + OWNER_TAG, OWNER_ASSET_AMOUNT); assetTransfer.ConstructTransaction(scriptTransferOwnerAsset); CRecipient rec = {scriptTransferOwnerAsset, 0, fSubtractFeeFromAmount}; vecSend.push_back(rec); } - // Get the owner outpoints if this is a subasset - if (assetType == AssetType::SUB) { + // Get the owner outpoints if this is a subasset or unique asset + if (assetType == AssetType::SUB || assetType == AssetType::UNIQUE) { // Verify that this wallet is the owner for the asset, and get the owner asset outpoint - if (!VerifyWalletHasAsset(GetParentName(asset.strName) + OWNER_TAG, error)) { - return false; + for (auto asset : assets) { + if (!VerifyWalletHasAsset(parentName + OWNER_TAG, error)) { + return false; + } } } - if (!pwallet->CreateTransactionWithAsset(vecSend, wtxNew, reservekey, nFeeRequired, nChangePosRet, strTxError, coin_control, asset, DecodeDestination(address), assetType)) { + if (!pwallet->CreateTransactionWithAssets(vecSend, wtxNew, reservekey, nFeeRequired, nChangePosRet, strTxError, coin_control, assets, DecodeDestination(address), assetType)) { if (!fSubtractFeeFromAmount && burnAmount + nFeeRequired > curBalance) strTxError = strprintf("Error: This transaction requires a transaction fee of at least %s", FormatMoney(nFeeRequired)); error = std::make_pair(RPC_WALLET_ERROR, strTxError); diff --git a/src/assets/assets.h b/src/assets/assets.h index ad18358717..75c2fd70ef 100644 --- a/src/assets/assets.h +++ b/src/assets/assets.h @@ -30,6 +30,9 @@ #define OWNER_UNITS 0 #define MIN_ASSET_LENGTH 3 #define OWNER_ASSET_AMOUNT 1 * COIN +#define UNIQUE_ASSET_AMOUNT 1 * COIN +#define UNIQUE_ASSET_UNITS 0 +#define UNIQUE_ASSETS_REISSUABLE 0 #define ASSET_TRANSFER_STRING "transfer_asset" #define ASSET_NEW_STRING "new_asset" @@ -308,8 +311,10 @@ std::string GetBurnAddress(const AssetType type); bool IsAssetNameValid(const std::string& name); bool IsAssetNameValid(const std::string& name, AssetType& assetType); +bool IsUniqueTagValid(const std::string& tag); bool IsAssetNameAnOwner(const std::string& name); std::string GetParentName(const std::string& name); // Gets the parent name of a subasset TEST/TESTSUB would return TEST +std::string GetUniqueAssetName(const std::string& parent, const std::string& tag); bool IsAssetNameSizeValid(const std::string& name); @@ -324,6 +329,7 @@ bool AssetFromScript(const CScript& scriptPubKey, CNewAsset& asset, std::string& bool OwnerAssetFromScript(const CScript& scriptPubKey, std::string& assetName, std::string& strAddress); bool ReissueAssetFromScript(const CScript& scriptPubKey, CReissueAsset& reissue, std::string& strAddress); +bool CheckIssueBurnTx(const CTxOut& txOut, const AssetType& type, const int numberIssued); bool CheckIssueBurnTx(const CTxOut& txOut, const AssetType& type); bool CheckReissueBurnTx(const CTxOut& txOut); @@ -335,10 +341,12 @@ bool CheckTransferOwnerTx(const CTxOut& txOut); bool CheckAmountWithUnits(const CAmount& nAmount, const uint8_t nUnits); bool IsScriptNewAsset(const CScript& scriptPubKey, int& nStartingIndex); +bool IsScriptNewUniqueAsset(const CScript& scriptPubKey, int& nStartingIndex); bool IsScriptOwnerAsset(const CScript& scriptPubKey, int& nStartingIndex); bool IsScriptReissueAsset(const CScript& scriptPubKey, int& nStartingIndex); bool IsScriptTransferAsset(const CScript& scriptPubKey, int& nStartingIndex); bool IsScriptNewAsset(const CScript& scriptPubKey); +bool IsScriptNewUniqueAsset(const CScript& scriptPubKey); bool IsScriptOwnerAsset(const CScript& scriptPubKey); bool IsScriptReissueAsset(const CScript& scriptPubKey); bool IsScriptTransferAsset(const CScript& scriptPubKey); @@ -370,6 +378,7 @@ std::string DecodeIPFS(std::string encoded); std::string EncodeIPFS(std::string decoded); bool CreateAssetTransaction(CWallet* pwallet, const CNewAsset& asset, const std::string& address, std::pair& error, std::string& rvnChangeAddress, CWalletTx& wtxNew, CReserveKey& reservekey, CAmount& nFeeRequired); +bool CreateAssetTransaction(CWallet* pwallet, const std::vector assets, const std::string& address, std::pair& error, std::string& rvnChangeAddress, CWalletTx& wtxNew, CReserveKey& reservekey, CAmount& nFeeRequired); bool CreateReissueAssetTransaction(CWallet* pwallet, const CReissueAsset& asset, const std::string& address, const std::string& changeAddress, std::pair& error, CWalletTx& wtxNew, CReserveKey& reservekey, CAmount& nFeeRequired); bool CreateTransferAssetTransaction(CWallet* pwallet, const std::vector< std::pair >vTransfers, const std::string& changeAddress, std::pair& error, CWalletTx& wtxNew, CReserveKey& reservekey, CAmount& nFeeRequired); bool SendAssetTransaction(CWallet* pwallet, CWalletTx& transaction, CReserveKey& reserveKey, std::pair& error, std::string& txid); diff --git a/src/assets/assettypes.h b/src/assets/assettypes.h index c64c29db57..54266832c0 100644 --- a/src/assets/assettypes.h +++ b/src/assets/assettypes.h @@ -27,6 +27,20 @@ enum AssetType REISSUE }; +std::string PrintAssetType(AssetType& assetType) { + switch (assetType) { + case ROOT: return "ROOT"; + case OWNER: return "OWNER"; + case SUB: return "SUB"; + case UNIQUE: return "UNIQUE"; + case MSGCHANNEL: return "MSGCHANNEL"; + case VOTE: return "VOTE"; + case INVALID: return "INVALID"; + case REISSUE: return "REISSUE"; + default: return "UNKNOWN"; + } +} + enum IssueAssetType { ISSUE_ROOT = 0, diff --git a/src/coins.cpp b/src/coins.cpp index fc74296db8..19d8456b8f 100644 --- a/src/coins.cpp +++ b/src/coins.cpp @@ -161,6 +161,27 @@ void AddCoins(CCoinsViewCache& cache, const CTransaction &tx, int nHeight, bool if (!assetsCache->AddPossibleOutPoint(possibleMine)) error("%s: Failed to add an reissued asset I own to my Unspent Asset Cache. Asset Name : %s", __func__, reissue.strName); + } else if (tx.IsNewUniqueAsset()) { + for (int n = 0; n < tx.vout.size(); n++) { + auto out = tx.vout[n]; + + CNewAsset asset; + std::string strAddress; + + if (IsScriptNewUniqueAsset(out.scriptPubKey)) { + AssetFromScript(out.scriptPubKey, asset, strAddress); + + // Add the new asset to cache + if (!assetsCache->AddNewAsset(asset, strAddress)) + error("%s : Failed at adding a new asset to our cache. asset: %s", __func__, + asset.strName); + + CAssetCachePossibleMine possibleMine(asset.strName, COutPoint(tx.GetHash(), n), out); + if (!assetsCache->AddPossibleOutPoint(possibleMine)) + error("%s: Failed to add an asset I own to my Unspent Asset Cache. Asset Name : %s", + __func__, asset.strName); + } + } } } } diff --git a/src/consensus/tx_verify.cpp b/src/consensus/tx_verify.cpp index 224a199f59..9ce9559dc2 100644 --- a/src/consensus/tx_verify.cpp +++ b/src/consensus/tx_verify.cpp @@ -209,11 +209,24 @@ bool CheckTransaction(const CTransaction& tx, CValidationState &state, CAssetsCa if (!TransferAssetFromScript(txout.scriptPubKey, transfer, address)) return state.DoS(100, false, REJECT_INVALID, "bad-txns-transfer-asset-bad-deserialize"); + // Check asset name validity and get type + AssetType assetType; + if (!IsAssetNameValid(transfer.strName, assetType)) { + return state.DoS(100, false, REJECT_INVALID, "bad-txns-transfer-asset-name-invalid"); + } + // If the transfer is an ownership asset. Check to make sure that it is OWNER_ASSET_AMOUNT if (IsAssetNameAnOwner(transfer.strName)) { if (transfer.nAmount != OWNER_ASSET_AMOUNT) return state.DoS(100, false, REJECT_INVALID, "bad-txns-transfer-owner-amount-was-not-1"); } + + // If the transfer is a unique asset. Check to make sure that it is UNIQUE_ASSET_AMOUNT + if (assetType == AssetType::UNIQUE) { + if (transfer.nAmount != UNIQUE_ASSET_AMOUNT) + return state.DoS(100, false, REJECT_INVALID, "bad-txns-transfer-unique-amount-was-not-1"); + } + } } } @@ -245,7 +258,6 @@ bool CheckTransaction(const CTransaction& tx, CValidationState &state, CAssetsCa /** RVN START */ if (AreAssetsDeployed()) { if (assetCache) { - // Get the new asset from the transaction if (tx.IsNewAsset()) { if(!tx.VerifyNewAsset()) return state.DoS(100, false, REJECT_INVALID, "bad-txns-verifying-issue-asset"); @@ -284,6 +296,50 @@ bool CheckTransaction(const CTransaction& tx, CValidationState &state, CAssetsCa if (!foundOwnerAsset) return state.DoS(100, false, REJECT_INVALID, "bad-txns-reissue-asset-bad-owner-asset"); + } else if (tx.IsNewUniqueAsset()) { + + std::string assetRoot = ""; + int assetCount = 0; + + for (auto out : tx.vout) { + CNewAsset asset; + std::string strAddress; + + if (IsScriptNewUniqueAsset(out.scriptPubKey)) { + if (!AssetFromScript(out.scriptPubKey, asset, strAddress)) + return state.DoS(100, false, REJECT_INVALID, "bad-txns-issue-unique-asset"); + + std::string strError = ""; + if (!asset.IsValid(strError, *assetCache, fMemPoolCheck, fCheckDuplicateInputs)) + return state.DoS(100, false, REJECT_INVALID, "bad-txns-" + strError); + + std::string root = GetParentName(asset.strName); + if (assetRoot.compare("") == 0) + assetRoot = root; + if (assetRoot.compare(root) != 0) + return state.DoS(100, false, REJECT_INVALID, "bad-txns-issue-unique-asset-mismatched-root"); + + assetCount += 1; + } + } + + if (assetCount < 1) + return state.DoS(100, false, REJECT_INVALID, "bad-txns-issue-unique-asset-no-outputs"); + + bool foundOwnerAsset = false; + for (auto out : tx.vout) { + CAssetTransfer transfer; + std::string transferAddress; + if (TransferAssetFromScript(out.scriptPubKey, transfer, transferAddress)) { + if (assetRoot + OWNER_TAG == transfer.strName) { + foundOwnerAsset = true; + break; + } + } + } + + if (!foundOwnerAsset) + return state.DoS(100, false, REJECT_INVALID, "bad-txns-issue-unique-asset-bad-owner-asset"); } } } diff --git a/src/primitives/transaction.h b/src/primitives/transaction.h index c3cf095ee7..dbc985159e 100644 --- a/src/primitives/transaction.h +++ b/src/primitives/transaction.h @@ -329,6 +329,8 @@ class CTransaction /** RVN START */ bool IsNewAsset() const; bool VerifyNewAsset() const; + bool IsNewUniqueAsset() const; + bool VerifyNewUniqueAsset(CCoinsViewCache& view) const; bool IsReissueAsset() const; bool VerifyReissueAsset(CCoinsViewCache& view) const; /** RVN END */ diff --git a/src/rpc/assets.cpp b/src/rpc/assets.cpp index ad06429f3c..4e362c52c2 100644 --- a/src/rpc/assets.cpp +++ b/src/rpc/assets.cpp @@ -60,21 +60,23 @@ UniValue UnitValueFromAmount(const CAmount& amount, const std::string asset_name UniValue issue(const JSONRPCRequest& request) { - if (request.fHelp || !AreAssetsDeployed() || request.params.size() < 2 || request.params.size() > 8) + if (request.fHelp || !AreAssetsDeployed() || request.params.size() < 1 || request.params.size() > 8) throw std::runtime_error( "issue \"asset_name\" qty \"( to_address )\" \"( change_address )\" ( units ) ( reissuable ) ( has_ipfs ) \"( ipfs_hash )\"\n" + AssetActivationWarning() + - "\nIssue an asset or subasset with unique name.\n" + "\nIssue an asset, subasset or unique asset.\n" + "Asset name must not conflict with any existing asset.\n" "Unit as the number of decimals precision for the asset (0 for whole units (\"1\"), 8 for max precision (\"1.00000000\")\n" "Reissuable is true/false for whether additional units can be issued by the original issuer.\n" + "If issuing a unique asset these values are required (and will be defaulted to): qty=1, units=0, reissuable=false.\n" "\nArguments:\n" "1. \"asset_name\" (string, required) a unique name\n" - "2. \"qty\" (numeric, required) the number of units to be issued\n" + "2. \"qty\" (numeric, optional, default=1) the number of units to be issued\n" "3. \"to_address\" (string), optional, default=\"\"), address asset will be sent to, if it is empty, address will be generated for you\n" "4. \"change_address\" (string), optional, default=\"\"), address the the rvn change will be sent to, if it is empty, change address will be generated for you\n" - "5. \"units\" (integer, optional, default=8, min=0, max=8), the number of decimals precision for the asset (0 for whole units (\"1\"), 8 for max precision (\"1.00000000\")\n" - "6. \"reissuable\" (boolean, optional, default=true), whether future reissuance is allowed\n" + "5. \"units\" (integer, optional, default=0, min=0, max=8), the number of decimals precision for the asset (0 for whole units (\"1\"), 8 for max precision (\"1.00000000\")\n" + "6. \"reissuable\" (boolean, optional, default=true (false for unique assets)), whether future reissuance is allowed\n" "7. \"has_ipfs\" (boolean, optional, default=false), whether ifps hash is going to be added to the asset\n" "8. \"ipfs_hash\" (string, optional but required if has_ipfs = 1), an ipfs hash\n" @@ -87,6 +89,7 @@ UniValue issue(const JSONRPCRequest& request) + HelpExampleCli("issue", "\"myassetname\" 1000 \"myaddress\" \"changeaddress\" 4") + HelpExampleCli("issue", "\"myassetname\" 1000 \"myaddress\" \"changeaddress\" 2 true") + HelpExampleCli("issue", "\"myassetname/mysubasset\" 1000 \"myaddress\" \"changeaddress\" 2 true") + + HelpExampleCli("issue", "\"myassetname#uniquetag\"") ); CWallet * const pwallet = GetWalletForJSONRPCRequest(request); @@ -99,9 +102,21 @@ UniValue issue(const JSONRPCRequest& request) EnsureWalletIsUnlocked(pwallet); - std::string asset_name = request.params[0].get_str(); + // Check asset name and infer assetType + std::string assetName = request.params[0].get_str(); + AssetType assetType; + if (!IsAssetNameValid(assetName, assetType)) { + throw JSONRPCError(RPC_INVALID_PARAMETER, std::string("Invalid asset name: ") + assetName); + } - CAmount nAmount = AmountFromValue(request.params[1]); + // Check assetType supported + if (!(assetType == AssetType::ROOT || assetType == AssetType::SUB || assetType == AssetType::UNIQUE)) { + throw JSONRPCError(RPC_INVALID_PARAMETER, std::string("Unsupported asset type: ") + PrintAssetType(assetType)); + } + + CAmount nAmount = COIN; + if (request.params.size() > 1) + nAmount = AmountFromValue(request.params[1]); std::string address = ""; if (request.params.size() > 2) @@ -143,10 +158,11 @@ UniValue issue(const JSONRPCRequest& request) } } - int units = 8; + int units = 0; if (request.params.size() > 4) units = request.params[4].get_int(); - bool reissuable = true; + + bool reissuable = assetType != AssetType::UNIQUE; if (request.params.size() > 5) reissuable = request.params[5].get_bool(); @@ -158,7 +174,12 @@ UniValue issue(const JSONRPCRequest& request) if (request.params.size() > 7 && has_ipfs) ipfs_hash = request.params[7].get_str(); - CNewAsset asset(asset_name, nAmount, units, reissuable ? 1 : 0, has_ipfs ? 1 : 0, DecodeIPFS(ipfs_hash)); + // check for required unique asset params + if (assetType == AssetType::UNIQUE && (nAmount != COIN || units != 0 || reissuable)) { + throw JSONRPCError(RPC_INVALID_PARAMETER, std::string("Invalid parameters for issuing a unique asset.")); + } + + CNewAsset asset(assetName, nAmount, units, reissuable ? 1 : 0, has_ipfs ? 1 : 0, DecodeIPFS(ipfs_hash)); CReserveKey reservekey(pwallet); CWalletTx transaction; @@ -179,6 +200,147 @@ UniValue issue(const JSONRPCRequest& request) return result; } +UniValue issueunique(const JSONRPCRequest& request) +{ + if (request.fHelp || !AreAssetsDeployed() || request.params.size() < 2 || request.params.size() > 3) + throw std::runtime_error( + "issueunique \"root_name\" [asset_tags] ( [ipfs_hashes] ) \"( to_address )\" \"( change_address )\"\n" + + AssetActivationWarning() + + "\nIssue unique asset(s).\n" + "root_name must be an asset you own.\n" + "An asset will be created for each element of asset_tags.\n" + "If provided ipfs_hashes must be the same length as asset_tags.\n" + "Five (5) RVN will be burned for each asset created.\n" + + "\nArguments:\n" + "1. \"root_name\" (string, required) name of the asset the unique asset(s) are being issued under\n" + "2. \"asset_tags\" (array, required) the unique tag for each asset which is to be issued\n" + "3. \"ipfs_hashes\" (array, optional) ipfs hashes corresponding to each supplied tag (should be same size as \"asset_tags\")\n" + "4. \"to_address\" (string, optional, default=\"\"), address assets will be sent to, if it is empty, address will be generated for you\n" + "5. \"change_address\" (string, optional, default=\"\"), address the the rvn change will be sent to, if it is empty, change address will be generated for you\n" + + "\nResult:\n" + "\"txid\" (string) The transaction id\n" + + "\nExamples:\n" + + HelpExampleCli("issueunique", "\"MY_ASSET\" [\"primo\",\"secundo\"]") + + HelpExampleCli("issueunique", "\"MY_ASSET\" [\"primo\",\"secundo\"] [\"first_hash\",\"second_hash\"]") + ); + + CWallet * const pwallet = GetWalletForJSONRPCRequest(request); + if (!EnsureWalletIsAvailable(pwallet, request.fHelp)) { + return NullUniValue; + } + + ObserveSafeMode(); + LOCK2(cs_main, pwallet->cs_wallet); + + EnsureWalletIsUnlocked(pwallet); + + + const std::string rootName = request.params[0].get_str(); + AssetType assetType; + if (!IsAssetNameValid(rootName, assetType)) { + throw JSONRPCError(RPC_INVALID_PARAMETER, std::string("Invalid asset name: ") + rootName); + } + if (assetType != AssetType::ROOT && assetType != AssetType::SUB) { + throw JSONRPCError(RPC_INVALID_PARAMETER, std::string("Root asset must be a regular top-level or sub-asset.")); + } + + const UniValue& assetTags = request.params[1]; + if (!assetTags.isArray() || assetTags.size() < 1) { + throw JSONRPCError(RPC_INVALID_PARAMETER, std::string("Asset tags must be a non-empty array.")); + } + + const UniValue& ipfsHashes = request.params[2]; + if (!ipfsHashes.isNull()) { + if (!ipfsHashes.isArray() || ipfsHashes.size() != assetTags.size()) { + throw JSONRPCError(RPC_INVALID_PARAMETER, std::string("If provided, IPFS hashes must be an array of the same size as the asset tags array.")); + } + } + + std::string address = ""; + if (request.params.size() > 3) + address = request.params[3].get_str(); + + if (!address.empty()) { + CTxDestination destination = DecodeDestination(address); + if (!IsValidDestination(destination)) { + throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, std::string("Invalid Raven address: ") + address); + } + } else { + // Create a new address + std::string strAccount; + + if (!pwallet->IsLocked()) { + pwallet->TopUpKeyPool(); + } + + // Generate a new key that is added to wallet + CPubKey newKey; + if (!pwallet->GetKeyFromPool(newKey)) { + throw JSONRPCError(RPC_WALLET_KEYPOOL_RAN_OUT, "Error: Keypool ran out, please call keypoolrefill first"); + } + CKeyID keyID = newKey.GetID(); + + pwallet->SetAddressBook(keyID, strAccount, "receive"); + + address = EncodeDestination(keyID); + } + + std::string changeAddress = ""; + if (request.params.size() > 4) + changeAddress = request.params[4].get_str(); + if (!changeAddress.empty()) { + CTxDestination destination = DecodeDestination(changeAddress); + if (!IsValidDestination(destination)) { + throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, + std::string("Invalid Change Address: Invalid Raven address: ") + changeAddress); + } + } + + std::vector assets; + for (int i = 0; i < assetTags.size(); i++) { + std::string tag = assetTags[i].get_str(); + + if (!IsUniqueTagValid(tag)) { + throw JSONRPCError(RPC_INVALID_PARAMETER, std::string("Unique asset tag is invalid: " + tag)); + } + + std::string assetName = GetUniqueAssetName(rootName, tag); + CNewAsset asset; + + if (ipfsHashes.isNull()) + { + asset = CNewAsset(assetName, UNIQUE_ASSET_AMOUNT, UNIQUE_ASSET_UNITS, UNIQUE_ASSETS_REISSUABLE, 0, ""); + } + else + { + asset = CNewAsset(assetName, UNIQUE_ASSET_AMOUNT, UNIQUE_ASSET_UNITS, UNIQUE_ASSETS_REISSUABLE, 1, + DecodeIPFS(ipfsHashes[i].get_str())); + } + + assets.push_back(asset); + } + + CReserveKey reservekey(pwallet); + CWalletTx transaction; + CAmount nRequiredFee; + std::pair error; + + // Create the Transaction + if (!CreateAssetTransaction(pwallet, assets, address, error, changeAddress, transaction, reservekey, nRequiredFee)) + throw JSONRPCError(error.first, error.second); + + // Send the Transaction to the network + std::string txid; + if (!SendAssetTransaction(pwallet, transaction, reservekey, error, txid)) + throw JSONRPCError(error.first, error.second); + + UniValue result(UniValue::VARR); + result.push_back(txid); + return result; +} UniValue listassetbalancesbyaddress(const JSONRPCRequest& request) { @@ -745,6 +907,7 @@ static const CRPCCommand commands[] = { // category name actor (function) argNames // ----------- ------------------------ ----------------------- ---------- { "assets", "issue", &issue, {"asset_name","qty","to_address","change_address","units","reissuable","has_ipfs","ipfs_hash"} }, + { "assets", "issueunique", &issueunique, {"root_name", "asset_tags", "ipfs_hashes", "to_address", "change_address"}}, { "assets", "listassetbalancesbyaddress", &listassetbalancesbyaddress, {"address"} }, { "assets", "getassetdata", &getassetdata, {"asset_name"}}, { "assets", "listmyassets", &listmyassets, {"asset", "verbose", "count", "start"}}, diff --git a/src/rpc/client.cpp b/src/rpc/client.cpp index 902da8acf2..5db8f5cc05 100644 --- a/src/rpc/client.cpp +++ b/src/rpc/client.cpp @@ -33,6 +33,8 @@ static const CRPCConvertParam vRPCConvertParams[] = { "issue", 4, "units" }, { "issue", 5, "reissuable" }, { "issue", 6, "has_ipfs" }, + { "issueunique", 1, "asset_tags"}, + { "issueunique", 2, "ipfs_hashes"}, { "transfer", 1, "qty"}, { "reissue", 1, "qty"}, { "reissue", 4, "reissuable"}, diff --git a/src/rpc/rawtransaction.cpp b/src/rpc/rawtransaction.cpp index d1c97dccaa..3640987017 100644 --- a/src/rpc/rawtransaction.cpp +++ b/src/rpc/rawtransaction.cpp @@ -356,17 +356,19 @@ UniValue createrawtransaction(const JSONRPCRequest& request) " Some operations require an amount of RVN to be sent to a burn address:\n" " transfer: 0\n" " issue: 500 to Issue Burn Address\n" + " issue_unique 5 to Issue Unique Burn Address\n" " reissue: 100 to Reissue Burn Address\n" "\nOwnership:\n" " These operations require an ownership token input for the asset being operated upon:\n" + " issue_unique\n" " reissue\n" "\nOutput Ordering:\n" " Asset operations require the following:\n" " 1) All coin outputs come first (including the burn output).\n" " 2) The owner token change output comes next (if required).\n" - " 3) An issue, reissue or any number of transfers comes last\n" + " 3) An issue, issue_unique, reissue or any number of transfers comes last\n" " (different types can't be mixed in a single transaction).\n" "\nArguments:\n" @@ -405,6 +407,15 @@ UniValue createrawtransaction(const JSONRPCRequest& request) " }\n" " }\n" " or\n" + " { (object) A json object describing new unique assets to issue\n" + " \"issue_unique\":\n" + " {\n" + " \"root_name\":\"root-name\", (string, required) name of the asset the unique asset(s) are being issued under\n" + " \"asset_tags\":[\"asset_tag\", ...], (array, required) the unique tag for each asset which is to be issued\n" + " \"ipfs_hashes\":[\"hash\", ...], (array, optional) ipfs hashes corresponding to each supplied tag (should be same size as \"asset_tags\")\n" + " }\n" + " }\n" + " or\n" " { (object) A json object describing follow-on asset issue. Requires matching ownership input.\n" " \"reissue\":\n" " {\n" @@ -429,6 +440,7 @@ UniValue createrawtransaction(const JSONRPCRequest& request) + HelpExampleCli("createrawtransaction", "\"[{\\\"txid\\\":\\\"mycoin\\\",\\\"vout\\\":0}]\" \"{\\\"address\\\":0.01}\"") + HelpExampleCli("createrawtransaction", "\"[{\\\"txid\\\":\\\"mycoin\\\",\\\"vout\\\":0}]\" \"{\\\"data\\\":\\\"00010203\\\"}\"") + HelpExampleCli("createrawtransaction", "\"[{\\\"txid\\\":\\\"mycoin\\\",\\\"vout\\\":0}]\" \"{\\\"RXissueAssetXXXXXXXXXXXXXXXXXhhZGt\\\":500,\\\"change_address\\\":change_amount,\\\"issuer_address\\\":{\\\"issue\\\":{\\\"asset_name\\\":\\\"MYASSET\\\",\\\"asset_quantity\\\":1000000,\\\"units\\\":1,\\\"reissuable\\\":0,\\\"has_ipfs\\\":1,\\\"ipfs_hash\\\":\\\"43f81c6f2c0593bde5a85e09ae662816eca80797\\\"}}}\"") + + HelpExampleCli("createrawtransaction", "\"[{\\\"txid\\\":\\\"mycoin\\\",\\\"vout\\\":0}]\" \"{\\\"RXissueUniqueAssetXXXXXXXXXXWEAe58\\\":20,\\\"change_address\\\":change_amount,\\\"issuer_address\\\":{\\\"issue_unique\\\":{\\\"root_name\\\":\\\"MYASSET\\\",\\\"asset_tags\\\":[\\\"ALPHA\\\",\\\"BETA\\\"],\\\"ipfs_hashes\\\":[\\\"43f81c6f2c0593bde5a85e09ae662816eca80797\\\",\\\"43f81c6f2c0593bde5a85e09ae662816eca80797\\\"]}}}\"") + HelpExampleCli("createrawtransaction", "\"[{\\\"txid\\\":\\\"mycoin\\\",\\\"vout\\\":0},{\\\"txid\\\":\\\"myasset\\\",\\\"vout\\\":0}]\" \"{\\\"address\\\":{\\\"transfer\\\":{\\\"MYASSET\\\":50}}}\"") + HelpExampleCli("createrawtransaction", "\"[{\\\"txid\\\":\\\"mycoin\\\",\\\"vout\\\":0},{\\\"txid\\\":\\\"myownership\\\",\\\"vout\\\":0}]\" \"{\\\"issuer_address\\\":{\\\"reissue\\\":{\\\"asset_name\\\":\\\"MYASSET\\\",\\\"asset_quantity\\\":2000000}}}\"") + HelpExampleRpc("createrawtransaction", "\"[{\\\"txid\\\":\\\"mycoin\\\",\\\"vout\\\":0}]\", \"{\\\"data\\\":\\\"00010203\\\"}\"") @@ -523,7 +535,8 @@ UniValue createrawtransaction(const JSONRPCRequest& request) auto asset_ = sendTo[name_].get_obj(); auto assetKey_ = asset_.getKeys()[0]; - if (assetKey_ == "issue") { + if (assetKey_ == "issue") + { if (asset_[0].type() != UniValue::VOBJ) throw JSONRPCError(RPC_INVALID_PARAMETER, std::string("Invalid parameter, the format must follow { \"issue\": {\"key\": value}, ...}")); @@ -562,7 +575,7 @@ UniValue createrawtransaction(const JSONRPCRequest& request) CAmount nAmount = AmountFromValue(asset_quantity); // Create a new asset - CNewAsset asset(asset_name.get_str(), nAmount, units.get_int(), reissuable.get_int(), has_ipfs.get_int(), ipfs_hash.get_str()); + CNewAsset asset(asset_name.get_str(), nAmount, units.get_int(), reissuable.get_int(), has_ipfs.get_int(), DecodeIPFS(ipfs_hash.get_str())); // Verify that data std::string strError = ""; @@ -582,7 +595,74 @@ UniValue createrawtransaction(const JSONRPCRequest& request) CTxOut out(0, scriptPubKey); rawTx.vout.push_back(out); - } else if (assetKey_ == "reissue") { + } + else if (assetKey_ == "issue_unique") + { + + if (asset_[0].type() != UniValue::VOBJ) + throw JSONRPCError(RPC_INVALID_PARAMETER, std::string("Invalid parameter, the format must follow { \"issue_unique\": {\"root_name\": value}, ...}")); + + // Get the asset data object from the json + auto assetData = asset_.getValues()[0].get_obj(); + + /**-------Process the assets data-------**/ + const UniValue& root_name = find_value(assetData, "root_name"); + if (!root_name.isStr()) + throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid parameter, missing asset data for key: root_name"); + + const UniValue& asset_tags = find_value(assetData, "asset_tags"); + if (!asset_tags.isArray() || asset_tags.size() < 1) + throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid parameter, missing asset data for key: asset_tags"); + + const UniValue& ipfs_hashes = find_value(assetData, "ipfs_hashes"); + if (!ipfs_hashes.isNull()) { + if (!ipfs_hashes.isArray() || ipfs_hashes.size() != asset_tags.size()) { + if (!ipfs_hashes.isNum()) + throw JSONRPCError(RPC_INVALID_PARAMETER, + "Invalid parameter, missing asset metadata for key: units"); + } + } + + // Create the scripts for the change of the ownership token + CScript scriptTransferOwnerAsset = GetScriptForDestination(destination); + CAssetTransfer assetTransfer(root_name.get_str() + OWNER_TAG, OWNER_ASSET_AMOUNT); + assetTransfer.ConstructTransaction(scriptTransferOwnerAsset); + + // Create the CTxOut for the owner token + CTxOut out(0, scriptTransferOwnerAsset); + rawTx.vout.push_back(out); + + // Create the assets + for (int i = 0; i < asset_tags.size(); i++) { + + // Create a new asset + CNewAsset asset; + if (ipfs_hashes.isNull()) { + asset = CNewAsset(GetUniqueAssetName(root_name.get_str(), asset_tags[i].get_str()), + UNIQUE_ASSET_AMOUNT, UNIQUE_ASSET_UNITS, UNIQUE_ASSETS_REISSUABLE, 0, ""); + } else { + asset = CNewAsset(GetUniqueAssetName(root_name.get_str(), asset_tags[i].get_str()), + UNIQUE_ASSET_AMOUNT, UNIQUE_ASSET_UNITS, UNIQUE_ASSETS_REISSUABLE, + 1, DecodeIPFS(ipfs_hashes[i].get_str())); + } + + // Verify that data + std::string strError = ""; + if (!asset.IsValid(strError, *passets)) + throw JSONRPCError(RPC_INVALID_PARAMETER, strError); + + // Construct the asset transaction + scriptPubKey = GetScriptForDestination(destination); + asset.ConstructTransaction(scriptPubKey); + + // Push the scriptPubKey into the vouts. + CTxOut out(0, scriptPubKey); + rawTx.vout.push_back(out); + + } + } + else if (assetKey_ == "reissue") + { if (asset_[0].type() != UniValue::VOBJ) throw JSONRPCError(RPC_INVALID_PARAMETER, std::string("Invalid parameter, the format must follow { \"reissue\": {\"key\": value}, ...}")); @@ -619,7 +699,7 @@ UniValue createrawtransaction(const JSONRPCRequest& request) if (!ipfs_hash.isStr()) throw JSONRPCError(RPC_INVALID_PARAMETER, "Invalid parameter, missing reissue metadata for key: ipfs_hash"); - reissueObj.strIPFSHash = ipfs_hash.get_str(); + reissueObj.strIPFSHash = DecodeIPFS(ipfs_hash.get_str()); } // Add the received data into the reissue object diff --git a/src/validation.cpp b/src/validation.cpp index 90736ac227..4a35a6e0b7 100644 --- a/src/validation.cpp +++ b/src/validation.cpp @@ -1733,6 +1733,27 @@ static DisconnectResult DisconnectBlock(const CBlock& block, const CBlockIndex* return DISCONNECT_FAILED; } } + } else if (tx.IsNewUniqueAsset()) { + for (int n = 0; n < tx.vout.size(); n++) { + auto out = tx.vout[n]; + CNewAsset asset; + std::string strAddress; + + if (IsScriptNewUniqueAsset(out.scriptPubKey)) { + if (!AssetFromScript(out.scriptPubKey, asset, strAddress)) { + error("%s : Failed to get unique asset from transaction. TXID : %s, vout: %s", __func__, + tx.GetHash().GetHex(), n); + return DISCONNECT_FAILED; + } + + if (assetsCache->ContainsAsset(asset.strName)) { + if (!assetsCache->RemoveNewAsset(asset, strAddress)) { + error("%s : Failed to Undo Unique Asset. Asset Name : %s", __func__, asset.strName); + return DISCONNECT_FAILED; + } + } + } + } } for (auto index : vAssetTxIndex) { @@ -2196,7 +2217,8 @@ static bool ConnectBlock(const CBlock& block, CValidationState& state, CBlockInd /** RVN START */ if (assetsCache) { - if (tx.IsNewAsset()) { + if (tx.IsNewAsset()) + { if (!AreAssetsDeployed()) return state.DoS(100, false, REJECT_INVALID, "bad-txns-new-asset-when-assets-is-not-active"); @@ -2215,7 +2237,9 @@ static bool ConnectBlock(const CBlock& block, CValidationState& state, CBlockInd if (!asset.IsValid(strError, *assetsCache)) return state.DoS(100, error("%s: %s", __func__, strError), REJECT_INVALID, "bad-txns-issue-asset"); - } else if (tx.IsReissueAsset()) { + } + else if (tx.IsReissueAsset()) + { if (!AreAssetsDeployed()) return state.DoS(100, false, REJECT_INVALID, "bad-txns-reissue-asset-when-assets-is-not-active"); @@ -2231,6 +2255,29 @@ static bool ConnectBlock(const CBlock& block, CValidationState& state, CBlockInd if (!reissue.IsValid(strError, *assetsCache)) return state.DoS(100, false, REJECT_INVALID, strError); } + else if (tx.IsNewUniqueAsset()) + { + if (!AreAssetsDeployed()) + return state.DoS(100, false, REJECT_INVALID, "bad-txns-issue-unique-asset-when-assets-is-not-active"); + + if (!tx.VerifyNewUniqueAsset(view)) + return state.DoS(100, false, REJECT_INVALID, "bad-txns-issue-unique-asset-failed-verify"); + + for (auto out : tx.vout) + { + if (IsScriptNewUniqueAsset(out.scriptPubKey)) + { + CNewAsset asset; + std::string strAddress; + if (!AssetFromScript(out.scriptPubKey, asset, strAddress)) + return state.DoS(100, false, REJECT_INVALID, "bad-txns-issue-unique-asset-serialization"); + + std::string strError = ""; + if (!asset.IsValid(strError, *assetsCache)) + return state.DoS(100, false, REJECT_INVALID, strError); + } + } + } } /** RVN END */ if (fAddressIndex) { @@ -3548,6 +3595,18 @@ static bool ContextualCheckBlock(const CBlock& block, CValidationState& state, c if (!ReissueAssetFromTransaction(*tx, reissue, strAddress)) return state.DoS(100, false, REJECT_INVALID, "bad-txns-reissue-asset"); } + + if (tx->IsNewUniqueAsset()) { + for (auto out : tx->vout) { + CNewAsset asset; + std::string strAddress; + + if (IsScriptNewUniqueAsset(out.scriptPubKey)) { + if (!AssetFromScript(out.scriptPubKey, asset, strAddress)) + return state.DoS(100, false, REJECT_INVALID, "bad-txns-issue-unique-asset"); + } + } + } } // Enforce rule that the coinbase starts with serialized block height diff --git a/src/wallet/wallet.cpp b/src/wallet/wallet.cpp index 7a1d431de5..4e74e0c483 100644 --- a/src/wallet/wallet.cpp +++ b/src/wallet/wallet.cpp @@ -2999,11 +2999,11 @@ bool CWallet::FundTransaction(CMutableTransaction& tx, CAmount& nFeeRet, int& nC return true; } -bool CWallet::CreateTransactionWithAsset(const std::vector& vecSend, CWalletTx& wtxNew, CReserveKey& reservekey, CAmount& nFeeRet, int& nChangePosInOut, - std::string& strFailReason, const CCoinControl& coin_control, const CNewAsset& asset, const CTxDestination destination, const AssetType& type, bool sign) +bool CWallet::CreateTransactionWithAssets(const std::vector& vecSend, CWalletTx& wtxNew, CReserveKey& reservekey, CAmount& nFeeRet, int& nChangePosInOut, + std::string& strFailReason, const CCoinControl& coin_control, const std::vector assets, const CTxDestination destination, const AssetType& type, bool sign) { CReissueAsset reissueAsset; - return CreateTransactionAll(vecSend, wtxNew, reservekey, nFeeRet, nChangePosInOut, strFailReason, coin_control, true, asset, destination, false, false, reissueAsset, type, sign); + return CreateTransactionAll(vecSend, wtxNew, reservekey, nFeeRet, nChangePosInOut, strFailReason, coin_control, true, assets, destination, false, false, reissueAsset, type, sign); } bool CWallet::CreateTransactionWithTransferAsset(const std::vector& vecSend, CWalletTx& wtxNew, CReserveKey& reservekey, CAmount& nFeeRet, int& nChangePosInOut, @@ -3035,16 +3035,32 @@ bool CWallet::CreateTransaction(const std::vector& vecSend, CWalletT return CreateTransactionAll(vecSend, wtxNew, reservekey, nFeeRet, nChangePosInOut, strFailReason, coin_control, false, asset, destination, false, false, reissueAsset, assetType, sign); } +bool CWallet::CreateTransactionAll(const std::vector& vecSend, CWalletTx& wtxNew, CReserveKey& reservekey, + CAmount& nFeeRet, int& nChangePosInOut, std::string& strFailReason, + const CCoinControl& coin_control, bool fNewAsset, const CNewAsset& asset, + const CTxDestination destination, bool fTransferAsset, bool fReissueAsset, + const CReissueAsset& reissueAsset, const AssetType& assetType, bool sign) +{ + std::vector assets; + assets.push_back(asset); + return CreateTransactionAll(vecSend, wtxNew, reservekey, nFeeRet, nChangePosInOut, strFailReason, coin_control, + fNewAsset, assets, destination, fTransferAsset, fReissueAsset, reissueAsset, assetType, + sign); +} -bool CWallet::CreateTransactionAll(const std::vector& vecSend, CWalletTx& wtxNew, CReserveKey& reservekey, CAmount& nFeeRet, - int& nChangePosInOut, std::string& strFailReason, const CCoinControl& coin_control, bool fNewAsset, const CNewAsset& asset, const CTxDestination destination, bool fTransferAsset, bool fReissueAsset, const CReissueAsset& reissueAsset, const AssetType& assetType, bool sign) +bool CWallet::CreateTransactionAll(const std::vector& vecSend, CWalletTx& wtxNew, CReserveKey& reservekey, + CAmount& nFeeRet, int& nChangePosInOut, std::string& strFailReason, + const CCoinControl& coin_control, bool fNewAsset, + const std::vector assets, const CTxDestination destination, + bool fTransferAsset, bool fReissueAsset, const CReissueAsset& reissueAsset, + const AssetType& assetType, bool sign) { + /** RVN START */ if (!AreAssetsDeployed() && (fTransferAsset || fNewAsset || fReissueAsset)) return false; - /** RVN START */ - if (fNewAsset && (asset.IsNull() || !IsValidDestination(destination))) + if (fNewAsset && (assets.size() < 1 || !IsValidDestination(destination))) return error("%s : Tried creating a new asset transaction and the asset was null or the destination was invalid", __func__); if ((fNewAsset && fTransferAsset) || (fReissueAsset && fTransferAsset) || (fReissueAsset && fNewAsset)) @@ -3061,7 +3077,7 @@ bool CWallet::CreateTransactionAll(const std::vector& vecSend, CWall for (const auto& recipient : vecSend) { /** RVN START */ - if (fTransferAsset || fReissueAsset || assetType == AssetType::SUB) { + if (fTransferAsset || fReissueAsset || assetType == AssetType::SUB || assetType == AssetType::UNIQUE) { CAssetTransfer assetTransfer; std::string address; if (TransferAssetFromScript(recipient.scriptPubKey, assetTransfer, address)) { @@ -3141,7 +3157,7 @@ bool CWallet::CreateTransactionAll(const std::vector& vecSend, CWall /** RVN START */ std::vector vAvailableCoins; std::map > mapAssetCoins; - if (fTransferAsset || fReissueAsset || assetType == AssetType::SUB) + if (fTransferAsset || fReissueAsset || assetType == AssetType::SUB || assetType == AssetType::UNIQUE) AvailableCoinsWithAssets(vAvailableCoins, mapAssetCoins, true, &coin_control); else AvailableCoins(vAvailableCoins, true, &coin_control); @@ -3315,18 +3331,21 @@ bool CWallet::CreateTransactionAll(const std::vector& vecSend, CWall /** RVN START */ if (AreAssetsDeployed()) { if (fNewAsset) { - // Create the asset transaction and push it back so it is the last CTxOut in the transaction - CScript scriptPubKey = GetScriptForDestination(destination); - CScript ownerScript = GetScriptForDestination(destination); - - asset.ConstructTransaction(scriptPubKey); - asset.ConstructOwnerTransaction(ownerScript); - - CTxOut ownerTxOut(0, ownerScript); - txNew.vout.push_back(ownerTxOut); - - CTxOut newTxOut(0, scriptPubKey); - txNew.vout.push_back(newTxOut); + for (auto asset : assets) { + // Create the owner token output for non-unique assets + if (assetType != AssetType::UNIQUE) { + CScript ownerScript = GetScriptForDestination(destination); + asset.ConstructOwnerTransaction(ownerScript); + CTxOut ownerTxOut(0, ownerScript); + txNew.vout.push_back(ownerTxOut); + } + + // Create the asset transaction and push it back so it is the last CTxOut in the transaction + CScript scriptPubKey = GetScriptForDestination(destination); + asset.ConstructTransaction(scriptPubKey); + CTxOut newTxOut(0, scriptPubKey); + txNew.vout.push_back(newTxOut); + } } else if (fReissueAsset) { // Create the asset transaction and push it back so it is the last CTxOut in the transaction CScript reissueScript = GetScriptForDestination(destination); diff --git a/src/wallet/wallet.h b/src/wallet/wallet.h index 195c3f5d63..fb792a9a64 100644 --- a/src/wallet/wallet.h +++ b/src/wallet/wallet.h @@ -989,8 +989,8 @@ class CWallet final : public CCryptoKeyStore, public CValidationInterface bool SignTransaction(CMutableTransaction& tx); /** RVN START */ - bool CreateTransactionWithAsset(const std::vector& vecSend, CWalletTx& wtxNew, CReserveKey& reservekey, CAmount& nFeeRet, int& nChangePosInOut, - std::string& strFailReason, const CCoinControl& coin_control, const CNewAsset& asset, const CTxDestination dest, const AssetType& assetType, bool sign = true); + bool CreateTransactionWithAssets(const std::vector& vecSend, CWalletTx& wtxNew, CReserveKey& reservekey, CAmount& nFeeRet, int& nChangePosInOut, + std::string& strFailReason, const CCoinControl& coin_control, const std::vector assets, const CTxDestination dest, const AssetType& assetType, bool sign = true); bool CreateTransactionWithTransferAsset(const std::vector& vecSend, CWalletTx& wtxNew, CReserveKey& reservekey, CAmount& nFeeRet, int& nChangePosInOut, std::string& strFailReason, const CCoinControl& coin_control, bool sign = true); @@ -1009,6 +1009,10 @@ class CWallet final : public CCryptoKeyStore, public CValidationInterface bool CreateTransactionAll(const std::vector& vecSend, CWalletTx& wtxNew, CReserveKey& reservekey, CAmount& nFeeRet, int& nChangePosInOut, std::string& strFailReason, const CCoinControl& coin_control, bool fNewAsset, const CNewAsset& asset, const CTxDestination dest, bool fTransferAsset, bool fReissueAsset, const CReissueAsset& reissueAsset, const AssetType& assetType, bool sign = true); + bool CreateTransactionAll(const std::vector& vecSend, CWalletTx& wtxNew, CReserveKey& reservekey, CAmount& nFeeRet, + int& nChangePosInOut, std::string& strFailReason, const CCoinControl& coin_control, bool fNewAsset, const std::vector assets, const CTxDestination destination, bool fTransferAsset, bool fReissueAsset, const CReissueAsset& reissueAsset, const AssetType& assetType, bool sign); + + /** RVN END */ bool CommitTransaction(CWalletTx& wtxNew, CReserveKey& reservekey, CConnman* connman, CValidationState& state); diff --git a/test/functional/assets.py b/test/functional/assets.py index 75b5a99fd4..022f630b71 100755 --- a/test/functional/assets.py +++ b/test/functional/assets.py @@ -10,7 +10,7 @@ from test_framework.util import ( assert_equal, assert_is_hash_string, - assert_raises_rpc_error + assert_raises_rpc_error, ) import string @@ -20,19 +20,19 @@ def set_test_params(self): self.setup_clean_chain = True self.num_nodes = 3 - def run_test(self): - self.log.info("Running test!") - + def activate_assets(self): + self.log.info("Generating RVN for node[0] and activating assets...") n0, n1, n2 = self.nodes[0], self.nodes[1], self.nodes[2] - self.log.info("Generating RVN for node[0] and activating assets...") n0.generate(1) self.sync_all() - n2.generate(431) - self.sync_all() - assert_equal(n0.getbalance(), 5000) n0.generate(431) self.sync_all() + assert_equal("active", n0.getblockchaininfo()['bip9_softforks']['assets']['status']) + + def big_test(self): + self.log.info("Running big test!") + n0, n1, n2 = self.nodes[0], self.nodes[1], self.nodes[2] self.log.info("Calling issue()...") address0 = n0.getnewaddress() @@ -164,8 +164,36 @@ def run_test(self): assert_equal(raven_assets[1], "RAVEN3") self.sync_all() + def issue_param_checks(self): + self.log.info("Checking bad parameter handling!") + n0, n1, n2 = self.nodes[0], self.nodes[1], self.nodes[2] + + # just plain bad asset name + assert_raises_rpc_error(-8, "Invalid asset name: bad-asset-name", \ + n0.issue, "bad-asset-name"); + + # trying to issue things that can't be issued + assert_raises_rpc_error(-8, "Unsupported asset type: OWNER", \ + n0.issue, "AN_OWNER!"); + assert_raises_rpc_error(-8, "Unsupported asset type: MSGCHANNEL", \ + n0.issue, "A_MSGCHANNEL~CHANNEL_4"); + assert_raises_rpc_error(-8, "Unsupported asset type: VOTE", \ + n0.issue, "A_VOTE^PEDRO"); + + # check bad unique params + assert_raises_rpc_error(-8, "Invalid parameters for issuing a unique asset.", \ + n0.issue, "A_UNIQUE#ASSET", 2) + assert_raises_rpc_error(-8, "Invalid parameters for issuing a unique asset.", \ + n0.issue, "A_UNIQUE#ASSET", 1, "", "", 1) + assert_raises_rpc_error(-8, "Invalid parameters for issuing a unique asset.", \ + n0.issue, "A_UNIQUE#ASSET", 1, "", "", 0, True) + + def chain_assets(self): self.log.info("Issuing chained assets in depth issue()...") + n0, n1, n2 = self.nodes[0], self.nodes[1], self.nodes[2] + chain_address = n0.getnewaddress() + ipfs_hash = "QmacSRmrkVmvJfbCpmU6pK72furJ8E8fbKHindrLxmYMQo" chain_string = "CHAIN1" n0.issue(asset_name=chain_string, qty=1000, to_address=chain_address, change_address="", \ units=4, reissuable=True, has_ipfs=True, ipfs_hash=ipfs_hash) @@ -201,7 +229,6 @@ def run_test(self): chain_assets = n1.listassets(asset="CHAIN2/*", verbose=False) assert_equal(len(chain_assets), 26) - self.log.info("Chaining reissue transactions...") address0 = n0.getnewaddress() n0.issue(asset_name="CHAIN_REISSUE", qty=1000, to_address=address0, change_address="", \ @@ -211,7 +238,7 @@ def run_test(self): self.sync_all() n0.reissue(asset_name="CHAIN_REISSUE", qty=1000, to_address=address0, change_address="", \ - reissuable=True) + reissuable=True) assert_raises_rpc_error(-4, "Error: The transaction was rejected! Reason given: bad-tx-reissue-chaining-not-allowed", n0.reissue, "CHAIN_REISSUE", 1000, address0, "", True) n0.generate(1) @@ -230,6 +257,11 @@ def run_test(self): assert_equal(assetdata["reissuable"], 1) assert_equal(assetdata["has_ipfs"], 0) + def run_test(self): + self.activate_assets(); + self.big_test(); + self.issue_param_checks(); + self.chain_assets(); if __name__ == '__main__': AssetTest().main() diff --git a/test/functional/rawassettransactions.py b/test/functional/rawassettransactions.py index ce9423ff07..6c40fdf70a 100755 --- a/test/functional/rawassettransactions.py +++ b/test/functional/rawassettransactions.py @@ -6,7 +6,7 @@ """Test the rawtransaction RPCs for asset transactions. """ from io import BytesIO - +from pprint import * from test_framework.test_framework import RavenTestFramework from test_framework.util import * from test_framework.mininode import CTransaction, CScriptTransfer @@ -14,19 +14,27 @@ class RawAssetTransactionsTest(RavenTestFramework): def set_test_params(self): self.setup_clean_chain = True - self.num_nodes = 2 + self.num_nodes = 3 - def issue_reissue_transfer_test(self): - ######################################## - # activate assets - self.nodes[0].generate(500) + def activate_assets(self): + self.log.info("Generating RVN for node[0] and activating assets...") + n0, n1, n2 = self.nodes[0], self.nodes[1], self.nodes[2] + + n0.generate(1) self.sync_all() + n0.generate(431) + self.sync_all() + assert_equal("active", n0.getblockchaininfo()['bip9_softforks']['assets']['status']) + + def issue_reissue_transfer_test(self): + self.log.info("Doing a big issue-reissue-transfer test...") + n0, n1, n2 = self.nodes[0], self.nodes[1], self.nodes[2] ######################################## # issue - to_address = self.nodes[0].getnewaddress() - change_address = self.nodes[0].getnewaddress() - unspent = self.nodes[0].listunspent()[0] + to_address = n0.getnewaddress() + change_address = n0.getnewaddress() + unspent = n0.listunspent()[0] inputs = [{k: unspent[k] for k in ['txid', 'vout']}] outputs = { 'n1issueAssetXXXXXXXXXXXXXXXXWdnemQ': 500, @@ -41,21 +49,21 @@ def issue_reissue_transfer_test(self): } } } - tx_issue = self.nodes[0].createrawtransaction(inputs, outputs) - tx_issue_signed = self.nodes[0].signrawtransaction(tx_issue) - tx_issue_hash = self.nodes[0].sendrawtransaction(tx_issue_signed['hex']) + tx_issue = n0.createrawtransaction(inputs, outputs) + tx_issue_signed = n0.signrawtransaction(tx_issue) + tx_issue_hash = n0.sendrawtransaction(tx_issue_signed['hex']) assert_is_hash_string(tx_issue_hash) self.log.info("issue tx: " + tx_issue_hash) - self.nodes[0].generate(1) + n0.generate(1) self.sync_all() - assert_equal(1000, self.nodes[0].listmyassets('TEST_ASSET')['TEST_ASSET']) - assert_equal(1, self.nodes[0].listmyassets('TEST_ASSET!')['TEST_ASSET!']) + assert_equal(1000, n0.listmyassets('TEST_ASSET')['TEST_ASSET']) + assert_equal(1, n0.listmyassets('TEST_ASSET!')['TEST_ASSET!']) ######################################## # reissue - unspent = self.nodes[0].listunspent()[0] - unspent_asset_owner = self.nodes[0].listmyassets('TEST_ASSET!', True)['TEST_ASSET!']['outpoints'][0] + unspent = n0.listunspent()[0] + unspent_asset_owner = n0.listmyassets('TEST_ASSET!', True)['TEST_ASSET!']['outpoints'][0] inputs = [ {k: unspent[k] for k in ['txid', 'vout']}, @@ -73,24 +81,24 @@ def issue_reissue_transfer_test(self): } } - tx_reissue = self.nodes[0].createrawtransaction(inputs, outputs) - tx_reissue_signed = self.nodes[0].signrawtransaction(tx_reissue) - tx_reissue_hash = self.nodes[0].sendrawtransaction(tx_reissue_signed['hex']) + tx_reissue = n0.createrawtransaction(inputs, outputs) + tx_reissue_signed = n0.signrawtransaction(tx_reissue) + tx_reissue_hash = n0.sendrawtransaction(tx_reissue_signed['hex']) assert_is_hash_string(tx_reissue_hash) self.log.info("reissue tx: " + tx_reissue_hash) - self.nodes[0].generate(1) + n0.generate(1) self.sync_all() - assert_equal(2000, self.nodes[0].listmyassets('TEST_ASSET')['TEST_ASSET']) - assert_equal(1, self.nodes[0].listmyassets('TEST_ASSET!')['TEST_ASSET!']) + assert_equal(2000, n0.listmyassets('TEST_ASSET')['TEST_ASSET']) + assert_equal(1, n0.listmyassets('TEST_ASSET!')['TEST_ASSET!']) self.sync_all() ######################################## # transfer - remote_to_address = self.nodes[1].getnewaddress() - unspent = self.nodes[0].listunspent()[0] - unspent_asset = self.nodes[0].listmyassets('TEST_ASSET', True)['TEST_ASSET']['outpoints'][0] + remote_to_address = n1.getnewaddress() + unspent = n0.listunspent()[0] + unspent_asset = n0.listmyassets('TEST_ASSET', True)['TEST_ASSET']['outpoints'][0] inputs = [ {k: unspent[k] for k in ['txid', 'vout']}, {k: unspent_asset[k] for k in ['txid', 'vout']}, @@ -108,8 +116,8 @@ def issue_reissue_transfer_test(self): } } } - tx_transfer = self.nodes[0].createrawtransaction(inputs, outputs) - tx_transfer_signed = self.nodes[0].signrawtransaction(tx_transfer) + tx_transfer = n0.createrawtransaction(inputs, outputs) + tx_transfer_signed = n0.signrawtransaction(tx_transfer) tx_hex = tx_transfer_signed['hex'] ######################################## @@ -122,7 +130,7 @@ def issue_reissue_transfer_test(self): tx.vin[1].scriptSig = hex_str_to_bytes(tampered_sig) tampered_hex = bytes_to_hex_str(tx.serialize()) assert_raises_rpc_error(-26, "mandatory-script-verify-flag-failed (Script failed an OP_EQUALVERIFY operation)", - self.nodes[0].sendrawtransaction, tampered_hex) + n0.sendrawtransaction, tampered_hex) ######################################## # try tampering with the asset script @@ -145,7 +153,7 @@ def issue_reissue_transfer_test(self): t2.deserialize(BytesIO(hex_str_to_bytes(tampered_transfer))) tampered_hex = bytes_to_hex_str(tx.serialize()) assert_raises_rpc_error(-26, "mandatory-script-verify-flag-failed (Signature must be zero for failed CHECK(MULTI)SIG operation)", - self.nodes[0].sendrawtransaction, tampered_hex) + n0.sendrawtransaction, tampered_hex) ######################################## # try tampering with asset outs so ins and outs don't add up @@ -163,11 +171,11 @@ def issue_reissue_transfer_test(self): } } } - tx_bad_transfer = self.nodes[0].createrawtransaction(inputs, bad_outputs) - tx_bad_transfer_signed = self.nodes[0].signrawtransaction(tx_bad_transfer) + tx_bad_transfer = n0.createrawtransaction(inputs, bad_outputs) + tx_bad_transfer_signed = n0.signrawtransaction(tx_bad_transfer) tx_bad_hex = tx_bad_transfer_signed['hex'] - assert_raises_rpc_error(-26, "bad-tx-asset-inputs-amount-mismatch-with-outputs-amount", - self.nodes[0].sendrawtransaction, tx_bad_hex) + assert_raises_rpc_error(-26, "bad-tx-inputs-outputs-mismatch Bad Transaction - Assets would be burnt TEST_ASSET", + n0.sendrawtransaction, tx_bad_hex) ######################################## # try tampering with asset outs so they don't use proper units @@ -185,27 +193,79 @@ def issue_reissue_transfer_test(self): } } } - tx_bad_transfer = self.nodes[0].createrawtransaction(inputs, bad_outputs) - tx_bad_transfer_signed = self.nodes[0].signrawtransaction(tx_bad_transfer) + tx_bad_transfer = n0.createrawtransaction(inputs, bad_outputs) + tx_bad_transfer_signed = n0.signrawtransaction(tx_bad_transfer) tx_bad_hex = tx_bad_transfer_signed['hex'] assert_raises_rpc_error(-26, "bad-txns-transfer-asset-amount-not-match-units", - self.nodes[0].sendrawtransaction, tx_bad_hex) + n0.sendrawtransaction, tx_bad_hex) ######################################## # send the good transfer - tx_transfer_hash = self.nodes[0].sendrawtransaction(tx_hex) + tx_transfer_hash = n0.sendrawtransaction(tx_hex) assert_is_hash_string(tx_transfer_hash) self.log.info("transfer tx: " + tx_transfer_hash) - self.nodes[0].generate(1) + n0.generate(1) + self.sync_all() + assert_equal(1600, n0.listmyassets('TEST_ASSET')['TEST_ASSET']) + assert_equal(1, n0.listmyassets('TEST_ASSET!')['TEST_ASSET!']) + assert_equal(400, n1.listmyassets('TEST_ASSET')['TEST_ASSET']) + + + def unique_assets_test(self): + self.log.info("Testing unique assets...") + n0, n1, n2 = self.nodes[0], self.nodes[1], self.nodes[2] + + root = "RINGU" + owner = f"{root}!" + n0.issue(root) + n0.generate(1) + self.sync_all() + + asset_tags = ["myprecious1", "bind3", "gold7", "men9"] + ipfs_hashes = ["QmWWQSuPMS6aXCbZKpEjPHPUZN2NjB3YrhJTHsV4X3vb2t"] * len(asset_tags) + + to_address = n0.getnewaddress() + change_address = n0.getnewaddress() + unspent = n0.listunspent()[0] + unspent_asset_owner = n0.listmyassets(owner, True)[owner]['outpoints'][0] + + inputs = [ + {k: unspent[k] for k in ['txid', 'vout']}, + {k: unspent_asset_owner[k] for k in ['txid', 'vout']}, + ] + + burn = 5 * len(asset_tags) + outputs = { + 'n1issueUniqueAssetXXXXXXXXXXS4695i': burn, + change_address: float(unspent['amount']) - (burn + 0.0001), + to_address: { + 'issue_unique': { + 'root_name': root, + 'asset_tags': asset_tags, + 'ipfs_hashes': ipfs_hashes, + } + } + } + + hex = n0.createrawtransaction(inputs, outputs) + signed_hex = n0.signrawtransaction(hex)['hex'] + tx_hash = n0.sendrawtransaction(signed_hex) + n0.generate(1) self.sync_all() - assert_equal(1600, self.nodes[0].listmyassets('TEST_ASSET')['TEST_ASSET']) - assert_equal(1, self.nodes[0].listmyassets('TEST_ASSET!')['TEST_ASSET!']) - assert_equal(400, self.nodes[1].listmyassets('TEST_ASSET')['TEST_ASSET']) + + for tag in asset_tags: + asset_name = f"{root}#{tag}" + assert_equal(1, n0.listmyassets()[asset_name]) + assert_equal(1, n0.listassets(asset_name, True)[asset_name]['has_ipfs']) + assert_equal(ipfs_hashes[0], n0.listassets(asset_name, True)[asset_name]['ipfs_hash']) def run_test(self): + self.activate_assets() self.issue_reissue_transfer_test() + self.unique_assets_test() + if __name__ == '__main__': RawAssetTransactionsTest().main() diff --git a/test/functional/test_framework/util.py b/test/functional/test_framework/util.py index 332de618ca..a80ef35df7 100644 --- a/test/functional/test_framework/util.py +++ b/test/functional/test_framework/util.py @@ -26,10 +26,22 @@ # Assert functions ################## +def assert_contains(val, arr): + if not (val in arr): + raise AssertionError("val %s not in arr" % (val)) + +def assert_does_not_contain(val, arr): + if (val in arr): + raise AssertionError("val %s is in arr" % (val)) + def assert_contains_pair(key, val, dict): if not (key in dict and val == dict[key]): raise AssertionError("k/v pair (%s,%s) not in dict" % (key, val)) +def assert_does_not_contain_key(key, dict): + if (key in dict): + raise AssertionError("key %s is in dict" % (key)) + def assert_fee_amount(fee, tx_size, fee_per_kB): """Assert the fee was in range""" target_fee = tx_size * fee_per_kB / 1000 diff --git a/test/functional/unique_assets.py b/test/functional/unique_assets.py new file mode 100755 index 0000000000..0ca51094cd --- /dev/null +++ b/test/functional/unique_assets.py @@ -0,0 +1,148 @@ +#!/usr/bin/env python3 +# Copyright (c) 2017 The Bitcoin Core developers +# Copyright (c) 2017 The Raven Core developers +# Distributed under the MIT software license, see the accompanying +# file COPYING or http://www.opensource.org/licenses/mit-license.php. +"""Testing unique asset use cases + +""" +import random + +from test_framework.test_framework import RavenTestFramework +from test_framework.util import ( + assert_contains, + assert_does_not_contain_key, + assert_equal, + assert_raises_rpc_error, +) + + +def gen_root_asset_name(): + size = random.randint(3, 14) + name = "" + for _ in range(1, size+1): + ch = random.randint(65, 65+25) + name += chr(ch) + return name + +def gen_unique_asset_name(root): + tag_ab = "-ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789@$%&*()[]{}<>_.;?\\:" + name = root + "#" + tag_size = random.randint(1, 15) + for _ in range(1, tag_size+1): + tag_c = tag_ab[random.randint(0, len(tag_ab) - 1)] + name += tag_c + return name + +class UniqueAssetTest(RavenTestFramework): + def set_test_params(self): + self.setup_clean_chain = True + self.num_nodes = 3 + + def activate_assets(self): + self.log.info("Generating RVN for node[0] and activating assets...") + n0 = self.nodes[0] + n0.generate(432) + self.sync_all() + assert_equal("active", n0.getblockchaininfo()['bip9_softforks']['assets']['status']) + + def issue_one(self): + self.log.info("Issuing a unique asset...") + n0 = self.nodes[0] + root = gen_root_asset_name() + n0.issue(asset_name=root) + n0.generate(1) + asset_name = gen_unique_asset_name(root) + tx_hash = n0.issue(asset_name=asset_name) + n0.generate(1) + assert_equal(1, n0.listmyassets()[asset_name]) + + def issue_invalid(self): + self.log.info("Trying some invalid calls...") + n0, n1 = self.nodes[0], self.nodes[1] + n1.generate(10) + self.sync_all() + + root = gen_root_asset_name() + asset_name = gen_unique_asset_name(root) + + # no root + assert_raises_rpc_error(-32600, f"Wallet doesn't have asset: {root}!", n0.issue, asset_name) + + # don't own root + n0.sendtoaddress(n1.getnewaddress(), 501) + n0.generate(1) + self.sync_all() + n1.issue(root) + n1.generate(1) + self.sync_all() + assert_contains(root, n0.listassets()) + assert_raises_rpc_error(-32600, f"Wallet doesn't have asset: {root}!", n0.issue, asset_name) + n1.transfer(f"{root}!", 1, n0.getnewaddress()) + n1.generate(1) + self.sync_all() + + # bad qty + assert_raises_rpc_error(-8, "Invalid parameters for issuing a unique asset.", n0.issue, asset_name, 2) + + # bad units + assert_raises_rpc_error(-8, "Invalid parameters for issuing a unique asset.", n0.issue, asset_name, 1, "", "", 1) + + # reissuable + assert_raises_rpc_error(-8, "Invalid parameters for issuing a unique asset.", n0.issue, asset_name, 1, "", "", 0, True) + + # already exists + n0.issue(asset_name) + n0.generate(1) + self.sync_all() + assert_raises_rpc_error(-8, f"Invalid parameter: asset_name '{asset_name}' has already been used", n0.issue, asset_name) + + + def issueunique_test(self): + self.log.info("Testing issueunique RPC...") + n0, n1 = self.nodes[0], self.nodes[1] + n0.sendtoaddress(n1.getnewaddress(), 501) + + root = gen_root_asset_name() + n0.issue(asset_name=root) + asset_tags = ["first", "second"] + ipfs_hashes = ["QmWWQSuPMS6aXCbZKpEjPHPUZN2NjB3YrhJTHsV4X3vb2t"] * len(asset_tags) + n0.issueunique(root, asset_tags, ipfs_hashes) + block_hash = n0.generate(1)[0] + + for tag in asset_tags: + asset_name = f"{root}#{tag}" + assert_equal(1, n0.listmyassets()[asset_name]) + assert_equal(1, n0.listassets(asset_name, True)[asset_name]['has_ipfs']) + assert_equal(ipfs_hashes[0], n0.listassets(asset_name, True)[asset_name]['ipfs_hash']) + + # invalidate + n0.invalidateblock(block_hash) + assert_does_not_contain_key(root, n0.listmyassets()) + assert_does_not_contain_key(asset_name, n0.listmyassets()) + + # reconsider + n0.reconsiderblock(block_hash) + assert_contains(root, n0.listmyassets()) + assert_contains(asset_name, n0.listmyassets()) + + # root doesn't exist + missing_asset = "VAPOUR" + assert_raises_rpc_error(-32600, f"Wallet doesn't have asset: {missing_asset}!", n0.issueunique, missing_asset, asset_tags) + + # don't own root + n1.issue(missing_asset) + n1.generate(1) + self.sync_all() + assert_contains(missing_asset, n0.listassets()) + assert_raises_rpc_error(-32600, f"Wallet doesn't have asset: {missing_asset}!", n0.issueunique, missing_asset, asset_tags) + + def run_test(self): + self.activate_assets() + self.issueunique_test() + self.issue_one() + self.issue_invalid() + + +if __name__ == '__main__': + UniqueAssetTest().main()