diff --git a/app-commons/src/main/scala/org/bitcoins/commons/jsonmodels/lnd/LndModels.scala b/app-commons/src/main/scala/org/bitcoins/commons/jsonmodels/lnd/LndModels.scala index 1e23f84944..8ebac567a6 100644 --- a/app-commons/src/main/scala/org/bitcoins/commons/jsonmodels/lnd/LndModels.scala +++ b/app-commons/src/main/scala/org/bitcoins/commons/jsonmodels/lnd/LndModels.scala @@ -52,4 +52,10 @@ case class TxDetails( destAddresses: Vector[BitcoinAddress], tx: Transaction, label: String -) +) extends LndModel + +case class UTXOLease( + id: ByteVector, + outPoint: TransactionOutPoint, + expiration: Long +) extends LndModel diff --git a/lnd-rpc-test/src/test/scala/org/bitcoins/lnd/rpc/LndRpcClientTest.scala b/lnd-rpc-test/src/test/scala/org/bitcoins/lnd/rpc/LndRpcClientTest.scala index 72c365a5dc..8514c5e5fc 100644 --- a/lnd-rpc-test/src/test/scala/org/bitcoins/lnd/rpc/LndRpcClientTest.scala +++ b/lnd-rpc-test/src/test/scala/org/bitcoins/lnd/rpc/LndRpcClientTest.scala @@ -7,6 +7,8 @@ import org.bitcoins.core.protocol.ln.currency._ import org.bitcoins.core.protocol.script.P2WPKHWitnessSPKV0 import org.bitcoins.testkit.fixtures.LndFixture +import scala.concurrent.Future + class LndRpcClientTest extends LndFixture { it must "get info from lnd" in { lnd => @@ -99,4 +101,19 @@ class LndRpcClientTest extends LndFixture { assert(balances.unsettledRemoteBalance == Satoshis.zero) } } + + it must "lease and release an output" in { lnd => + for { + utxos <- lnd.listUnspent + leaseFs = utxos.map(u => lnd.leaseOutput(u.outPointOpt.get, 100)) + _ <- Future.sequence(leaseFs) + leases <- lnd.listLeases() + _ = assert(leases.size == utxos.size) + + releaseFs = utxos.map(u => lnd.releaseOutput(u.outPointOpt.get)) + _ <- Future.sequence(releaseFs) + + leases <- lnd.listLeases() + } yield assert(leases.isEmpty) + } } diff --git a/lnd-rpc/src/main/scala/org/bitcoins/lnd/rpc/LndRpcClient.scala b/lnd-rpc/src/main/scala/org/bitcoins/lnd/rpc/LndRpcClient.scala index 4b060c2862..573058c09d 100644 --- a/lnd-rpc/src/main/scala/org/bitcoins/lnd/rpc/LndRpcClient.scala +++ b/lnd-rpc/src/main/scala/org/bitcoins/lnd/rpc/LndRpcClient.scala @@ -29,9 +29,16 @@ import org.bitcoins.crypto._ import org.bitcoins.lnd.rpc.LndRpcClient._ import org.bitcoins.lnd.rpc.LndUtils._ import org.bitcoins.lnd.rpc.config.{LndInstance, LndInstanceLocal} -import scodec.bits.ByteVector +import scodec.bits._ import signrpc._ -import walletrpc.{FinalizePsbtRequest, SendOutputsRequest, WalletKitClient} +import walletrpc.{ + FinalizePsbtRequest, + LeaseOutputRequest, + ListLeasesRequest, + ReleaseOutputRequest, + SendOutputsRequest, + WalletKitClient +} import java.io.{File, FileInputStream} import java.net.InetSocketAddress @@ -474,6 +481,64 @@ class LndRpcClient(val instance: LndInstance, binaryOpt: Option[File] = None)( } } + def listLeases(): Future[Vector[UTXOLease]] = { + listLeases(ListLeasesRequest()) + } + + def listLeases(request: ListLeasesRequest): Future[Vector[UTXOLease]] = { + logger.trace("lnd calling listleases") + + wallet + .listLeases(request) + .map(_.lockedUtxos.toVector.map { lease => + val txId = DoubleSha256DigestBE(lease.outpoint.get.txidBytes) + val vout = UInt32(lease.outpoint.get.outputIndex) + val outPoint = TransactionOutPoint(txId, vout) + UTXOLease(lease.id, outPoint, lease.expiration) + }) + } + + def leaseOutput( + outpoint: TransactionOutPoint, + leaseSeconds: Long): Future[Long] = { + val outPoint = + OutPoint(outpoint.txId.bytes, outputIndex = outpoint.vout.toInt) + + val request = LeaseOutputRequest(id = LndRpcClient.leaseId, + outpoint = Some(outPoint), + expirationSeconds = leaseSeconds) + + leaseOutput(request) + } + + /** LeaseOutput locks an output to the given ID, preventing it from being available for any future coin selection attempts. + * The absolute time of the lock's expiration is returned. + * The expiration of the lock can be extended by successive invocations of this RPC. + * @param request LeaseOutputRequest + * @return Unix timestamp for when the lease expires + */ + def leaseOutput(request: LeaseOutputRequest): Future[Long] = { + logger.trace("lnd calling leaseoutput") + + wallet.leaseOutput(request).map(_.expiration) + } + + def releaseOutput(outpoint: TransactionOutPoint): Future[Unit] = { + val outPoint = + OutPoint(outpoint.txId.bytes, outputIndex = outpoint.vout.toInt) + + val request = + ReleaseOutputRequest(id = LndRpcClient.leaseId, outpoint = Some(outPoint)) + + releaseOutput(request) + } + + def releaseOutput(request: ReleaseOutputRequest): Future[Unit] = { + logger.trace("lnd calling releaseoutput") + + wallet.releaseOutput(request).map(_ => ()) + } + /** Broadcasts the given transaction * @return None if no error, otherwise the error string */ @@ -639,6 +704,12 @@ class LndRpcClient(val instance: LndInstance, binaryOpt: Option[File] = None)( object LndRpcClient { + /** Lease id should be unique per application + * this is the sha256 of "lnd bitcoin-s" + */ + val leaseId: ByteString = + hex"8c45ee0b90e3afd0fb4d6f39afa3c5d551ee5f2c7ac2d06820ed3d16582186d2" + /** The current version we support of Lnd */ private[bitcoins] val version = "0.13.1"