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

com.wavesplatform.wallet.Wallet.scala Maven / Gradle / Ivy

The newest version!
package com.wavesplatform.wallet

import java.io.File
import com.google.common.primitives.{Bytes, Ints}
import com.wavesplatform.account.{Address, KeyPair, SeedKeyPair}
import com.wavesplatform.common.state.ByteStr
import com.wavesplatform.crypto
import com.wavesplatform.lang.ValidationError
import com.wavesplatform.settings.WalletSettings
import com.wavesplatform.transaction.TxValidationError.MissingSenderPrivateKey
import com.wavesplatform.utils.*
import play.api.libs.json.*

import scala.collection.concurrent.TrieMap
import scala.util.{Failure, Success, Try}

trait Wallet {
  def seed: Array[Byte]
  def nonce: Int
  def privateKeyAccounts: Seq[SeedKeyPair]
  def generateNewAccounts(howMany: Int): Seq[SeedKeyPair]
  def generateNewAccount(): Option[SeedKeyPair]
  def generateNewAccount(nonce: Int): Option[SeedKeyPair]
  def deleteAccount(account: SeedKeyPair): Boolean
  def privateKeyAccount(account: Address): Either[ValidationError, SeedKeyPair]
}

object Wallet {
  implicit class WalletExtension(private val wallet: Wallet) extends AnyVal {
    def findPrivateKey(addressString: String): Either[ValidationError, KeyPair] =
      for {
        acc        <- Address.fromString(addressString)
        privKeyAcc <- wallet.privateKeyAccount(acc)
      } yield privKeyAcc

    def exportAccountSeed(account: Address): Either[ValidationError, Array[Byte]] =
      wallet.privateKeyAccount(account).map(_.seed)
  }

  def generateNewAccount(seed: Array[Byte], nonce: Int): SeedKeyPair = {
    val accountSeed = generateAccountSeed(seed, nonce)
    KeyPair(ByteStr(accountSeed))
  }

  def generateAccountSeed(seed: Array[Byte], nonce: Int): Array[Byte] =
    crypto.secureHash(Bytes.concat(Ints.toByteArray(nonce), seed))

  @throws[IllegalArgumentException]("if invalid wallet configuration provided")
  def apply(settings: WalletSettings): Wallet =
    new WalletImpl(settings.file, settings.password, settings.seed)

  private[this] final case class WalletData(seed: ByteStr, accountSeeds: Set[ByteStr], nonce: Int)

  private[this] object WalletData {
    implicit val walletFormat: Format[WalletData] = Json.format
  }

  private[this] final class WalletImpl(maybeFile: Option[File], passwordOpt: Option[String], maybeSeedFromConfig: Option[ByteStr])
      extends ScorexLogging
      with Wallet {

    private[this] lazy val encryptionKey = {
      val password = passwordOpt.getOrElse(PasswordProvider.askPassword())
      JsonFileStorage.prepareKey(password)
    }

    private[this] lazy val actualSeed = maybeSeedFromConfig.getOrElse {
      val randomSeed = ByteStr(randomBytes(64))
      log.info(s"Your randomly generated seed is ${randomSeed.toString}")
      randomSeed
    }

    private[this] var walletData: WalletData = {
      if (maybeFile.isEmpty)
        WalletData(actualSeed, Set.empty, 0)
      else {
        def loadOrImport(walletFile: File): Try[WalletData] =
          Try(JsonFileStorage.load[WalletData](walletFile.getCanonicalPath, Some(this.encryptionKey)))

        val file = maybeFile.get
        if (file.isFile && file.length() > 0) {
          loadOrImport(maybeFile.get) match {
            case Failure(exception) =>
              throw new IllegalArgumentException(
                s"Failed to open existing wallet file '${maybeFile.get}' maybe provided password is incorrect",
                exception
              )
            case Success(walletData) =>
              require(maybeSeedFromConfig.forall(_ == walletData.seed), "Seed from config doesn't match the actual seed")
              walletData
          }
        } else {
          WalletData(actualSeed, Set.empty, 0)
        }
      }
    }

    private[this] object WalletLock {
      private[this] val lockObject = new Object
      def write[T](f: => T): T     = lockObject.synchronized(f)
    }

    private[this] val accountsCache: TrieMap[String, SeedKeyPair] = {
      val accounts = walletData.accountSeeds.map(KeyPair(_))
      TrieMap(accounts.map(acc => acc.toAddress.toString -> acc).toSeq*)
    }

    override def seed: Array[Byte] =
      this.walletData.seed.arr

    override def privateKeyAccounts: Seq[SeedKeyPair] =
      this.accountsCache.values.toVector

    override def generateNewAccounts(howMany: Int): Seq[SeedKeyPair] =
      (1 to howMany)
        .flatMap(_ => this.generateNewAccountWithoutSave())
        .tap(_ => this.saveWalletFile())

    override def generateNewAccount(): Option[SeedKeyPair] = WalletLock.write {
      generateNewAccount(getAndIncrementNonce())
    }

    override def generateNewAccount(nonce: Int): Option[SeedKeyPair] = WalletLock.write {
      generateNewAccountWithoutSave(nonce).map(acc => {
        this.saveWalletFile()
        acc
      })
    }

    override def deleteAccount(account: SeedKeyPair): Boolean = WalletLock.write {
      val before = walletData.accountSeeds.size
      walletData = walletData.copy(accountSeeds = walletData.accountSeeds - ByteStr(account.seed))
      accountsCache -= account.toAddress.toString
      this.saveWalletFile()
      before > walletData.accountSeeds.size
    }

    override def privateKeyAccount(account: Address): Either[ValidationError, SeedKeyPair] =
      accountsCache.get(account.toString).toRight[ValidationError](MissingSenderPrivateKey)

    override def nonce: Int =
      walletData.nonce

    private[this] def saveWalletFile(): Unit =
      maybeFile.foreach(f => JsonFileStorage.save(walletData, f.getCanonicalPath, Some(encryptionKey)))

    private[this] def generateNewAccountWithoutSave(): Option[SeedKeyPair] = WalletLock.write {
      generateNewAccountWithoutSave(getAndIncrementNonce())
    }

    private[this] def generateNewAccountWithoutSave(nonce: Int): Option[SeedKeyPair] = WalletLock.write {
      val account = Wallet.generateNewAccount(seed, nonce)

      val address = account.toAddress.toString
      if (!accountsCache.contains(address)) {
        accountsCache += account.toAddress.toString -> account
        walletData = walletData.copy(accountSeeds = walletData.accountSeeds + ByteStr(account.seed))
        log.info("Added account #" + privateKeyAccounts.size)
        Some(account)
      } else None
    }

    private[this] def getAndIncrementNonce(): Int = WalletLock.write {
      val oldNonce = walletData.nonce
      walletData = walletData.copy(nonce = walletData.nonce + 1)
      oldNonce
    }
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy