All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.wavesplatform.account.Recipient.scala Maven / Gradle / Ivy

The newest version!
package com.wavesplatform.account

import com.google.common.cache.{Cache, CacheBuilder}

import java.nio.ByteBuffer
import com.google.common.primitives.{Bytes, Ints}
import com.wavesplatform.common.state.ByteStr
import com.wavesplatform.common.utils.Base58
import com.wavesplatform.crypto
import com.wavesplatform.lang.ValidationError
import com.wavesplatform.lang.v1.traits.domain.Recipient as RideRecipient
import com.wavesplatform.serialization.Deser
import com.wavesplatform.transaction.TxValidationError.{GenericError, InvalidAddress, WrongChain}
import com.wavesplatform.utils.{EthEncoding, StringBytes, base58Length}
import play.api.libs.json.*

sealed trait AddressOrAlias {
  def chainId: Byte
  def bytes: Array[Byte]
}

final class Address private (val chainId: Byte, val publicKeyHash: Array[Byte], checksum: Array[Byte]) extends AddressOrAlias {
  override lazy val bytes: Array[Byte] = Bytes.concat(Array(1.toByte, chainId), publicKeyHash, checksum)
  override lazy val toString: String   = ByteStr(bytes).toString

  override def equals(obj: Any): Boolean = obj match {
    case a: Address => java.util.Arrays.equals(publicKeyHash, a.publicKeyHash) && chainId == a.chainId
    case _          => false
  }

  private lazy val hc = Ints.fromByteArray(checksum)

  override def hashCode(): Int = hc
}

final case class Alias(chainId: Byte, name: String) extends AddressOrAlias {
  override lazy val bytes: Array[Byte] = Bytes.concat(Array(Alias.AddressVersion, chainId), Deser.serializeArrayWithLength(name.utf8Bytes))
  override lazy val toString: String   = s"alias:${chainId.toChar}:$name"
}

object AddressOrAlias {
  def fromRide(r: RideRecipient): Either[ValidationError, AddressOrAlias] =
    r match {
      case RideRecipient.Address(bytes) => Address.fromBytes(bytes.arr, Some(AddressScheme.current.chainId))
      case RideRecipient.Alias(name)    => Alias.create(name)
    }

  def fromString(s: String, expectedChainId: Option[Byte] = None): Either[ValidationError, AddressOrAlias] =
    if (s.startsWith(Alias.Prefix))
      Alias.fromString(s, expectedChainId)
    else
      Address.fromString(s, expectedChainId)

  def fromBytes(buf: Array[Byte]): Either[ValidationError, AddressOrAlias] = buf.headOption match {
    case Some(Address.AddressVersion) => Address.fromBytes(buf, None)
    case Some(Alias.AddressVersion)   => Alias.fromBytes(buf, None)
    case _                            => throw new IllegalArgumentException(s"Not a valid recipient: ${ByteStr(buf)}")
  }
}

object Address {
  val Prefix: String           = "address:"
  val AddressVersion: Byte     = 1
  val ChecksumLength: Int      = 4
  val HashLength: Int          = 20
  val AddressLength: Int       = 1 + 1 + HashLength + ChecksumLength
  val AddressStringLength: Int = base58Length(AddressLength)

  private[this] val publicKeyBytesCache: Cache[(ByteStr, Byte), Address] = CacheBuilder
    .newBuilder()
    .softValues()
    .maximumSize(200000)
    .build()

  private[this] val bytesCache: Cache[ByteStr, Either[InvalidAddress, Address]] = CacheBuilder
    .newBuilder()
    .softValues()
    .maximumSize(200000)
    .build()

  def apply(publicKeyHash: Array[Byte], chainId: Byte = AddressScheme.current.chainId): Address = {
    require(publicKeyHash.length == HashLength, "Public key hash should be 20 bytes long")
    val checksumPayload = Bytes.concat(Array(1.toByte, AddressScheme.current.chainId), publicKeyHash)
    val checksum        = crypto.secureHash(checksumPayload)
    new Address(chainId, publicKeyHash, checksum.take(4))
  }

  def fromHexString(hexString: String): Address =
    Address(EthEncoding.toBytes(hexString))

  def fromPublicKey(publicKey: PublicKey, chainId: Byte = scheme.chainId): Address = {
    publicKeyBytesCache.get(
      (publicKey, chainId),
      { () =>
        val withoutChecksum = ByteBuffer
          .allocate(1 + 1 + HashLength)
          .put(AddressVersion)
          .put(chainId)
          .put(crypto.secureHash(publicKey.arr), 0, HashLength)
          .array()

        val bytes = ByteBuffer
          .allocate(AddressLength)
          .put(withoutChecksum)
          .put(calcCheckSum(withoutChecksum), 0, ChecksumLength)
          .array()

        createUnsafe(bytes)
      }
    )
  }

  def fromBytes(addressBytes: Array[Byte], expectedChainId: Option[Byte] = Some(AddressScheme.current.chainId)): Either[InvalidAddress, Address] = {
    bytesCache.get(
      ByteStr(addressBytes),
      { () =>
        Either
          .cond(
            addressBytes.length == Address.AddressLength,
            (),
            InvalidAddress(s"Wrong addressBytes length: expected: ${Address.AddressLength}, actual: ${addressBytes.length}")
          )
          .flatMap { _ =>
            val Array(version, network, _*) = addressBytes: @unchecked

            (for {
              _ <- Either.cond(version == AddressVersion, (), s"Unknown address version: $version")
              _ <- expectedChainId match {
                case Some(exp) if exp != network =>
                  Left(s"Address belongs to another network: expected: $exp(${exp.toChar}), actual: $network(${network.toChar})")
                case _ => Right(())
              }
              checkSum          = addressBytes.takeRight(ChecksumLength)
              checkSumGenerated = calcCheckSum(addressBytes.dropRight(ChecksumLength))
              _ <- Either.cond(java.util.Arrays.equals(checkSum, checkSumGenerated), (), s"Bad address checksum")
            } yield createUnsafe(addressBytes)).left.map(err => InvalidAddress(err))
          }
      }
    )
  }

  def fromString(addressStr: String, expectedChainId: Option[Byte] = Some(AddressScheme.current.chainId)): Either[ValidationError, Address] = {
    val base58String = if (addressStr.startsWith(Prefix)) addressStr.drop(Prefix.length) else addressStr
    for {
      _ <- Either.cond(
        base58String.length <= AddressStringLength,
        (),
        InvalidAddress(s"Wrong address string length: max=$AddressStringLength, actual: ${base58String.length}")
      )
      byteArray <- Base58.tryDecodeWithLimit(base58String).toEither.left.map(ex => InvalidAddress(s"Unable to decode base58: ${ex.getMessage}"))
      address   <- fromBytes(byteArray, expectedChainId)
    } yield address
  }

  def calcCheckSum(withoutChecksum: Array[Byte]): Array[Byte] = {
    val fullHash = crypto.secureHash(withoutChecksum)
    fullHash.take(ChecksumLength)
  }

  implicit val jsonFormat: Format[Address] = Format[Address](
    Reads(jsValue => fromString(jsValue.as[String]).fold(err => JsError(err.toString), JsSuccess(_))),
    Writes(addr => JsString(addr.toString))
  )

  @inline
  private[this] def scheme: AddressScheme = AddressScheme.current

  // Optimization, should not be used externally
  private[this] def createUnsafe(addressBytes: Array[Byte]): Address =
    new Address(addressBytes(1), addressBytes.drop(2).dropRight(4), addressBytes.takeRight(4))
}

object Alias {
  val Prefix: String = "alias:"

  val AddressVersion: Byte = 2
  val MinLength            = 4
  val MaxLength            = 30

  val AliasAlphabet = "-.0123456789@_abcdefghijklmnopqrstuvwxyz"

  def create(name: String): Either[ValidationError, Alias] = {
    createWithChainId(name, AddressScheme.current.chainId)
  }

  def fromString(str: String, expectedChainId: Option[Byte] = Some(AddressScheme.current.chainId)): Either[ValidationError, Alias] = {
    val aliasPatternInfo = "Alias string pattern is 'alias::"

    if (!str.startsWith(Prefix)) {
      Left(GenericError(aliasPatternInfo))
    } else {
      val charSemicolonAlias = str.drop(Prefix.length)
      val chainId            = charSemicolonAlias(0).toByte
      val name               = charSemicolonAlias.drop(2)
      if (charSemicolonAlias(1) != ':') {
        Left(GenericError(aliasPatternInfo))
      } else {
        createWithChainId(name, chainId, expectedChainId)
      }
    }
  }

  def fromBytes(bytes: Array[Byte], expectedChainId: Option[Byte]): Either[ValidationError, Alias] = {
    bytes match {
      case Array(`AddressVersion`, chainId, _, _, rest*) =>
        createWithChainId(new String(rest.toArray, "UTF-8"), chainId, expectedChainId)

      case _ =>
        Left(GenericError("Bad alias bytes"))
    }
  }

  private[this] def isValidAliasChar(c: Char): Boolean =
    ('0' <= c && c <= '9') || ('a' <= c && c <= 'z') || c == '_' || c == '@' || c == '-' || c == '.'

  private[wavesplatform] def createWithChainId(
      name: String,
      chainId: Byte,
      expectedChainId: Option[Byte] = Some(AddressScheme.current.chainId)
  ): Either[ValidationError, Alias] =
    if (name.length < MinLength || MaxLength < name.length)
      Left(GenericError(s"Alias '$name' length should be between $MinLength and $MaxLength"))
    else if (!name.forall(isValidAliasChar))
      Left(GenericError(s"Alias should contain only following characters: $AliasAlphabet"))
    else
      expectedChainId match {
        case Some(expected) if expected != chainId => Left(WrongChain(expected, chainId))
        case _                                     => Right(Alias(chainId, name))
      }

  implicit val writes: Writes[Alias] = Writes(a => JsString(a.toString))
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy