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

sss.openstar.wallet.Wallet.scala Maven / Gradle / Ivy

package sss.openstar.wallet


import com.typesafe.scalalogging.Logger
import scorex.crypto.signatures.PublicKey
import sss.ancillary.Logging
import sss.db.ops.DbOps.{DbRunOps, OptFutureTxOps}
import sss.db.{DbOptimisticLockingException, FutureTx, Row}
import sss.openstar.account.NodeIdentity
import sss.openstar.account.Ops.MakeTxSig
import sss.openstar.contract.{Decumbrance, Encumbrance, NullDecumbrance, NullEncumbrance, PrivateKeySig, ReturnSecretDec, SaleOrReturnSecretEnc, SaleSecretDec, SingleIdentityDec, SingleIdentityEnc, SinglePrivateKey}
import sss.openstar.balanceledger._
import sss.openstar.chains.TxWriterActor.{InternalCommit, InternalTempNack, InternalTxResult}
import sss.openstar.tools.SendTxSupport.{NackTxResult, TempNackTxResult}
//import sss.openstar.contract._
import sss.openstar.schemamigration.SqlSchemaNames.ColumnNames.{amountCol, clientCol}
import sss.openstar.eventbus.EventPublish
import sss.openstar.identityledger.IdentityServiceQuery
import sss.openstar.ledger.{LedgerId, LedgerItem, SignedTxEntry, TxId}
import sss.openstar.tools.SendTxSupport.SendTx
import sss.openstar.util.Amount.Ops._
import sss.openstar.util.{Amount, AmountBuilder}
import sss.openstar.wallet.ClientWallet.{ClientBalance, NewClientBalance}
import sss.openstar.wallet.Wallet.{Payment, PublicKeyFilter, Transfer}
import sss.openstar.wallet.WalletPersistence.{Lodgement, LodgementStatus}
import sss.openstar.{Currency, UniqueNodeIdentifier}
import sss.openstar.ledger.TxSig
import java.util.Locale
import scala.concurrent.duration.Duration
import scala.concurrent.{Await, Future}
import scala.reflect.runtime.universe._
import scala.util.control.NonFatal
import scala.util.{Failure, Success, Try}

/**
 * This wallet will attempt to double spend depending on when the unspent are marked spent!
 *
 */
object Wallet {
  type PublicKeyFilter = PublicKey => Boolean


  def syncLedgerWithWallet[C <: Currency](balanceLedger: BalanceLedgerQuery[C],
                           wallets: Seq[Wallet[C]], log: Logger): Unit = {

    balanceLedger.keys().foreach { key =>
      balanceLedger.entry(key) match {
        case Some(entry) =>
          wallets.foreach { wallet =>
            wallet.toLodgement(key, entry) match {
              case Some(l) =>

                wallet.credit(l).foreach(l => {
                  log.info(s"${wallet.walletOwner} recovering index $key amount: ${entry.amount}")
                })

              case None =>
            }
          }
        case None =>
      }
    }

  }

  case class CreditTooLow(msg: String) extends RuntimeException(msg)

  case class Transfer(amount:Long, numBlocksToReclaimAfter: Int, txOutput: Seq[TxOutput])

  case class Payment(identity: String,
                     amount: Long,
                     numBlocksToReclaimAfter: Int,
                     txIdentifier: Option[String] = None,
                     blockHeight: Long = 0)

  case class UnSpent(txIndex: TxIndex, out: LazyTxOutput)

}


final class Wallet[+C <: Currency: TypeTag](val walletOwner: UniqueNodeIdentifier,
                                      val ledgerId: LedgerId,
                                      nodeIdControlsPublicKey: PublicKeyFilter,
                                      identityServiceQuery: IdentityServiceQuery,
                                      walletPersistence: WalletPersistence,
                                            private[wallet] val clientWallet: ClientWallet,
                                            val currentBlockHeight: () => Long
            )(implicit events: EventPublish,
              cb: AmountBuilder[C]) extends Logging {


  private val preventReentry = new Object()

  import Wallet._
  import walletPersistence.implDb

  implicit val impLedgerId = ledgerId

  override def hashCode(): Int = walletOwner.hashCode()

  override def equals(o: Any): Boolean = o match {
    case w: Wallet[C @unchecked] =>
      w.walletOwner.toLowerCase(Locale.ENGLISH) == walletOwner.toLowerCase(Locale.ENGLISH) &&
        w.ledgerId == ledgerId
    case _ => false
  }

  override def toString: UniqueNodeIdentifier = {
    s"Wallet: $walletOwner, Ledger Id: ${ledgerId.id}"
  }

  //  def reclaimInFlight(closedHeight: Long): Int =
  //    walletPersistence.reclaimInFlight(closedHeight)

  def listEntries: Seq[Lodgement] = walletPersistence.list(ledgerId)

  /**
   * Make NO assumptions about what the returned Lodgement
   * does to the balance. The only way to get the balance is call balance()
   *
   * @param txIndex
   * @return
   */
  def confirmSpent(txIndex: TxIndex): Option[Lodgement] = preventReentry.synchronized {
    walletPersistence
      .markSpent(txIndex)
  }


  /**
   * Make NO assumptions about what the returned Lodgement
   * does to the balance. The only way to get the balance is call balance()
   *
   * @param txIndex
   * @param txOutput
   * @return
   */
  def apply(txIndex: TxIndex, txOutput: TxOutput): Option[Lodgement] = preventReentry.synchronized {
    //log.debug("Trying to lodge {} {} to {}", txIndex, txOutput, walletOwner)
    val l = toLodgement(txIndex, txOutput)
      l.flatMap(credit)
  }

  private[wallet] def credit(lodgement: Lodgement): Option[Lodgement] = creditFTx(lodgement)

  private[wallet] def creditFTx(lodgement: Lodgement): Option[Lodgement] = preventReentry.synchronized {

    val result = for {
      lodgeOpt <- walletPersistence.trackFTx(lodgement)
      creditOpt <- lodgeOpt.map {
        case Lodgement(Some(client), _, _, txOutput, _) =>
          clientWallet.credit(txOutput.amount, client)
            .map(newBalance => {
              events publish NewClientBalance(
                ledgerId, walletOwner, client, Amount[C](newBalance), amount(txOutput.amount))
              log.info(s"${walletOwner} crediting ${txOutput.amount} to client wallet $client (${ledgerId.id})")
            })
        case l =>
          log.info(s"${walletOwner} crediting ${l.txOutput.amount} (${ledgerId.id})")
          FutureTx.unit(())
      }.toFutureTxOpt
    } yield lodgeOpt

    result.dbRunSyncGet
  }


  def clientBalanceTotal(): Long = preventReentry.synchronized (clientWallet.total)

  def clientBalance(client: String): Option[Amount] = preventReentry.synchronized (clientWallet.balance(client).map(amount))

  def clientBalances(startIndex: Long, pageSize: Int): Seq[ClientBalance] = preventReentry.synchronized {
    clientWallet.listClients(startIndex, pageSize)
      .map(r => ClientBalance(
        ledgerId,
        walletOwner,
        r.string(clientCol),
        amount(r.long(amountCol))))
  }


  private def toLodgement(txIndex: TxIndex, txOutput: TxOutput): Option[Lodgement] = {

    txOutput.encumbrance match {
      case SinglePrivateKey(pKey, _,  minBlockHeight) =>

        if (nodeIdControlsPublicKey(pKey)) {
          Option(Lodgement(LodgementStatus.UnSpent, txIndex, txOutput, minBlockHeight))
        } else {
          identityServiceQuery.identify(pKey).flatMap { acc =>
            if (acc.nodeId == walletOwner)
              Option(Lodgement(LodgementStatus.UnSpent, txIndex, txOutput, minBlockHeight))
            else
              None
          }
        }

      case SingleIdentityEnc(`walletOwner`, None, minBlockHeight) =>
        Option(Lodgement(LodgementStatus.UnSpent, txIndex, txOutput, minBlockHeight))

      case SingleIdentityEnc(`walletOwner`, c@Some(client), 0) =>

        Option(Lodgement(c, LodgementStatus.UnSpent, txIndex, txOutput, 0))

      case SingleIdentityEnc(`walletOwner`, Some(_), minBlockHeight) =>
        // A non zero blockheight and a client shouldn't happen, but if it does, just keep the money
        Option(Lodgement(LodgementStatus.UnSpent, txIndex, txOutput, minBlockHeight))

      case SaleOrReturnSecretEnc(`walletOwner`, _, _, minBlockHeight) =>
        Option(Lodgement(LodgementStatus.UnSpent, txIndex, txOutput, minBlockHeight))

      case NullEncumbrance =>
        log.warn(s"Keeping NullEncumbrance for $walletOwner, amount ${txOutput.amount}")
        Option(Lodgement(LodgementStatus.UnSpent, txIndex, txOutput, 0))
      case _ => None
    }
  }

  def markSpent(spentIns: Seq[TxInput]): Unit = preventReentry.synchronized {
    spentIns.foreach { in =>
      walletPersistence.markSpent(in.txIndex)
    }
  }

  private[wallet] def markUnSpent(spentIns: Seq[TxInput], activeAtHeight: Long = 0): Unit = {
    spentIns.foreach { in =>
      walletPersistence.markUnSpent(in.txIndex, activeAtHeight)
    }
  }

  def update(txId: TxId,
             creditsOrderedByIndex: Seq[TxOutput],
             inBlock: Long = currentBlockHeight()): Unit = preventReentry.synchronized ({

    creditsOrderedByIndex.indices foreach { i =>
      //toLodgement(TxIndex(txId, i), creditsOrderedByIndex(i))
      val l = Lodgement(TxIndex(txId, i), creditsOrderedByIndex(i), inBlock)
      credit(l)
    }
  })



  def output(amount: Amount, encumbrance: Encumbrance): TxOutput =
    TxOutput(amount.value, encumbrance)

  def appendOutputs(tx: Tx, txOutput: TxOutput*): Tx = {
    val newTxOuts = tx.outs ++ txOutput
    StandardTx(tx.ins, newTxOuts)
  }

  def amount(value: Long): Amount = Amount[C](value)
  /**
   *
   * @param amountToSpend
   * @param numBlocksToReclaimAfterIfNotSpent use this when creating a tx to be transmitted by
   *                                          someone else. If they don't transmit, reclaim the index
   * @return
   */
  def createTx(amountToSpend: Amount,
               numBlocksToReclaimAfterIfNotSpent: Int): Try[Tx] = preventReentry.synchronized {


    val h = currentBlockHeight()

    def unSpent = findUnSpent(h)

    def fund(acc: Seq[TxInput], outs: LazyList[UnSpent], fundedTo: Amount, target: Amount): (Seq[TxInput], Seq[TxOutput]) = {

        if (fundedTo.value == target.value) {
          val clientWalletTotal = clientWallet.total
          log.debug(s"Client wallet total $clientWalletTotal for ${walletOwner}")
          if (hasFunds(Amount[C](clientWalletTotal), outs)) {
            (acc, Seq.empty)
          } else {
            throw CreditTooLow(s"hasFunds failed 2, funded to ${fundedTo} num inputs is ${acc.size}")
          }
        } else {
          outs.headOption match {
            case None =>

              throw CreditTooLow(s"Not enough credit, funded to ${fundedTo} num inputs is ${acc.size}")
            case Some(unspent) =>
              if ((target - fundedTo).get.value >= unspent.out.amount) {
                val txIn = toInput(unspent)
                fund(acc :+ txIn, outs.tail, (fundedTo + unspent.out.amount).get, target)
              } else {
                val txIn = toInput(unspent)
                val change = Amount.trySubtract(unspent.out.amount,(target - fundedTo)).get
                val minBalance = Amount.trySubtract(clientWallet.total, change)

                log.info(s"[Owner:$walletOwner] minbalance $minBalance change $change target $target fundedTo $fundedTo unspent ${unspent.out.amount}")
                if (hasFunds(minBalance.get.valueOrZeroIfNegative, outs.tail)) {
                  (acc :+ txIn, Seq(TxOutput(change.value, encumberToIdentity())))
                } else {
                  throw CreditTooLow(s"hasFunds failed, funded to ${fundedTo} num inputs is ${acc.size}")
                }

              }
          }
        }
      }

      Try {
        require(amountToSpend.value > 0, s"Amount is $amountToSpend, cannot spend this")
        fund(Seq(), unSpent, Amount(0), amountToSpend)
      } flatMap {
        case  (newIns, change) =>

          FutureTx.sequence(newIns.map(in =>
            walletPersistence.markInFlightFTx(
              in.txIndex,
              numBlocksToReclaimAfterIfNotSpent + h)
          )).dbRunSync.map(_ => StandardTx(newIns, change))

      } recoverWith {
        case e: DbOptimisticLockingException =>
          log.warn(e.toString)
          createTx(amountToSpend, numBlocksToReclaimAfterIfNotSpent)
        case NonFatal(e) =>
          log.warn(s"Failed to create tx for amount $amountToSpend", e)
          Failure(e)
      }

  }


  @scala.annotation.tailrec
  final private[wallet] def hasFunds(amount: Amount, outs: LazyList[UnSpent]): Boolean = {
    if (amount.value == 0) true
    else {
      outs.headOption match {
        case None => false
        case Some(out) =>
          if (out.out.amount < amount.value) {
            hasFunds((amount - out.out.amount).get, outs.tail)
          } else true
      }
    }
  }

  private[wallet] def toInput(unSpent: UnSpent): TxInput = {
    TxInput(unSpent.txIndex, unSpent.out.amount, createDecumbrance(unSpent.out.encumbrance))
  }

  private[wallet] def createDecumbrance(enc: Encumbrance): Decumbrance = {
    enc match {
      case _: SinglePrivateKey => PrivateKeySig
      case SaleOrReturnSecretEnc(returnIdentity,
      claimants,
      hashOfSecret,
      returnBlockHeight) => {
        if (returnIdentity == walletOwner) ReturnSecretDec
        //If the only claimant is you.
        else if (claimants == Seq(walletOwner)) SaleSecretDec
        else throw new IllegalArgumentException("This encumbrance is nothing to do with our identity.")
      }

      case _: SingleIdentityEnc => SingleIdentityDec

      case NullEncumbrance => NullDecumbrance()

    }
  }

  def encumberToTrustee(
                         trusteeIdentity: String
                       ): Encumbrance =
    SingleIdentityEnc(trusteeIdentity, walletOwner)


  def encumberToIdentity(atBlockHeight: Long = 0,
                         someIdentity: String = walletOwner
                        ): Encumbrance =
    SingleIdentityEnc(someIdentity, None, atBlockHeight)


  private[wallet] def unSpentOpt(lodgement: Lodgement, atBlockHeight: Long): Option[UnSpent] = {

    val txOut = lodgement.txOutput
    txOut.encumbrance match {
      case SinglePrivateKey(pKey, _, minBlockHeight) if minBlockHeight <= atBlockHeight =>
        if (nodeIdControlsPublicKey(pKey)) {
          Option(UnSpent(lodgement.txIndex, txOut))
        } else {
          identityServiceQuery.identify(pKey).flatMap { acc =>
            if (acc.nodeId == walletOwner) Option(UnSpent(lodgement.txIndex, txOut))
            else None
          }
        }

      case SingleIdentityEnc(`walletOwner`, _, blockHeight) if blockHeight <= atBlockHeight =>
        Option(UnSpent(lodgement.txIndex, txOut))

      case SaleOrReturnSecretEnc(`walletOwner`, _, _, returnBlockHeight) if returnBlockHeight <= atBlockHeight =>
        Option(UnSpent(lodgement.txIndex, txOut))

      case NullEncumbrance => Option(UnSpent(lodgement.txIndex, txOut))

      case x =>
        //log.debug(s"$walletOwner: $lodgement at blockheight $atBlockHeight spent ($x)")
        None
    }

  }

  def syncLedgerWithWallet[C1 <: Currency](balanceLedger: BalanceLedgerQuery[C1]): Unit = {
    Wallet.syncLedgerWithWallet(balanceLedger, Seq(this), log)
  }

  def find(txIndex: TxIndex): Option[Row] = walletPersistence.find(txIndex)

  def syncWalletWithLedger[C1 <: Currency](balanceLedger: BalanceLedgerQuery[C1]): Unit = preventReentry.synchronized {
    findBadIndices(balanceLedger) foreach { bad =>
      log.debug(s"Bad index found for ${walletOwner} marking spent ${bad} (${ledgerId.id})")
      walletPersistence.markSpent(bad)
    }
  }

  private def findBadIndices[C1 <: Currency](balanceLedger: BalanceLedgerQuery[C1]): Seq[TxIndex] = {

    walletPersistence.listUnSpent(Long.MaxValue).foldLeft(Seq.empty[TxIndex]) {
      (acc, us) => {
        if (balanceLedger.entry(us.txIndex).isEmpty) {
          log.warn(s"Wallet ${walletOwner} contained a txIndex not in the ledger for amount ${us.txOutput.amount}")
          us.txIndex +: acc
        } else {
          acc
        }
      }

    }
  }


  def balance(atBlockHeight: Long = currentBlockHeight()): Try[Amount] = preventReentry.synchronized(
    findUnSpent(atBlockHeight).foldLeft(Try(amount(0)))((acc, e) => acc + e.out.amount)
  )

  //def balanceInFlight(atBlockHeight: Long): Int = findInFlight(atBlockHeight).foldLeft(0)((acc, e) => acc + e.out.amount)

  /*private[wallet] def findInFlight(atBlockHeight: Long): Seq[UnSpent] = {
    val allInflightEntries = walletPersistence.listInFlight

    allInflightEntries.foldLeft(Seq[UnSpent]())((acc: Seq[UnSpent], lodgement: Lodgement) =>
      unSpentOpt(lodgement, atBlockHeight) match {
        case Some(u) => acc :+ u
        case None => acc
      }
    )
  }*/

  private[wallet] def findUnSpent(atBlockHeight: Long): LazyList[UnSpent] = {

    def allWalletEntries = walletPersistence.listUnSpent(atBlockHeight)

    allWalletEntries.map { lodgement: Lodgement =>
      unSpentOpt(lodgement, atBlockHeight)
    }.collect { case Some(u) => u }

  }

  def unlockWallet(nodeIdentity: NodeIdentity)(implicit sendTx: SendTx): UnlockedWallet[C] =
    new UnlockedWallet[C](this, nodeIdentity, preventReentry)

}

class UnlockedWallet[+C <: Currency] private[wallet](val w: Wallet[C], nodeIdentity: NodeIdentity, preventReentry: Object)(implicit sendTx: SendTx, events: EventPublish) extends Logging {

  import w._

  import scala.concurrent.ExecutionContext.Implicits.global

  require(nodeIdentity.id == w.walletOwner, s"Attempt to use identity ${nodeIdentity.id} to unlock wallet belonging to ${w.walletOwner}")


  override def hashCode(): Int = w.hashCode()

  override def equals(o: Any): Boolean = w.equals(o)

  def transferBetweenClients(amount: Long, fromClient: String, toClient: String): Try[Unit] = preventReentry.synchronized {
    clientWallet.transfer(amount, fromClient, toClient).map(_ => {
      val fromClientBal = w.amount(clientWallet.balance(fromClient).get)
      val toClientBal = w.amount(clientWallet.balance(toClient).get)
      events publish NewClientBalance(ledgerId, walletOwner, fromClient, fromClientBal, w.amount(-amount))
      events publish NewClientBalance(ledgerId, walletOwner, toClient, toClientBal, w.amount(amount))
    })
  }

  def payTrustee(amount: Long, trustee: String): Try[Future[InternalTxResult]] = preventReentry.synchronized {
    createTx(w.amount(amount), 2) map { tx =>
      val enc = encumberToTrustee(trustee)
      val finalTxUnsigned = appendOutputs(tx, TxOutput(amount, enc))
      val signedTxF = sign(finalTxUnsigned)
      signedTxF flatMap { signedTx =>
        // use the tx minus the output as we don't need to track the output in this wallet
        sendAndUpdateLocal(tx, signedTx)
      }
    }
  }

  def withdrawClientMoney(amountValue: Long, client: String, encumbrance: Encumbrance): Try[Future[InternalTxResult]] = preventReentry.synchronized {

    val amount = w.amount(amountValue)
    val mybBal = balance()
    val tBefore = clientWallet.total
    val before = clientWallet.balance(client)
    clientWallet.withdraw(amountValue, client) flatMap { newBalance =>
      val after = clientWallet.balance(client)
      val tAfter = clientWallet.total
      log.info(s"Balance $mybBal :  for $client before $before, after $after: total clients money before $tBefore total after: $tAfter")

      createTx(amount, 2) flatMap { tx =>
        Try {
          //val enc = encumberToIdentity(currentBlockHeight(), client)
          val finalTxUnsigned = appendOutputs(tx, TxOutput(amountValue, encumbrance))
          val signedTx = sign(finalTxUnsigned)
          // use the tx minus the output as we don't need to track the output in this wallet
          (tx, signedTx)
        }
      } map {
        case (tx, signedTxF) =>
          signedTxF.flatMap { signedTx =>
            sendAndUpdateLocal(tx, signedTx) map {
              case success: InternalCommit =>
                events publish NewClientBalance(ledgerId, walletOwner, client, w.amount(newBalance), w.amount(-amountValue))
                success
              case problem =>
                clientWallet.lodge(amountValue, client).recover {
                  case e => log.error(e.toString)
                }
                problem
            }
          } andThen { //if one of the Futures fails
            case Failure(e) =>
              log.error(e.toString)
              clientWallet.lodge(amountValue, client).recover {
                case e => log.error(e.toString)
              }
          }
      } recoverWith { //if one of the Try's fails
        case tryError =>
          log.error(tryError.toString)
          clientWallet.lodge(amountValue, client).recover {
            case e =>
              log.error(e.toString)
          }
          Failure(tryError)
      }
    }
  }

  def sign(tx: Tx, secretOpt: Option[Array[Byte]] = None): Future[SignedTxEntry] = {

    val sigs = tx.ins.map { in =>
      in.sig match {
        case PrivateKeySig => nodeIdentity.defaultNodeVerifier.signer.sign(tx.txId).map(PrivateKeySig.createUnlockingSignature)
        case NullDecumbrance(_) => Future.successful(Seq.empty)
        case SingleIdentityDec => SingleIdentityDec.createUnlockingSignature(
          tx.txId, nodeIdentity.tagBytes, nodeIdentity.defaultNodeVerifier.signer.sign(_)
        )

        case SaleSecretDec if secretOpt.nonEmpty =>
          val secret = secretOpt.get
          require(secret.size >= 16, s"Secret must be 16 bytes or more (not ${secret.size})")
          SaleSecretDec.createUnlockingSignature(
            tx.txId,
            nodeIdentity.id,
            nodeIdentity.tagBytes,
            nodeIdentity.defaultNodeVerifier.signer.sign(_),
            secret)

        case SaleSecretDec => SaleSecretDec
          .createUnlockingSignature(tx.txId, nodeIdentity.id, nodeIdentity.tagBytes, nodeIdentity.defaultNodeVerifier.signer.sign(_))
        case ReturnSecretDec => ReturnSecretDec.createUnlockingSignature(tx.txId, nodeIdentity.tagBytes, nodeIdentity.defaultNodeVerifier.signer.sign(_))
      }
    }
    Future
      .sequence(sigs)
      .flatMap(s => nodeIdentity.txSig(tx.txId, s))
      .map(SignedTxEntry(tx.toBytes, _))
  }


  def payAsync(tx: Tx, secret: Option[Array[Byte]] = None): Future[InternalTxResult] = {
    sign(tx, secret).flatMap { signedTx =>
      sendAndUpdateLocal(tx, signedTx)
    }
  }

  private def sendAndUpdateLocal(tx: Tx, signedTx: SignedTxEntry): Future[InternalTxResult] = {

    sendTx(LedgerItem(ledgerId, signedTx.txId, signedTx.toBytes)).whenCommitted.map(_.internalCommit).map {

      case r@InternalCommit(_, blockChainTxId) =>
        preventReentry.synchronized {
          update(blockChainTxId.blockTxId.txId, tx.outs, blockChainTxId.height)
          r
        }

    } recover {
      case TempNackTxResult(nack) => preventReentry.synchronized {
        w.markUnSpent(tx.ins)
        nack
      }
      case NackTxResult(nack) => preventReentry.synchronized {
        w.markSpent(tx.ins)
        log.warn(s"ATTENTION ${walletOwner}! A tx has been permanently rejected, marking those inputs as 'spent'")
        tx.ins.foreach(in => log.warn(s"MARKING SPENT FOR ${walletOwner} - '${in}'"))
        nack
      }
    }
  }


  def payAsync(transfer: Transfer): Future[InternalTxResult] = {
    log.debug(s"Attempting to create a transfer tx for ${transfer.amount} with wallet $walletOwner")
    val amountToTransfer = amount(transfer.amount)
    Future.fromTry(createTx(amountToTransfer, transfer.numBlocksToReclaimAfter)).flatMap { tx =>
      val unSignedTx = appendOutputs(tx, transfer.txOutput: _*)
      sign(unSignedTx).flatMap { signedTx =>
        // use the tx minus the output as we don't need to track the output in this wallet
        sendAndUpdateLocal(tx, signedTx)
      }
    }
  }

  def payAsync(payment: Payment): Future[InternalTxResult] = {
    log.debug(s"Attempting to create a tx for ${payment.amount} with wallet $walletOwner")
    val amountToTransfer = amount(payment.amount)
    Future.fromTry(createTx(amountToTransfer, payment.numBlocksToReclaimAfter)).flatMap { tx =>
      addPaymentToTx(payment, tx) flatMap {
        case (_, signedTx: SignedTxEntry) =>
          //ignore the added payment and only track the pre payment tx
          sendAndUpdateLocal(tx, signedTx)
      }
    }
  }


  def addPaymentToTx(payment: Payment, tx: Tx): Future[(Tx, SignedTxEntry)] = {
    val enc = encumberToIdentity(payment.blockHeight, payment.identity)
    val finalTxUnsigned = appendOutputs(tx, TxOutput(payment.amount, enc))
    sign(finalTxUnsigned) map { signedTx =>
      (finalTxUnsigned, signedTx)
    }
  }

  def pay(payment: Payment)(implicit timeout: Duration): Future[InternalTxResult] = Future.fromTry {
    val amountToPay = amount(payment.amount)
    createTx(amountToPay, payment.numBlocksToReclaimAfter)
  } flatMap { tx =>
      addPaymentToTx(payment, tx).flatMap {
        case (_, signedTx) =>
          // use the tx minus the output as we don't need to track the output in this wallet
          sendAndUpdateLocal(tx, signedTx)
      }
  }

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy