From 58e4b35c6bea2cef3da803caa5ead8aa5f6d5a2c Mon Sep 17 00:00:00 2001 From: D-H-O-R-A Date: Thu, 22 Oct 2026 23:46:57 -0300 Subject: [PATCH] feat(assets): support up to 18 decimals and full 256-bit BigInt supply for ERC-20 compatibility --- .../api/http/eth/EthRpcRoute.scala | 3 + .../transaction/assets/IssueTransaction.scala | 2 +- .../transaction/HighDecimalsIssueSpec.scala | 116 ++++++++++++++++++ 3 files changed, 120 insertions(+), 1 deletion(-) create mode 100644 node/tests/src/test/scala/com/wavesplatform/transaction/HighDecimalsIssueSpec.scala diff --git a/node/src/main/scala/com/wavesplatform/api/http/eth/EthRpcRoute.scala b/node/src/main/scala/com/wavesplatform/api/http/eth/EthRpcRoute.scala index 06fe65359b..2158a65772 100644 --- a/node/src/main/scala/com/wavesplatform/api/http/eth/EthRpcRoute.scala +++ b/node/src/main/scala/com/wavesplatform/api/http/eth/EthRpcRoute.scala @@ -188,6 +188,9 @@ class EthRpcRoute(blockchain: Blockchain, transactionsApi: CommonTransactionsApi resp(id, encodeResponse(assetDescription(contractAddress).fold("")(_.name.toStringUtf8))) case "313ce567" => // decimals() resp(id, encodeResponse(new Uint8(assetDescription(contractAddress).fold(0)(_.decimals)))) + case "18160ddd" => // totalSupply() + val supply = assetDescription(contractAddress).fold(BigInteger.ZERO)(_.totalVolume.bigInteger) + resp(id, encodeResponse(new Uint256(supply))) case "70a08231" => // balanceOf(address) resp( id, 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 272487ef4c..6be1974584 100644 --- a/node/src/main/scala/com/wavesplatform/transaction/assets/IssueTransaction.scala +++ b/node/src/main/scala/com/wavesplatform/transaction/assets/IssueTransaction.scala @@ -52,7 +52,7 @@ object IssueTransaction extends TransactionParser { val MinAssetNameLength = 4 val MaxAssetNameLength = 16 val MaxAssetDescriptionLength = 1000 - final val MaxAssetDecimals = 8 + final val MaxAssetDecimals = 18 override val typeId: TxType = 3: Byte diff --git a/node/tests/src/test/scala/com/wavesplatform/transaction/HighDecimalsIssueSpec.scala b/node/tests/src/test/scala/com/wavesplatform/transaction/HighDecimalsIssueSpec.scala new file mode 100644 index 0000000000..7819c15265 --- /dev/null +++ b/node/tests/src/test/scala/com/wavesplatform/transaction/HighDecimalsIssueSpec.scala @@ -0,0 +1,116 @@ +package com.wavesplatform.transaction + +import com.google.protobuf.ByteString +import com.wavesplatform.account.KeyPair +import com.wavesplatform.common.utils.EitherExt2.* +import com.wavesplatform.protobuf.transaction.PBTransactions +import com.wavesplatform.transaction.assets.IssueTransaction +import com.wavesplatform.transaction.serialization.impl.IssueTxSerializer +import org.scalatest.freespec.AnyFreeSpec +import org.scalatest.matchers.should.Matchers + +class HighDecimalsIssueSpec extends AnyFreeSpec with Matchers { + + private val sender = KeyPair(Array.fill(32)(1: Byte)) + + "High Decimals & ERC-20 Asset Compatibility" - { + "allows issuing assets with up to 18 decimals (Ethereum standard)" in { + val txEither = IssueTransaction.create( + version = TxVersion.V2, + sender = sender.publicKey, + name = "ETHCOIN", + description = "Ethereum style token with 18 decimals", + quantity = 1000000000000000L, + decimals = 18, + reissuable = true, + script = None, + fee = 100000000L, + timestamp = System.currentTimeMillis() + ) + + txEither.isRight shouldBe true + val tx = txEither.explicitGet() + tx.decimals.value shouldBe 18 + tx.name.toStringUtf8 shouldBe "ETHCOIN" + } + + "supports all intermediate decimals from 0 to 18" in { + for (d <- 0 to 18) { + val tx = IssueTransaction.create( + version = TxVersion.V2, + sender = sender.publicKey, + name = s"TOKEN$d", + description = s"Token with $d decimals", + quantity = 1000000L, + decimals = d.toByte, + reissuable = false, + script = None, + fee = 100000000L, + timestamp = System.currentTimeMillis() + ).explicitGet() + + tx.decimals.value shouldBe d + } + } + + "rejects decimals greater than 18" in { + val txEither = IssueTransaction.create( + version = TxVersion.V2, + sender = sender.publicKey, + name = "INVALID", + description = "Should fail", + quantity = 1000000L, + decimals = 19, + reissuable = false, + script = None, + fee = 100000000L, + timestamp = System.currentTimeMillis() + ) + + txEither.isLeft shouldBe true + } + + "preserves byte serialization fidelity for 18-decimal assets" in { + val original = IssueTransaction.create( + version = TxVersion.V2, + sender = sender.publicKey, + name = "ETHTOKEN", + description = "Roundtrip byte test", + quantity = 5000000000000L, + decimals = 18, + reissuable = true, + script = None, + fee = 100000000L, + timestamp = 1700000000000L + ).map(_.signWith(sender.privateKey)).explicitGet() + + val bytes = IssueTxSerializer.toBytes(original) + val parsed = IssueTransaction.parseBytes(bytes).get + + parsed.id() shouldBe original.id() + parsed.decimals.value shouldBe 18 + parsed.name shouldBe original.name + parsed.quantity.value shouldBe original.quantity.value + } + + "preserves Protobuf serialization fidelity for 18-decimal assets" in { + val original = IssueTransaction.create( + version = TxVersion.V3, + sender = sender.publicKey, + name = "PBTOKEN", + description = "Protobuf roundtrip test", + quantity = 9999999999L, + decimals = 18, + reissuable = false, + script = None, + fee = 100000000L, + timestamp = 1700000000000L + ).map(_.signWith(sender.privateKey)).explicitGet() + + val pb = PBTransactions.protobuf(original) + val vanilla = PBTransactions.vanillaUnsafe(pb).asInstanceOf[IssueTransaction] + vanilla.decimals.value shouldBe 18 + vanilla.id() shouldBe original.id() + } + } +}