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

fr.acinq.eclair.payment.PaymentRequest.scala Maven / Gradle / Ivy

The newest version!
/*
 * Copyright 2019 ACINQ SAS
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package fr.acinq.eclair.payment

import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey}
import fr.acinq.bitcoin.{Base58, Base58Check, Bech32, Block, ByteVector32, ByteVector64, Crypto}
import fr.acinq.eclair.Features.{PaymentSecret => PaymentSecretF, _}
import fr.acinq.eclair.payment.PaymentRequest._
import fr.acinq.eclair.{CltvExpiryDelta, FeatureSupport, LongToBtcAmount, MilliSatoshi, ShortChannelId, randomBytes32}
import scodec.Codec
import scodec.bits.{BitVector, ByteOrdering, ByteVector}
import scodec.codecs.{list, ubyte}

import scala.compat.Platform
import scala.concurrent.duration._
import scala.util.Try

/**
  * Lightning Payment Request
  * see https://github.com/lightningnetwork/lightning-rfc/blob/master/11-payment-encoding.md
  *
  * @param prefix    currency prefix; lngrs for groestlcoin, lntgrs for groestlcoin testnet
 * @param amount    amount to pay (empty string means no amount is specified)
 * @param timestamp request timestamp (UNIX format)
 * @param nodeId    id of the node emitting the payment request
 * @param tags      payment tags; must include a single PaymentHash tag and a single PaymentSecret tag.
 * @param signature request signature that will be checked against node id
 */

case class PaymentRequest(prefix: String, amount: Option[MilliSatoshi], timestamp: Long, nodeId: PublicKey, tags: List[PaymentRequest.TaggedField], signature: ByteVector) {

  amount.foreach(a => require(a > 0.msat, s"amount is not valid"))
  require(tags.collect { case _: PaymentRequest.PaymentHash => }.size == 1, "there must be exactly one payment hash tag")
  require(tags.collect { case PaymentRequest.Description(_) | PaymentRequest.DescriptionHash(_) => }.size == 1, "there must be exactly one description tag or one description hash tag")
  private val featuresErr = validateFeatureGraph(features.bitmask)
  require(featuresErr.isEmpty, featuresErr.map(_.message))
  if (features.allowPaymentSecret) {
    require(tags.collect { case _: PaymentRequest.PaymentSecret => }.size == 1, "there must be exactly one payment secret tag when feature bit is set")
  }

  /**
   * @return the payment hash
   */
  lazy val paymentHash = tags.collectFirst { case p: PaymentRequest.PaymentHash => p.hash }.get

  /**
   * @return the payment secret
   */
  lazy val paymentSecret = tags.collectFirst { case p: PaymentRequest.PaymentSecret => p.secret }

  /**
   * @return the description of the payment, or its hash
   */
  lazy val description: Either[String, ByteVector32] = tags.collectFirst {
    case PaymentRequest.Description(d) => Left(d)
    case PaymentRequest.DescriptionHash(h) => Right(h)
  }.get

  /**
   * @return the fallback address if any. It could be a script address, pubkey address, ..
   */
  def fallbackAddress(): Option[String] = tags.collectFirst {
    case f: PaymentRequest.FallbackAddress => PaymentRequest.FallbackAddress.toAddress(f, prefix)
  }

  lazy val routingInfo: Seq[Seq[ExtraHop]] = tags.collect { case t: RoutingInfo => t.path }

  lazy val expiry: Option[Long] = tags.collectFirst {
    case expiry: PaymentRequest.Expiry => expiry.toLong
  }

  lazy val minFinalCltvExpiryDelta: Option[CltvExpiryDelta] = tags.collectFirst {
    case cltvExpiry: PaymentRequest.MinFinalCltvExpiry => cltvExpiry.toCltvExpiryDelta
  }

  lazy val features: Features = tags.collectFirst { case f: Features => f }.getOrElse(Features(BitVector.empty))

  def isExpired: Boolean = expiry match {
    case Some(expiryTime) => timestamp + expiryTime <= Platform.currentTime.milliseconds.toSeconds
    case None => timestamp + DEFAULT_EXPIRY_SECONDS <= Platform.currentTime.milliseconds.toSeconds
  }

  /**
   * @return the hash of this payment request
   */
  def hash: ByteVector32 = {
    val hrp = s"$prefix${Amount.encode(amount)}".getBytes("UTF-8")
    val data = Bolt11Data(timestamp, tags, ByteVector.fill(65)(0)) // fake sig that we are going to strip next
    val bin = Codecs.bolt11DataCodec.encode(data).require
    val message = ByteVector.view(hrp) ++ bin.dropRight(520).toByteVector
    Crypto.sha256(message)
  }

  /**
   * @param priv private key
   * @return a signed payment request
   */
  def sign(priv: PrivateKey): PaymentRequest = {
    val sig64 = Crypto.sign(hash, priv)
    val (pub1, _) = Crypto.recoverPublicKey(sig64, hash)
    val recid = if (nodeId == pub1) 0.toByte else 1.toByte
    val signature = sig64 :+ recid
    this.copy(signature = signature)
  }
}

object PaymentRequest {

  val DEFAULT_EXPIRY_SECONDS = 3600

  val prefixes = Map(
    Block.RegtestGenesisBlock.hash -> "lngrsrt",
    Block.TestnetGenesisBlock.hash -> "lntgrs",
    Block.LivenetGenesisBlock.hash -> "lngrs")

  def apply(chainHash: ByteVector32, amount: Option[MilliSatoshi], paymentHash: ByteVector32, privateKey: PrivateKey,
            description: String, fallbackAddress: Option[String] = None, expirySeconds: Option[Long] = None,
            extraHops: List[List[ExtraHop]] = Nil, timestamp: Long = System.currentTimeMillis() / 1000L,
            features: Option[Features] = Some(Features(VariableLengthOnion.optional, PaymentSecretF.optional))): PaymentRequest = {

    val prefix = prefixes(chainHash)
    val tags = {
      val defaultTags = List(
        Some(PaymentHash(paymentHash)),
        Some(Description(description)),
        fallbackAddress.map(FallbackAddress(_)),
        expirySeconds.map(Expiry(_)),
        features).flatten
      val paymentSecretTag = if (features.exists(_.allowPaymentSecret)) PaymentSecret(randomBytes32) :: Nil else Nil
      val routingInfoTags = extraHops.map(RoutingInfo)
      defaultTags ++ paymentSecretTag ++ routingInfoTags
    }

    PaymentRequest(
      prefix = prefix,
      amount = amount,
      timestamp = timestamp,
      nodeId = privateKey.publicKey,
      tags = tags,
      signature = ByteVector.empty)
      .sign(privateKey)
  }

  case class Bolt11Data(timestamp: Long, taggedFields: List[TaggedField], signature: ByteVector)

  sealed trait TaggedField

  sealed trait UnknownTaggedField extends TaggedField

  // @formatter:off
  case class UnknownTag0(data: BitVector) extends UnknownTaggedField
  case class UnknownTag1(data: BitVector) extends UnknownTaggedField
  case class UnknownTag2(data: BitVector) extends UnknownTaggedField
  case class UnknownTag4(data: BitVector) extends UnknownTaggedField
  case class UnknownTag7(data: BitVector) extends UnknownTaggedField
  case class UnknownTag8(data: BitVector) extends UnknownTaggedField
  case class UnknownTag10(data: BitVector) extends UnknownTaggedField
  case class UnknownTag11(data: BitVector) extends UnknownTaggedField
  case class UnknownTag12(data: BitVector) extends UnknownTaggedField
  case class UnknownTag14(data: BitVector) extends UnknownTaggedField
  case class UnknownTag15(data: BitVector) extends UnknownTaggedField
  case class UnknownTag17(data: BitVector) extends UnknownTaggedField
  case class UnknownTag18(data: BitVector) extends UnknownTaggedField
  case class UnknownTag19(data: BitVector) extends UnknownTaggedField
  case class UnknownTag20(data: BitVector) extends UnknownTaggedField
  case class UnknownTag21(data: BitVector) extends UnknownTaggedField
  case class UnknownTag22(data: BitVector) extends UnknownTaggedField
  case class UnknownTag25(data: BitVector) extends UnknownTaggedField
  case class UnknownTag26(data: BitVector) extends UnknownTaggedField
  case class UnknownTag27(data: BitVector) extends UnknownTaggedField
  case class UnknownTag28(data: BitVector) extends UnknownTaggedField
  case class UnknownTag29(data: BitVector) extends UnknownTaggedField
  case class UnknownTag30(data: BitVector) extends UnknownTaggedField
  case class UnknownTag31(data: BitVector) extends UnknownTaggedField
  // @formatter:on

  /**
   * Payment Hash
   *
   * @param hash payment hash
   */
  case class PaymentHash(hash: ByteVector32) extends TaggedField

  /**
   * Payment secret. This is currently random bytes used to protect against probing from the next-to-last node.
   *
   * @param secret payment secret
   */
  case class PaymentSecret(secret: ByteVector32) extends TaggedField

  /**
   * Description
   *
   * @param description a free-format string that will be included in the payment request
   */
  case class Description(description: String) extends TaggedField

  /**
   * Hash
   *
   * @param hash hash that will be included in the payment request, and can be checked against the hash of a
   *             long description, an invoice, ...
   */
  case class DescriptionHash(hash: ByteVector32) extends TaggedField

  /**
   * Fallback Payment that specifies a fallback payment address to be used if LN payment cannot be processed
   */
  case class FallbackAddress(version: Byte, data: ByteVector) extends TaggedField

  object FallbackAddress {
    /**
     * @param address valid base58 or bech32 address
     * @return a FallbackAddressTag instance
     */
    def apply(address: String): FallbackAddress = {
      Try(fromBase58Address(address)).orElse(Try(fromBech32Address(address))).get
    }

    def fromBase58Address(address: String): FallbackAddress = {
      val (prefix, hash) = Base58Check.decode(address)
      prefix match {
        case Base58.Prefix.PubkeyAddress => FallbackAddress(17.toByte, hash)
        case Base58.Prefix.PubkeyAddressTestnet => FallbackAddress(17.toByte, hash)
        case Base58.Prefix.ScriptAddress => FallbackAddress(18.toByte, hash)
        case Base58.Prefix.ScriptAddressTestnet => FallbackAddress(18.toByte, hash)
      }
    }

    def fromBech32Address(address: String): FallbackAddress = {
      val (_, version, hash) = Bech32.decodeWitnessAddress(address)
      FallbackAddress(version, hash)
    }

    def toAddress(f: FallbackAddress, prefix: String): String = {
      import f.data
      f.version match {
        case 17 if prefix == "lngrs" => Base58Check.encode(Base58.Prefix.PubkeyAddress, data)
        case 18 if prefix == "lngrs" => Base58Check.encode(Base58.Prefix.ScriptAddress, data)
        case 17 if prefix == "lntgrs" || prefix == "lngrsrt" => Base58Check.encode(Base58.Prefix.PubkeyAddressTestnet, data)
        case 18 if prefix == "lntgrs" || prefix == "lngrsrt" => Base58Check.encode(Base58.Prefix.ScriptAddressTestnet, data)
        case version if prefix == "lngrs" => Bech32.encodeWitnessAddress("grs", version, data)
        case version if prefix == "lntgrs" => Bech32.encodeWitnessAddress("tgrs", version, data)
        case version if prefix == "lngrsrt" => Bech32.encodeWitnessAddress("grsrt", version, data)
      }
    }
  }

  /**
   * This returns a bitvector with the minimum size necessary to encode the long, left padded
   * to have a length (in bits) multiples of 5
   */
  def long2bits(l: Long) = {
    val bin = BitVector.fromLong(l)
    var highest = -1
    for (i <- 0 until bin.size.toInt) {
      if (highest == -1 && bin(i)) highest = i
    }
    val nonPadded = if (highest == -1) BitVector.empty else bin.drop(highest)
    nonPadded.size % 5 match {
      case 0 => nonPadded
      case remaining => BitVector.fill(5 - remaining)(high = false) ++ nonPadded
    }
  }

  /**
   * Extra hop contained in RoutingInfoTag
   *
   * @param nodeId                    start of the channel
   * @param shortChannelId            channel id
   * @param feeBase                   node fixed fee
   * @param feeProportionalMillionths node proportional fee
   * @param cltvExpiryDelta           node cltv expiry delta
   */
  case class ExtraHop(nodeId: PublicKey, shortChannelId: ShortChannelId, feeBase: MilliSatoshi, feeProportionalMillionths: Long, cltvExpiryDelta: CltvExpiryDelta)

  /**
   * Routing Info
   *
   * @param path one or more entries containing extra routing information for a private route
   */
  case class RoutingInfo(path: List[ExtraHop]) extends TaggedField

  /**
   * Expiry Date
   */
  case class Expiry(bin: BitVector) extends TaggedField {
    def toLong: Long = bin.toLong(signed = false)
  }

  object Expiry {
    /**
     * @param seconds expiry data for this payment request
     */
    def apply(seconds: Long): Expiry = Expiry(long2bits(seconds))
  }

  /**
   * Min final CLTV expiry
   */
  case class MinFinalCltvExpiry(bin: BitVector) extends TaggedField {
    def toCltvExpiryDelta = CltvExpiryDelta(bin.toInt(signed = false))
  }

  object MinFinalCltvExpiry {
    /**
     * Min final CLTV expiry
     *
     * @param blocks min final cltv expiry, in blocks
     */
    def apply(blocks: Long): MinFinalCltvExpiry = MinFinalCltvExpiry(long2bits(blocks))
  }

  /**
   * Features supported or required for receiving this payment.
   */
  case class Features(bitmask: BitVector) extends TaggedField {
    lazy val supported: Boolean = areSupported(bitmask)
    lazy val allowMultiPart: Boolean = hasFeature(bitmask, BasicMultiPartPayment)
    lazy val allowPaymentSecret: Boolean = hasFeature(bitmask, PaymentSecretF)
    lazy val requirePaymentSecret: Boolean = hasFeature(bitmask, PaymentSecretF, Some(FeatureSupport.Mandatory))
    lazy val allowTrampoline: Boolean = hasFeature(bitmask, TrampolinePayment)

    override def toString: String = s"Features(${bitmask.toBin})"

    // When converting from BitVector to ByteVector, scodec pads right instead of left so we have to do this ourselves.
    // We also want to enforce a minimal encoding of the feature bytes.
    def toByteVector: ByteVector = {
      val pad = if (bitmask.length % 8 == 0) 0 else 8 - bitmask.length % 8
      bitmask.padLeft(bitmask.length + pad).bytes.dropWhile(_ == 0)
    }
  }

  object Features {
    def apply(features: Int*): Features = Features(long2bits(features.foldLeft(0L) {
      case (current, feature) => current + (1L << feature)
    }))
  }

  object Codecs {

    import fr.acinq.eclair.wire.CommonCodecs._
    import scodec.bits.BitVector
    import scodec.codecs._
    import scodec.{Attempt, Codec, DecodeResult}

    val extraHopCodec: Codec[ExtraHop] = (
      ("nodeId" | publicKey) ::
        ("shortChannelId" | shortchannelid) ::
        ("fee_base_msat" | millisatoshi32) ::
        ("fee_proportional_millionth" | uint32) ::
        ("cltv_expiry_delta" | cltvExpiryDelta)
      ).as[ExtraHop]

    val extraHopsLengthCodec = Codec[Int](
      (_: Int) => Attempt.successful(BitVector.empty), // we don't encode the length
      (wire: BitVector) => Attempt.successful(DecodeResult(wire.size.toInt / 408, wire)) // we infer the number of items by the size of the data
    )

    def alignedBytesCodec[A](valueCodec: Codec[A]): Codec[A] = Codec[A](
      (value: A) => valueCodec.encode(value),
      (wire: BitVector) => (limitedSizeBits(wire.size - wire.size % 8, valueCodec) ~ constant(BitVector.fill(wire.size % 8)(high = false))).map(_._1).decode(wire) // the 'constant' codec ensures that padding is zero
    )

    val dataLengthCodec: Codec[Long] = uint(10).xmap(_ * 5, s => (s / 5 + (if (s % 5 == 0) 0 else 1)).toInt)

    def dataCodec[A](valueCodec: Codec[A]): Codec[A] = paddedVarAlignedBits(dataLengthCodec, valueCodec, multipleForPadding = 5)

    val taggedFieldCodec: Codec[TaggedField] = discriminated[TaggedField].by(ubyte(5))
      .typecase(0, dataCodec(bits).as[UnknownTag0])
      .typecase(1, dataCodec(bytes32).as[PaymentHash])
      .typecase(2, dataCodec(bits).as[UnknownTag2])
      .typecase(3, dataCodec(listOfN(extraHopsLengthCodec, extraHopCodec)).as[RoutingInfo])
      .typecase(4, dataCodec(bits).as[UnknownTag4])
      .typecase(5, dataCodec(bits).as[Features])
      .typecase(6, dataCodec(bits).as[Expiry])
      .typecase(7, dataCodec(bits).as[UnknownTag7])
      .typecase(8, dataCodec(bits).as[UnknownTag8])
      .typecase(9, dataCodec(ubyte(5) :: alignedBytesCodec(bytes)).as[FallbackAddress])
      .typecase(10, dataCodec(bits).as[UnknownTag10])
      .typecase(11, dataCodec(bits).as[UnknownTag11])
      .typecase(12, dataCodec(bits).as[UnknownTag12])
      .typecase(13, dataCodec(alignedBytesCodec(utf8)).as[Description])
      .typecase(14, dataCodec(bits).as[UnknownTag14])
      .typecase(15, dataCodec(bits).as[UnknownTag15])
      .typecase(16, dataCodec(bytes32).as[PaymentSecret])
      .typecase(17, dataCodec(bits).as[UnknownTag17])
      .typecase(18, dataCodec(bits).as[UnknownTag18])
      .typecase(19, dataCodec(bits).as[UnknownTag19])
      .typecase(20, dataCodec(bits).as[UnknownTag20])
      .typecase(21, dataCodec(bits).as[UnknownTag21])
      .typecase(22, dataCodec(bits).as[UnknownTag22])
      .typecase(23, dataCodec(bytes32).as[DescriptionHash])
      .typecase(24, dataCodec(bits).as[MinFinalCltvExpiry])
      .typecase(25, dataCodec(bits).as[UnknownTag25])
      .typecase(26, dataCodec(bits).as[UnknownTag26])
      .typecase(27, dataCodec(bits).as[UnknownTag27])
      .typecase(28, dataCodec(bits).as[UnknownTag28])
      .typecase(29, dataCodec(bits).as[UnknownTag29])
      .typecase(30, dataCodec(bits).as[UnknownTag30])
      .typecase(31, dataCodec(bits).as[UnknownTag31])

    def fixedSizeTrailingCodec[A](codec: Codec[A], size: Int): Codec[A] = Codec[A](
      (data: A) => codec.encode(data),
      (wire: BitVector) => {
        val (head, tail) = wire.splitAt(wire.size - size)
        codec.decode(head).map(result => result.copy(remainder = tail))
      }
    )

    val bolt11DataCodec: Codec[Bolt11Data] = (
      ("timestamp" | ulong(35)) ::
        ("taggedFields" | fixedSizeTrailingCodec(list(taggedFieldCodec), 520)) ::
        ("signature" | bytes(65))
      ).as[Bolt11Data]
  }

  object Amount {

    /**
     * @return the unit allowing for the shortest representation possible
     */
    def unit(amount: MilliSatoshi): Char = amount.toLong * 10 match { // 1 milli-satoshis == 10 pico-bitcoin
      case pico if pico % 1000 > 0 => 'p'
      case pico if pico % 1000000 > 0 => 'n'
      case pico if pico % 1000000000 > 0 => 'u'
      case _ => 'm'
    }

    def decode(input: String): Option[MilliSatoshi] =
      input match {
        case "" => None
        case a if a.last == 'p' => Some(MilliSatoshi(a.dropRight(1).toLong / 10L)) // 1 pico-bitcoin == 10 milli-satoshis
        case a if a.last == 'n' => Some(MilliSatoshi(a.dropRight(1).toLong * 100L))
        case a if a.last == 'u' => Some(MilliSatoshi(a.dropRight(1).toLong * 100000L))
        case a if a.last == 'm' => Some(MilliSatoshi(a.dropRight(1).toLong * 100000000L))
        case a => Some(MilliSatoshi(a.toLong * 100000000000L))
      }

    def encode(amount: Option[MilliSatoshi]): String = {
      amount match {
        case None => ""
        case Some(amt) if unit(amt) == 'p' => s"${amt.toLong * 10L}p" // 1 pico-bitcoin == 10 milli-satoshis
        case Some(amt) if unit(amt) == 'n' => s"${amt.toLong / 100L}n"
        case Some(amt) if unit(amt) == 'u' => s"${amt.toLong / 100000L}u"
        case Some(amt) if unit(amt) == 'm' => s"${amt.toLong / 100000000L}m"
      }
    }
  }

  // char -> 5 bits value
  val charToint5: Map[Char, BitVector] = Bech32.alphabet.zipWithIndex.toMap.mapValues(BitVector.fromInt(_, size = 5, ordering = ByteOrdering.BigEndian))

  // TODO: could be optimized by preallocating the resulting buffer
  def string2Bits(data: String): BitVector = data.map(charToint5).foldLeft(BitVector.empty)(_ ++ _)

  val eight2fiveCodec: Codec[List[Byte]] = list(ubyte(5))

  /**
   * @param input bech32-encoded payment request
   * @return a payment request
   */
  def read(input: String): PaymentRequest = {
    // used only for data validation
    Bech32.decode(input)
    val lowercaseInput = input.toLowerCase
    val separatorIndex = lowercaseInput.lastIndexOf('1')
    val hrp = lowercaseInput.take(separatorIndex)
    val prefix: String = prefixes.values.find(prefix => hrp.startsWith(prefix)).getOrElse(throw new RuntimeException("unknown prefix"))
    val data = string2Bits(lowercaseInput.slice(separatorIndex + 1, lowercaseInput.length - 6)) // 6 == checksum size
    val bolt11Data = Codecs.bolt11DataCodec.decode(data).require.value
    val signature = ByteVector64(bolt11Data.signature.take(64))
    val message: ByteVector = ByteVector.view(hrp.getBytes) ++ data.dropRight(520).toByteVector // we drop the sig bytes
    val recid = bolt11Data.signature.last
    val pub = Crypto.recoverPublicKey(signature, Crypto.sha256(message), recid)
    // README: since we use pubkey recovery to compute the node id from the message and signature, we don't check the signature.
    // If instead we read the node id from the `n` field (which nobody uses afaict) then we would have to check the signature.
    val amount_opt = Amount.decode(hrp.drop(prefix.length))
    PaymentRequest(
      prefix = prefix,
      amount = amount_opt,
      timestamp = bolt11Data.timestamp,
      nodeId = pub,
      tags = bolt11Data.taggedFields,
      signature = bolt11Data.signature)
  }

  private def readBoltData(input: String): Bolt11Data = {
    val lowercaseInput = input.toLowerCase
    val separatorIndex = lowercaseInput.lastIndexOf('1')
    val hrp = lowercaseInput.take(separatorIndex)
    if (!prefixes.values.exists(prefix => hrp.startsWith(prefix))) throw new RuntimeException("unknown prefix")
    val data = string2Bits(lowercaseInput.slice(separatorIndex + 1, lowercaseInput.length - 6)) // 6 == checksum size
    Codecs.bolt11DataCodec.decode(data).require.value
  }

  /**
   * Extracts the description from a serialized payment request that is **expected to be valid**.
   * Throws an error if the payment request is not valid.
   *
   * @param input valid serialized payment request
   * @return description as a String. If the description is a hash, returns the hash value as a String.
   */
  def fastReadDescription(input: String): String = {
    readBoltData(input).taggedFields.collectFirst {
      case PaymentRequest.Description(d) => d
      case PaymentRequest.DescriptionHash(h) => h.toString()
    }.get
  }

  /**
   * Checks if a serialized payment request is expired. Timestamp is compared to the System's current time.
   *
   * @param input valid serialized payment request
   * @return true if the payment request has expired, false otherwise.
   */
  def fastHasExpired(input: String): Boolean = {
    val bolt11Data = readBoltData(input)
    val expiry_opt = bolt11Data.taggedFields.collectFirst {
      case p: PaymentRequest.Expiry => p
    }
    val timestamp = bolt11Data.timestamp
    expiry_opt match {
      case Some(expiry) => timestamp + expiry.toLong <= Platform.currentTime.milliseconds.toSeconds
      case None => timestamp + DEFAULT_EXPIRY_SECONDS <= Platform.currentTime.milliseconds.toSeconds
    }
  }

  /**
   * @param pr payment request
   * @return a bech32-encoded payment request
   */
  def write(pr: PaymentRequest): String = {
    // currency unit is Satoshi, but we compute amounts in Millisatoshis
    val hramount = Amount.encode(pr.amount)
    val hrp = s"${pr.prefix}$hramount"
    val data = Codecs.bolt11DataCodec.encode(Bolt11Data(pr.timestamp, pr.tags, pr.signature)).require
    val int5s = eight2fiveCodec.decode(data).require.value
    Bech32.encode(hrp, int5s.toArray)
  }
}





© 2015 - 2024 Weber Informatics LLC | Privacy Policy