diff --git a/node-it/src/test/scala/com/wavesplatform/it/sync/transactions/IssueNFTSuite.scala b/node-it/src/test/scala/com/wavesplatform/it/sync/transactions/IssueNFTSuite.scala new file mode 100644 index 00000000000..2f6ea70ec3c --- /dev/null +++ b/node-it/src/test/scala/com/wavesplatform/it/sync/transactions/IssueNFTSuite.scala @@ -0,0 +1,165 @@ +package com.wavesplatform.it.sync.transactions + +import com.typesafe.config.Config +import com.wavesplatform.account.KeyPair +import com.wavesplatform.common.utils._ +import com.wavesplatform.it.api.SyncHttpApi._ +import com.wavesplatform.it.transactions.BaseTransactionSuite +import com.wavesplatform.it.util._ +import com.wavesplatform.it.{Node, NodeConfigs} +import com.wavesplatform.transaction.Asset.Waves +import com.wavesplatform.transaction.assets.IssueTransactionV2 +import com.wavesplatform.transaction.transfer.TransferTransactionV1 +import org.scalatest.prop.TableDrivenPropertyChecks + +class IssueNFTSuite extends BaseTransactionSuite with TableDrivenPropertyChecks { + + val firstNode: Node = nodes.head + val secondNode: Node = nodes.last + + val secondNodeIssuer = KeyPair("second_node_issuer".getBytes()) + val firstNodeIssuer = KeyPair("first_node_issuer".getBytes()) + + override def nodeConfigs: Seq[Config] = + NodeConfigs.newBuilder + .overrideBase(_.quorum(0)) + .withDefault(1) + .withSpecial(_.raw(s""" + |waves.blockchain.custom.functionality.pre-activated-features = { + | 2 = 0 + | 3 = 0 + | 4 = 0 + | 5 = 0 + | 6 = 0 + | 7 = 0 + | 9 = 0 + | 10 = 0 + | 11 = 0 + | 12 = 0 + | 13 = 0 + |} + """.stripMargin)) + .buildNonConflicting() + + test("Can't issue NFT before activation") { + val assetName = "NFTAsset" + val assetDescription = "my asset description" + + firstNode.transfer( + firstNode.privateKey.address, + firstNodeIssuer.address, + 10.waves, + 0.001.waves, + waitForTx = true + ) + + assertBadRequest( + firstNode.issue(firstAddress, assetName, assetDescription, 1, 0, reissuable = false, 1.waves / 1000, waitForTx = true) + ) + } + + test("Able to issue NFT token with reduced fee") { + val assetName = "NFTAsset" + val assetDescription = "my asset description" + + val ttx = TransferTransactionV1 + .selfSigned( + Waves, + secondNode.privateKey, + secondNodeIssuer, + 10.waves, + System.currentTimeMillis(), + Waves, + 0.001.waves, + Array.emptyByteArray + ) + .explicitGet() + + secondNode.signedBroadcast(ttx.json(), waitForTx = true) + + val itx = IssueTransactionV2 + .selfSigned( + 'I', + secondNodeIssuer, + assetName.getBytes(), + assetDescription.getBytes(), + 1, + 0, + false, + None, + 0.001.waves, + System.currentTimeMillis() + ) + .explicitGet() + + secondNode.signedBroadcast(itx.json(), waitForTx = true) + + secondNode.assertAssetBalance(secondNodeIssuer.address, itx.assetId().base58, 1L) + } + + test("Can't issue reissuable NFT") { + val assetName = "NFTAsset" + val assetDescription = "my asset description" + + val itx = IssueTransactionV2 + .selfSigned( + 'I', + secondNodeIssuer, + assetName.getBytes(), + assetDescription.getBytes(), + 1, + 0, + true, + None, + 0.001.waves, + System.currentTimeMillis() + ) + .explicitGet() + + assertBadRequestAndResponse(secondNode.signedBroadcast(itx.json(), waitForTx = true), "does not exceed minimal value") + } + + test("Can't issue NFT with quantity > 1") { + val assetName = "NFTAsset" + val assetDescription = "my asset description" + + val itx = IssueTransactionV2 + .selfSigned( + 'I', + secondNodeIssuer, + assetName.getBytes(), + assetDescription.getBytes(), + 2, + 0, + true, + None, + 0.001.waves, + System.currentTimeMillis() + ) + .explicitGet() + + assertBadRequestAndResponse(secondNode.signedBroadcast(itx.json(), waitForTx = true), "does not exceed minimal value") + } + + test("Can't issue token with reduced fee if decimals > 0") { + val assetName = "NFTAsset" + val assetDescription = "my asset description" + + val itx = IssueTransactionV2 + .selfSigned( + 'I', + secondNodeIssuer, + assetName.getBytes(), + assetDescription.getBytes(), + 1, + 1, + true, + None, + 0.001.waves, + System.currentTimeMillis() + ) + .explicitGet() + + assertBadRequestAndResponse(secondNode.signedBroadcast(itx.json(), waitForTx = true), "does not exceed minimal value") + } +} diff --git a/node/src/main/scala/com/wavesplatform/features/BlockchainFeature.scala b/node/src/main/scala/com/wavesplatform/features/BlockchainFeature.scala index 5b935ef4d1d..5d4ba7ac100 100644 --- a/node/src/main/scala/com/wavesplatform/features/BlockchainFeature.scala +++ b/node/src/main/scala/com/wavesplatform/features/BlockchainFeature.scala @@ -16,6 +16,7 @@ object BlockchainFeatures { val SmartAccountTrading = BlockchainFeature(10, "Smart Account Trading") val Ride4DApps = BlockchainFeature(11, "RIDE 4 DAPPS") val OrderV3 = BlockchainFeature(12, "Order Version 3") + val ReduceNFTFee = BlockchainFeature(13, "Reduce NFT fee") // When next fork-parameter is created, you must replace all uses of the DummyFeature with the new one. val DummyFeature = BlockchainFeature(-1, "Non Votable!") @@ -32,7 +33,8 @@ object BlockchainFeatures { SmartAccountTrading, SmartAssets, Ride4DApps, - OrderV3 + OrderV3, + ReduceNFTFee ).map(f => f.id -> f).toMap val implemented: Set[Short] = dict.keySet diff --git a/node/src/main/scala/com/wavesplatform/state/diffs/CommonValidation.scala b/node/src/main/scala/com/wavesplatform/state/diffs/CommonValidation.scala index f56977ef7a3..8af0ec86f66 100644 --- a/node/src/main/scala/com/wavesplatform/state/diffs/CommonValidation.scala +++ b/node/src/main/scala/com/wavesplatform/state/diffs/CommonValidation.scala @@ -26,6 +26,7 @@ object CommonValidation { val ScriptExtraFee = 400000L val FeeUnit = 100000 + val NFTMultiplier = 0.001 val FeeConstants: Map[Byte, Long] = Map( GenesisTransaction.typeId -> 0, @@ -193,23 +194,26 @@ object CommonValidation { def disallowTxFromFuture[T <: Transaction](settings: FunctionalitySettings, time: Long, tx: T): Either[ValidationError, T] = { val allowTransactionsFromFutureByTimestamp = tx.timestamp < settings.allowTransactionsFromFutureUntil if (!allowTransactionsFromFutureByTimestamp && tx.timestamp - time > settings.maxTransactionTimeForwardOffset.toMillis) - Left(Mistiming(s"""Transaction timestamp ${tx.timestamp} + Left( + Mistiming( + s"""Transaction timestamp ${tx.timestamp} |is more than ${settings.maxTransactionTimeForwardOffset.toMillis}ms in the future |relative to block timestamp $time""".stripMargin - .replaceAll("\n", " ") - .replaceAll("\r", ""))) + .replaceAll("\n", " ") + .replaceAll("\r", ""))) else Right(tx) } def disallowTxFromPast[T <: Transaction](settings: FunctionalitySettings, prevBlockTime: Option[Long], tx: T): Either[ValidationError, T] = prevBlockTime match { case Some(t) if (t - tx.timestamp) > settings.maxTransactionTimeBackOffset.toMillis => - Left(Mistiming(s"""Transaction timestamp ${tx.timestamp} + Left( + Mistiming( + s"""Transaction timestamp ${tx.timestamp} |is more than ${settings.maxTransactionTimeBackOffset.toMillis}ms in the past - |relative to previous block timestamp $prevBlockTime""" - .stripMargin - .replaceAll("\n", " ") - .replaceAll("\r", ""))) + |relative to previous block timestamp $prevBlockTime""".stripMargin + .replaceAll("\n", " ") + .replaceAll("\r", ""))) case _ => Right(tx) } @@ -223,6 +227,14 @@ object CommonValidation { case tx: DataTransaction => val base = if (blockchain.isFeatureActivated(BlockchainFeatures.SmartAccounts, height)) tx.bodyBytes() else tx.bytes() baseFee + (base.length - 1) / 1024 + case itx: IssueTransaction => + lazy val nftActivated = blockchain.activatedFeatures + .get(BlockchainFeatures.ReduceNFTFee.id) + .exists(_ <= height) + + val multiplier = if (itx.isNFT && nftActivated) NFTMultiplier else 1 + + (baseFee * multiplier).toLong case _ => baseFee } } diff --git a/node/src/main/scala/com/wavesplatform/transaction/assets/IssueTransaction.scala b/node/src/main/scala/com/wavesplatform/transaction/assets/IssueTransaction.scala index 3e1d42db8a4..f094589b235 100644 --- a/node/src/main/scala/com/wavesplatform/transaction/assets/IssueTransaction.scala +++ b/node/src/main/scala/com/wavesplatform/transaction/assets/IssueTransaction.scala @@ -25,6 +25,8 @@ trait IssueTransaction extends ProvenTransaction with VersionedTransaction { final lazy val assetId = id override final val assetFee: (Asset, Long) = (Waves, fee) + val isNFT: Boolean = quantity == 1 && decimals == 0 && !reissuable + val issueJson: Coeval[JsObject] = Coeval.evalOnce( jsonBase() ++ Json.obj( "version" -> version,