Please wait. This can take some minutes ...
Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance.
Project price only 1 $
You can buy this project and download/modify it how often you want.
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)
}
}
}