mirror of
https://github.com/bitcoin-s/bitcoin-s.git
synced 2025-01-18 13:24:25 +01:00
parent
272f31aeaa
commit
d60d984a6b
@ -28,4 +28,4 @@ jobs:
|
||||
~/.bitcoin-s/binaries
|
||||
key: ${{ runner.os }}-cache
|
||||
- name: run tests
|
||||
run: sbt ++2.12.15 coverage chainTest/test chain/coverageReport chain/coverageAggregate chain/coveralls cryptoJVM/test cryptoTestJVM/test cryptoJVM/coverageReport cryptoJVM/coverageAggregate cryptoJVM/coveralls coreTestJVM/test dlcTest/test coreJVM/coverageReport coreJVM/coverageAggregate coreJVM/coveralls secp256k1jni/test zmq/test zmq/coverageReport zmq/coverageAggregate zmq/coveralls appCommonsTest/test appServerTest/test oracleServerTest/test
|
||||
run: sbt ++2.12.15 coverage chainTest/test chain/coverageReport chain/coverageAggregate chain/coveralls cryptoJVM/test cryptoTestJVM/test cryptoJVM/coverageReport cryptoJVM/coverageAggregate cryptoJVM/coveralls coreTestJVM/test dlcTest/test coreJVM/coverageReport coreJVM/coverageAggregate coreJVM/coveralls secp256k1jni/test zmq/test zmq/coverageReport zmq/coverageAggregate zmq/coveralls appCommonsTest/test appServerTest/test oracleServerTest/test lnurlTest/test
|
||||
|
@ -28,4 +28,4 @@ jobs:
|
||||
~/.bitcoin-s/binaries
|
||||
key: ${{ runner.os }}-cache
|
||||
- name: run tests
|
||||
run: sbt ++2.13.8 coverage chainTest/test chain/coverageReport chain/coverageAggregate chain/coveralls cryptoTestJVM/test cryptoJVM/test cryptoJVM/coverageReport cryptoJVM/coverageAggregate cryptoJVM/coveralls coreTestJVM/test dlcTest/test coreJVM/coverageReport coreJVM/coverageAggregate coreJVM/coveralls secp256k1jni/test zmq/test zmq/coverageReport zmq/coverageAggregate zmq/coveralls appCommonsTest/test appServerTest/test oracleServerTest/test
|
||||
run: sbt ++2.13.8 coverage chainTest/test chain/coverageReport chain/coverageAggregate chain/coveralls cryptoTestJVM/test cryptoJVM/test cryptoJVM/coverageReport cryptoJVM/coverageAggregate cryptoJVM/coveralls coreTestJVM/test dlcTest/test coreJVM/coverageReport coreJVM/coverageAggregate coreJVM/coveralls secp256k1jni/test zmq/test zmq/coverageReport zmq/coverageAggregate zmq/coveralls appCommonsTest/test appServerTest/test oracleServerTest/test lnurlTest/test
|
||||
|
@ -44,7 +44,7 @@ import ujson.{Num, Str, Value}
|
||||
import scodec.bits.ByteVector
|
||||
|
||||
import java.io.File
|
||||
import java.net.{InetAddress, InetSocketAddress, URI}
|
||||
import java.net._
|
||||
import java.nio.file.Path
|
||||
import java.time._
|
||||
import java.util.UUID
|
||||
@ -83,6 +83,12 @@ object JsonReaders {
|
||||
}
|
||||
}
|
||||
|
||||
implicit object URLReads extends Reads[URL] {
|
||||
|
||||
override def reads(json: JsValue): JsResult[URL] =
|
||||
SerializerUtil.processJsString[URL](str => new URL(str))(json)
|
||||
}
|
||||
|
||||
implicit object ZonedDateTimeReads extends Reads[ZonedDateTime] {
|
||||
|
||||
override def reads(json: JsValue): JsResult[ZonedDateTime] =
|
||||
|
20
build.sbt
20
build.sbt
@ -143,6 +143,16 @@ lazy val clightningRpc = project
|
||||
.settings(CommonSettings.prodSettings: _*)
|
||||
.dependsOn(asyncUtilsJVM, bitcoindRpc)
|
||||
|
||||
lazy val lnurl = project
|
||||
.in(file("lnurl"))
|
||||
.settings(CommonSettings.prodSettings: _*)
|
||||
.dependsOn(appCommons, asyncUtilsJVM, tor)
|
||||
|
||||
lazy val lnurlTest = project
|
||||
.in(file("lnurl-test"))
|
||||
.settings(CommonSettings.testSettings: _*)
|
||||
.dependsOn(lnurl, testkit)
|
||||
|
||||
lazy val tor = project
|
||||
.in(file("tor"))
|
||||
.settings(CommonSettings.prodSettings: _*)
|
||||
@ -221,6 +231,8 @@ lazy val `bitcoin-s` = project
|
||||
serverRoutes,
|
||||
lndRpc,
|
||||
lndRpcTest,
|
||||
lnurl,
|
||||
lnurlTest,
|
||||
tor,
|
||||
torTest,
|
||||
scripts,
|
||||
@ -279,6 +291,8 @@ lazy val `bitcoin-s` = project
|
||||
serverRoutes,
|
||||
lndRpc,
|
||||
lndRpcTest,
|
||||
lnurl,
|
||||
lnurlTest,
|
||||
tor,
|
||||
torTest,
|
||||
scripts,
|
||||
@ -763,7 +777,11 @@ lazy val dlcWalletTest = project
|
||||
name := "bitcoin-s-dlc-wallet-test",
|
||||
libraryDependencies ++= Deps.dlcWalletTest
|
||||
)
|
||||
.dependsOn(coreJVM % testAndCompile, dlcWallet, testkit, testkitCoreJVM, dlcTest)
|
||||
.dependsOn(coreJVM % testAndCompile,
|
||||
dlcWallet,
|
||||
testkit,
|
||||
testkitCoreJVM,
|
||||
dlcTest)
|
||||
|
||||
lazy val dlcNode = project
|
||||
.in(file("dlc-node"))
|
||||
|
@ -2,6 +2,7 @@ package org.bitcoins.core.currency
|
||||
|
||||
import org.bitcoins.core.consensus.Consensus
|
||||
import org.bitcoins.core.number._
|
||||
import org.bitcoins.core.protocol.ln.currency.{LnCurrencyUnit, MilliSatoshis}
|
||||
import org.bitcoins.core.serializers.RawSatoshisSerializer
|
||||
import org.bitcoins.crypto.{Factory, NetworkElement}
|
||||
import scodec.bits.ByteVector
|
||||
@ -85,8 +86,10 @@ sealed abstract class CurrencyUnit
|
||||
//try removing this and running code, you should see
|
||||
//failures in the 'walletTest' module
|
||||
obj match {
|
||||
case cu: CurrencyUnit => cu.satoshis == satoshis
|
||||
case _ => false
|
||||
case cu: CurrencyUnit => cu.satoshis == satoshis
|
||||
case ln: LnCurrencyUnit => satoshis == ln.toSatoshis
|
||||
case ms: MilliSatoshis => satoshis == ms.toSatoshis
|
||||
case _ => false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
package org.bitcoins.core.protocol.ln.currency
|
||||
|
||||
import org.bitcoins.core._
|
||||
import org.bitcoins.core.currency.Satoshis
|
||||
import org.bitcoins.core.currency.{CurrencyUnit, Satoshis}
|
||||
import org.bitcoins.core.number._
|
||||
import org.bitcoins.core.protocol.ln._
|
||||
import org.bitcoins.core.util.Bech32
|
||||
@ -24,6 +24,21 @@ sealed abstract class LnCurrencyUnit
|
||||
def ==(ln: LnCurrencyUnit): Boolean =
|
||||
toPicoBitcoinValue == ln.toPicoBitcoinValue
|
||||
|
||||
override def equals(obj: Any): Boolean = {
|
||||
//needed for cases like
|
||||
//1BTC == 100,000,000 satoshis should be true
|
||||
//weirdly enough, this worked in scala version < 2.13.4
|
||||
//but seems to be broken in 2.13.4 :/
|
||||
//try removing this and running code, you should see
|
||||
//failures in the 'lnurl' module
|
||||
obj match {
|
||||
case ln: LnCurrencyUnit => toPicoBitcoinValue == ln.toPicoBitcoinValue
|
||||
case ms: MilliSatoshis => toMSat == ms
|
||||
case cu: CurrencyUnit => toSatoshis == cu.satoshis
|
||||
case _ => false
|
||||
}
|
||||
}
|
||||
|
||||
override def +(ln: LnCurrencyUnit): LnCurrencyUnit = {
|
||||
PicoBitcoins(toPicoBitcoinValue + ln.toPicoBitcoinValue)
|
||||
}
|
||||
|
@ -51,6 +51,21 @@ sealed abstract class MilliSatoshis
|
||||
toLnCurrencyUnit != lnCurrencyUnit
|
||||
}
|
||||
|
||||
override def equals(obj: Any): Boolean = {
|
||||
//needed for cases like
|
||||
//1BTC == 100,000,000 satoshis should be true
|
||||
//weirdly enough, this worked in scala version < 2.13.4
|
||||
//but seems to be broken in 2.13.4 :/
|
||||
//try removing this and running code, you should see
|
||||
//failures in the 'lnurl' module
|
||||
obj match {
|
||||
case ln: LnCurrencyUnit => toLnCurrencyUnit == ln
|
||||
case ms: MilliSatoshis => this.toBigInt == ms.toBigInt
|
||||
case cu: CurrencyUnit => toSatoshis == cu.satoshis
|
||||
case _ => false
|
||||
}
|
||||
}
|
||||
|
||||
def >=(ln: LnCurrencyUnit): Boolean = {
|
||||
toLnCurrencyUnit >= ln
|
||||
}
|
||||
|
@ -240,7 +240,7 @@ sealed abstract class Bech32 {
|
||||
val length = bech32.length
|
||||
val maxLength =
|
||||
// is this a LN invoice or not?
|
||||
if (bech32.startsWith("ln"))
|
||||
if (bech32.toLowerCase.startsWith("ln"))
|
||||
// BOLT 11 is not fully bech32 compatible
|
||||
// https://github.com/lightningnetwork/lightning-rfc/blob/master/11-payment-encoding.md#requirements
|
||||
Integer.MAX_VALUE
|
||||
|
@ -0,0 +1,68 @@
|
||||
package org.bitcoins.lnurl
|
||||
|
||||
import org.bitcoins.core.protocol.ln.LnInvoice
|
||||
import org.bitcoins.core.protocol.ln.currency.MilliSatoshis
|
||||
import org.bitcoins.lnurl.json.LnURLJsonModels._
|
||||
import org.bitcoins.testkit.util.BitcoinSAsyncTest
|
||||
|
||||
class LnURLClientTest extends BitcoinSAsyncTest {
|
||||
val client = new LnURLClient(None)
|
||||
|
||||
it must "make a pay request" in {
|
||||
val lnurl = LnURL.fromString(
|
||||
"LNURL1DP68GURN8GHJ7MRWW4EXCTNXD9SHG6NPVCHXXMMD9AKXUATJDSKHQCTE8AEK2UMND9HKU0TPXUCRJVEHXV6RYVEEX5CNSCESXYURSEP5VE3RWCNP89JRJD33XUEXGCFNX5URZCT9XYMRQDRXVVCKZVRPV43KYV3E8YMKZE3H89SNWEVZTNK")
|
||||
|
||||
client.makeRequest(lnurl).map {
|
||||
case pay: LnURLPayResponse =>
|
||||
assert(pay.maxSendable >= MilliSatoshis.zero)
|
||||
assert(pay.minSendable >= MilliSatoshis.zero)
|
||||
case _: LnURLWithdrawResponse =>
|
||||
fail("Incorrect response parsed")
|
||||
}
|
||||
}
|
||||
|
||||
it must "make a pay request and get the invoice" in {
|
||||
val lnurl = LnURL.fromString(
|
||||
"LNURL1DP68GURN8GHJ7MRWW4EXCTNXD9SHG6NPVCHXXMMD9AKXUATJDSKHQCTE8AEK2UMND9HKU0TPXUCRJVEHXV6RYVEEX5CNSCESXYURSEP5VE3RWCNP89JRJD33XUEXGCFNX5URZCT9XYMRQDRXVVCKZVRPV43KYV3E8YMKZE3H89SNWEVZTNK")
|
||||
|
||||
client.makeRequest(lnurl).flatMap {
|
||||
case pay: LnURLPayResponse =>
|
||||
val amt = pay.minSendable.toLnCurrencyUnit
|
||||
client.getInvoice(pay, amt).map { inv =>
|
||||
assert(inv.amount.contains(amt))
|
||||
}
|
||||
case _: LnURLWithdrawResponse =>
|
||||
fail("Incorrect response parsed")
|
||||
}
|
||||
}
|
||||
|
||||
it must "make a withdrawal request" in {
|
||||
val lnurl = LnURL.fromString(
|
||||
"LNURL1DP68GURN8GHJ7MRWW4EXCTNXD9SHG6NPVCHXXMMD9AKXUATJDSKHW6T5DPJ8YCTH8AEK2UMND9HKU0FJVSCNZDPHVYENVDTPVYCRSVMPXVMRSCEEXGERQVPSXV6X2C3KX9JXZVMZ8PNXZDR9VY6N2DRZVG6RWEPCVYMRZDMRV9SK2D3KV43XVCF58DT")
|
||||
|
||||
client.makeRequest(lnurl).map {
|
||||
case _: LnURLPayResponse =>
|
||||
fail("Incorrect response parsed")
|
||||
case w: LnURLWithdrawResponse =>
|
||||
assert(w.defaultDescription.nonEmpty)
|
||||
assert(w.k1.nonEmpty)
|
||||
assert(w.maxWithdrawable >= MilliSatoshis.zero)
|
||||
assert(w.minWithdrawable >= MilliSatoshis.zero)
|
||||
}
|
||||
}
|
||||
|
||||
it must "make a withdrawal request and do withdrawal" in {
|
||||
val lnurl = LnURL.fromString(
|
||||
"LNURL1DP68GURN8GHJ7MRWW4EXCTNXD9SHG6NPVCHXXMMD9AKXUATJDSKHW6T5DPJ8YCTH8AEK2UMND9HKU0FJVSCNZDPHVYENVDTPVYCRSVMPXVMRSCEEXGERQVPSXV6X2C3KX9JXZVMZ8PNXZDR9VY6N2DRZVG6RWEPCVYMRZDMRV9SK2D3KV43XVCF58DT")
|
||||
|
||||
val inv = LnInvoice.fromString(
|
||||
"lnbc1302470n1p3x3ssapp5axqf6dsusf98895vdhw97rn0szk4z6cxa5hfw3s2q5ksn3575qssdzz2pskjepqw3hjqnmsv4h9xct5wvszsnmjv3jhygzfgsazqem9dejhyctvtan82mny9ycqzpgxqzuysp5q97feeev2tnjsc0qn9kezqlgs8eekwfkxsc28uwxp9elnzkj2n0s9qyyssq02hkrz7dr0adx09t6w2tr9k8nczvq094r7qx297tsdupgeg5t3m8hvmkl7mqhtvx94he3swlg2qzhqk2j39wehcmv9awc06gex82e8qq0u0pm6")
|
||||
|
||||
client.makeRequest(lnurl).flatMap {
|
||||
case _: LnURLPayResponse =>
|
||||
fail("Incorrect response parsed")
|
||||
case w: LnURLWithdrawResponse =>
|
||||
client.doWithdrawal(w, inv).map(bool => assert(bool))
|
||||
}
|
||||
}
|
||||
}
|
31
lnurl-test/src/test/scala/org/bitcoins/lnurl/LnURLTest.scala
Normal file
31
lnurl-test/src/test/scala/org/bitcoins/lnurl/LnURLTest.scala
Normal file
@ -0,0 +1,31 @@
|
||||
package org.bitcoins.lnurl
|
||||
|
||||
import org.bitcoins.testkitcore.util.BitcoinSUnitTest
|
||||
|
||||
import scala.util.{Failure, Success}
|
||||
|
||||
class LnURLTest extends BitcoinSUnitTest {
|
||||
|
||||
it must "correctly encode" in {
|
||||
val url =
|
||||
"https://service.com/api?q=3fc3645b439ce8e7f2553a69e5267081d96dcd340693afabe04be7b0ccd178df"
|
||||
|
||||
val expected =
|
||||
"LNURL1DP68GURN8GHJ7UM9WFMXJCM99E3K7MF0V9CXJ0M385EKVCENXC6R2C35XVUKXEFCV5MKVV34X5EKZD3EV56NYD3HXQURZEPEXEJXXEPNXSCRVWFNV9NXZCN9XQ6XYEFHVGCXXCMYXYMNSERXFQ5FNS"
|
||||
|
||||
assert(LnURL.fromURL(url).toString.toUpperCase == expected)
|
||||
}
|
||||
|
||||
it must "correctly decode" in {
|
||||
val str =
|
||||
"LNURL1DP68GURN8GHJ7UM9WFMXJCM99E3K7MF0V9CXJ0M385EKVCENXC6R2C35XVUKXEFCV5MKVV34X5EKZD3EV56NYD3HXQURZEPEXEJXXEPNXSCRVWFNV9NXZCN9XQ6XYEFHVGCXXCMYXYMNSERXFQ5FNS"
|
||||
|
||||
val expected =
|
||||
"https://service.com/api?q=3fc3645b439ce8e7f2553a69e5267081d96dcd340693afabe04be7b0ccd178df"
|
||||
|
||||
LnURL.decode(str) match {
|
||||
case Failure(exception) => fail(exception)
|
||||
case Success(value) => assert(value == expected)
|
||||
}
|
||||
}
|
||||
}
|
59
lnurl/src/main/scala/org/bitcoins/lnurl/LnURL.scala
Normal file
59
lnurl/src/main/scala/org/bitcoins/lnurl/LnURL.scala
Normal file
@ -0,0 +1,59 @@
|
||||
package org.bitcoins.lnurl
|
||||
|
||||
import org.bitcoins.core.number._
|
||||
import org.bitcoins.core.util._
|
||||
import org.bitcoins.crypto.StringFactory
|
||||
import scodec.bits.ByteVector
|
||||
|
||||
import java.net.URL
|
||||
import scala.util.{Failure, Success, Try}
|
||||
|
||||
class LnURL private (private val str: String) {
|
||||
|
||||
val url: URL = LnURL.decode(str) match {
|
||||
case Failure(_) =>
|
||||
throw new IllegalArgumentException("Invalid LnURL encoding")
|
||||
case Success(value) => new URL(value)
|
||||
}
|
||||
|
||||
override def toString: String = str.toUpperCase
|
||||
}
|
||||
|
||||
object LnURL extends StringFactory[LnURL] {
|
||||
final val lnurlHRP = "lnurl"
|
||||
|
||||
def decode(l: LnURL): Try[String] = decode(l.url)
|
||||
|
||||
def decode(url: URL): Try[String] = decode(url.toString)
|
||||
|
||||
def decode(url: String): Try[String] = {
|
||||
Bech32.splitToHrpAndData(url, Bech32Encoding.Bech32).map {
|
||||
case (hrp, data) =>
|
||||
require(hrp.equalsIgnoreCase(lnurlHRP),
|
||||
s"LNURL must start with $lnurlHRP")
|
||||
val converted = NumberUtil.convertUInt5sToUInt8(data)
|
||||
val bytes = UInt8.toBytes(converted)
|
||||
new String(bytes.toArray, "UTF-8")
|
||||
}
|
||||
}
|
||||
|
||||
override def fromStringT(string: String): Try[LnURL] = {
|
||||
LnURL.decode(string).map(fromURL)
|
||||
}
|
||||
|
||||
override def fromString(string: String): LnURL = {
|
||||
fromStringT(string).get
|
||||
}
|
||||
|
||||
def fromURL(uri: String): LnURL = {
|
||||
val bytes = ByteVector(uri.getBytes)
|
||||
val data = NumberUtil.convertUInt8sToUInt5s(UInt8.toUInt8s(bytes))
|
||||
val dataWithHRP = Bech32.hrpExpand(lnurlHRP) ++ data
|
||||
val checksum = Bech32.createChecksum(dataWithHRP, Bech32Encoding.Bech32)
|
||||
val all: Vector[UInt5] = data ++ checksum
|
||||
val encoding = Bech32.encode5bitToString(all)
|
||||
|
||||
new LnURL(lnurlHRP + Bech32.separator + encoding)
|
||||
}
|
||||
|
||||
}
|
101
lnurl/src/main/scala/org/bitcoins/lnurl/LnURLClient.scala
Normal file
101
lnurl/src/main/scala/org/bitcoins/lnurl/LnURLClient.scala
Normal file
@ -0,0 +1,101 @@
|
||||
package org.bitcoins.lnurl
|
||||
|
||||
import akka.actor.ActorSystem
|
||||
import akka.http.scaladsl.Http
|
||||
import akka.http.scaladsl.client.RequestBuilding.Get
|
||||
import akka.http.scaladsl.model.HttpRequest
|
||||
import akka.util.ByteString
|
||||
import grizzled.slf4j.Logging
|
||||
import org.bitcoins.core.currency._
|
||||
import org.bitcoins.core.protocol.ln.LnInvoice
|
||||
import org.bitcoins.core.protocol.ln.currency._
|
||||
import org.bitcoins.lnurl.json._
|
||||
import org.bitcoins.lnurl.json.LnURLJsonModels._
|
||||
import org.bitcoins.tor._
|
||||
import play.api.libs.json._
|
||||
|
||||
import java.net.{URI, URL}
|
||||
import scala.concurrent._
|
||||
|
||||
class LnURLClient(proxyParams: Option[Socks5ProxyParams])(implicit
|
||||
system: ActorSystem)
|
||||
extends Logging {
|
||||
implicit protected val ec: ExecutionContext = system.dispatcher
|
||||
|
||||
private val http = Http(system)
|
||||
|
||||
private def sendRequest(request: HttpRequest): Future[String] = {
|
||||
val httpConnectionPoolSettings =
|
||||
Socks5ClientTransport.createConnectionPoolSettings(
|
||||
new URI(request.uri.toString),
|
||||
proxyParams)
|
||||
|
||||
http
|
||||
.singleRequest(request, settings = httpConnectionPoolSettings)
|
||||
.flatMap(response =>
|
||||
response.entity.dataBytes
|
||||
.runFold(ByteString.empty)(_ ++ _))
|
||||
.map(payload => payload.decodeString(ByteString.UTF_8))
|
||||
}
|
||||
|
||||
private def sendRequestAndParse[T <: LnURLJsonModel](request: HttpRequest)(
|
||||
implicit reads: Reads[T]): Future[T] = {
|
||||
sendRequest(request)
|
||||
.map { str =>
|
||||
val json = Json.parse(str)
|
||||
json.validate[T] match {
|
||||
case JsSuccess(value, _) => value
|
||||
case JsError(errors) =>
|
||||
json.validate[LnURLStatus] match {
|
||||
case JsSuccess(value, _) =>
|
||||
throw new RuntimeException(
|
||||
value.reason.getOrElse("Error parsing response"))
|
||||
case JsError(_) =>
|
||||
throw new RuntimeException(
|
||||
s"Error parsing json $str, got ${errors.mkString("\n")}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def makeRequest(lnURL: LnURL): Future[LnURLResponse] = {
|
||||
makeRequest(lnURL.url)
|
||||
}
|
||||
|
||||
def makeRequest(url: URL): Future[LnURLResponse] = {
|
||||
makeRequest(url.toString)
|
||||
}
|
||||
|
||||
def makeRequest(str: String): Future[LnURLResponse] = {
|
||||
sendRequestAndParse[LnURLResponse](Get(str))
|
||||
}
|
||||
|
||||
def getInvoice(
|
||||
pay: LnURLPayResponse,
|
||||
amount: LnCurrencyUnit): Future[LnInvoice] = {
|
||||
getInvoice(pay, amount.toSatoshis)
|
||||
}
|
||||
|
||||
def getInvoice(
|
||||
pay: LnURLPayResponse,
|
||||
amount: MilliSatoshis): Future[LnInvoice] = {
|
||||
getInvoice(pay, amount.toSatoshis)
|
||||
}
|
||||
|
||||
def getInvoice(
|
||||
pay: LnURLPayResponse,
|
||||
amount: CurrencyUnit): Future[LnInvoice] = {
|
||||
val msats = MilliSatoshis(amount)
|
||||
val symbol = if (pay.callback.toString.contains("?")) "&" else "?"
|
||||
val url = s"${pay.callback}${symbol}amount=${msats.toLong}"
|
||||
sendRequestAndParse[LnURLPayInvoice](Get(url)).map(_.pr)
|
||||
}
|
||||
|
||||
def doWithdrawal(
|
||||
withdraw: LnURLWithdrawResponse,
|
||||
invoice: LnInvoice): Future[Boolean] = {
|
||||
val symbol = if (withdraw.callback.toString.contains("?")) "&" else "?"
|
||||
val url = s"${withdraw.callback}${symbol}k1=${withdraw.k1}&pr=$invoice"
|
||||
sendRequestAndParse[LnURLStatus](Get(url)).map(_.status.toUpperCase == "OK")
|
||||
}
|
||||
}
|
@ -0,0 +1,91 @@
|
||||
package org.bitcoins.lnurl.json
|
||||
|
||||
import org.bitcoins.core.protocol.ln.currency.MilliSatoshis
|
||||
import org.bitcoins.lnurl.json.LnURLTag._
|
||||
import play.api.libs.json._
|
||||
import org.bitcoins.commons.serializers.JsonReaders._
|
||||
import org.bitcoins.core.protocol.ln.LnInvoice
|
||||
|
||||
import java.net._
|
||||
|
||||
sealed abstract class LnURLJsonModel
|
||||
|
||||
sealed abstract class LnURLResponse extends LnURLJsonModel {
|
||||
def tag: LnURLTag
|
||||
def callback: URL
|
||||
}
|
||||
|
||||
case class LnURLStatus(status: String, reason: Option[String])
|
||||
extends LnURLJsonModel
|
||||
|
||||
object LnURLJsonModels {
|
||||
|
||||
implicit val LnURLStatusReads: Reads[LnURLStatus] = Json.reads[LnURLStatus]
|
||||
|
||||
case class LnURLSuccessAction(
|
||||
tag: SuccessActionTag,
|
||||
message: Option[String],
|
||||
description: Option[String],
|
||||
url: Option[URL],
|
||||
ciphertext: Option[String],
|
||||
iv: Option[String])
|
||||
|
||||
implicit val LnURLSuccessActionReads: Reads[LnURLSuccessAction] =
|
||||
Json.reads[LnURLSuccessAction]
|
||||
|
||||
case class LnURLPayResponse(
|
||||
callback: URL,
|
||||
maxSendable: MilliSatoshis,
|
||||
minSendable: MilliSatoshis,
|
||||
private val metadata: String)
|
||||
extends LnURLResponse {
|
||||
override val tag: LnURLTag = PayRequest
|
||||
lazy val metadataJs: JsValue = Json.parse(metadata)
|
||||
}
|
||||
|
||||
implicit val LnURLPayResponseReads: Reads[LnURLPayResponse] =
|
||||
Json.reads[LnURLPayResponse]
|
||||
|
||||
case class LnURLPayInvoice(
|
||||
pr: LnInvoice,
|
||||
successAction: Option[LnURLSuccessAction])
|
||||
extends LnURLJsonModel
|
||||
|
||||
implicit val LnURLPayInvoiceReads: Reads[LnURLPayInvoice] =
|
||||
Json.reads[LnURLPayInvoice]
|
||||
|
||||
case class LnURLWithdrawResponse(
|
||||
callback: URL,
|
||||
k1: String,
|
||||
defaultDescription: String,
|
||||
minWithdrawable: MilliSatoshis,
|
||||
maxWithdrawable: MilliSatoshis
|
||||
) extends LnURLResponse {
|
||||
override val tag: LnURLTag = WithdrawRequest
|
||||
}
|
||||
|
||||
implicit val LnURLWithdrawResponseReads: Reads[LnURLWithdrawResponse] =
|
||||
Json.reads[LnURLWithdrawResponse]
|
||||
|
||||
implicit val LnURLResponseReads: Reads[LnURLResponse] = {
|
||||
case other @ (JsNull | _: JsBoolean | JsNumber(_) | JsString(_) | JsArray(
|
||||
_)) =>
|
||||
throw new IllegalArgumentException(s"Expected JsObject, got $other")
|
||||
case obj: JsObject =>
|
||||
obj.value.get("tag") match {
|
||||
case None =>
|
||||
throw new RuntimeException(s"Error parsing json, no tag, got $obj")
|
||||
case Some(tagJs) =>
|
||||
tagJs.validate[LnURLTag] match {
|
||||
case JsError(errors) =>
|
||||
throw new IllegalArgumentException(
|
||||
s"Invalid json, got $obj, errors ${errors.mkString("\n")}")
|
||||
case JsSuccess(tag, _) =>
|
||||
tag match {
|
||||
case PayRequest => obj.validate[LnURLPayResponse]
|
||||
case WithdrawRequest => obj.validate[LnURLWithdrawResponse]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
57
lnurl/src/main/scala/org/bitcoins/lnurl/json/LnURLTag.scala
Normal file
57
lnurl/src/main/scala/org/bitcoins/lnurl/json/LnURLTag.scala
Normal file
@ -0,0 +1,57 @@
|
||||
package org.bitcoins.lnurl.json
|
||||
|
||||
import org.bitcoins.commons.serializers.SerializerUtil
|
||||
import org.bitcoins.crypto.StringFactory
|
||||
import play.api.libs.json._
|
||||
|
||||
sealed abstract class LnURLTag(override val toString: String)
|
||||
|
||||
object LnURLTag extends StringFactory[LnURLTag] {
|
||||
|
||||
case object WithdrawRequest extends LnURLTag("withdrawRequest")
|
||||
case object PayRequest extends LnURLTag("payRequest")
|
||||
|
||||
val all: Vector[LnURLTag] = Vector(WithdrawRequest, PayRequest)
|
||||
|
||||
override def fromStringOpt(string: String): Option[LnURLTag] = {
|
||||
all.find(_.toString == string)
|
||||
}
|
||||
|
||||
override def fromString(string: String): LnURLTag = {
|
||||
fromStringOpt(string).getOrElse(
|
||||
sys.error(s"Could not find a LnURLTag for string $string"))
|
||||
}
|
||||
|
||||
implicit val LnURLTagTagReads: Reads[LnURLTag] = (json: JsValue) =>
|
||||
SerializerUtil.processJsStringOpt[LnURLTag](fromStringOpt)(json)
|
||||
|
||||
implicit val LnURLTagTagWrites: Writes[LnURLTag] = (tag: LnURLTag) =>
|
||||
JsString(tag.toString)
|
||||
}
|
||||
|
||||
sealed abstract class SuccessActionTag(override val toString: String)
|
||||
|
||||
object SuccessActionTag extends StringFactory[SuccessActionTag] {
|
||||
|
||||
case object Message extends SuccessActionTag("message")
|
||||
case object URL extends SuccessActionTag("url")
|
||||
case object AES extends SuccessActionTag("aes")
|
||||
|
||||
val all: Vector[SuccessActionTag] = Vector(Message, URL, AES)
|
||||
|
||||
override def fromStringOpt(string: String): Option[SuccessActionTag] = {
|
||||
all.find(_.toString == string)
|
||||
}
|
||||
|
||||
override def fromString(string: String): SuccessActionTag = {
|
||||
fromStringOpt(string).getOrElse(
|
||||
sys.error(s"Could not find a SuccessActionTag for string $string"))
|
||||
}
|
||||
|
||||
implicit val SuccessActionTagReads: Reads[SuccessActionTag] =
|
||||
(json: JsValue) =>
|
||||
SerializerUtil.processJsStringOpt[SuccessActionTag](fromStringOpt)(json)
|
||||
|
||||
implicit val SuccessActionTagWrites: Writes[SuccessActionTag] =
|
||||
(tag: SuccessActionTag) => JsString(tag.toString)
|
||||
}
|
Loading…
Reference in New Issue
Block a user