1
0
Fork 0
mirror of https://github.com/ACINQ/eclair.git synced 2025-02-23 06:35:11 +01:00

Handle invoice with amounts larger than 1btc (#2684)

For amounts that are multiples of 1btc, we shouldn't use a multiplier
and should directly encode this amount.
This commit is contained in:
Bastien Teinturier 2023-06-06 10:44:53 +02:00 committed by GitHub
parent 5ab84712bf
commit 42dfa9f535
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 48 additions and 28 deletions

View file

@ -477,22 +477,23 @@ object Bolt11Invoice {
/**
* @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 unit(amount: MilliSatoshi): Option[Char] = amount.toLong * 10 match { // 1 milli-satoshis == 10 pico-bitcoin
case pico if pico % 1_000 > 0 => Some('p')
case pico if pico % 1_000_000 > 0 => Some('n')
case pico if pico % 1_000_000_000 > 0 => Some('u')
case pico if pico % 1_000_000_000_000L > 0 => Some('m')
case _ => None
}
def decode(input: String): Try[Option[MilliSatoshi]] =
(input match {
case "" => Success(None)
case a if a.last == 'p' && a.dropRight(1).last != '0' => Failure(new IllegalArgumentException("invalid sub-millisatoshi precision"))
case a if a.last == 'p' => Success(Some(MilliSatoshi(a.dropRight(1).toLong / 10L))) // 1 pico-bitcoin == 0.1 milli-satoshis
case a if a.last == 'n' => Success(Some(MilliSatoshi(a.dropRight(1).toLong * 100L)))
case a if a.last == 'u' => Success(Some(MilliSatoshi(a.dropRight(1).toLong * 100000L)))
case a if a.last == 'm' => Success(Some(MilliSatoshi(a.dropRight(1).toLong * 100000000L)))
case a => Success(Some(MilliSatoshi(a.toLong * 100000000000L)))
case a if a.last == 'p' => Success(Some(MilliSatoshi(a.dropRight(1).toLong / 10))) // 1 pico-bitcoin == 0.1 milli-satoshis
case a if a.last == 'n' => Success(Some(MilliSatoshi(a.dropRight(1).toLong * 100)))
case a if a.last == 'u' => Success(Some(MilliSatoshi(a.dropRight(1).toLong * 100_000)))
case a if a.last == 'm' => Success(Some(MilliSatoshi(a.dropRight(1).toLong * 100_000_000)))
case a => Success(Some(MilliSatoshi(a.toLong * 100_000_000_000L)))
}).map {
case None => None
case Some(MilliSatoshi(0)) => None
@ -500,12 +501,15 @@ object Bolt11Invoice {
}
def encode(amount: Option[MilliSatoshi]): String = {
(amount: @unchecked) match {
amount match {
case None => ""
case Some(amt) if unit(amt) == 'p' => s"${amt.toLong * 10L}p" // 1 pico-bitcoin == 0.1 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"
case Some(amt) => unit(amt) match {
case Some('p') => s"${amt.toLong * 10}p" // 1 pico-bitcoin == 0.1 milli-satoshis
case Some('n') => s"${amt.toLong / 100}n"
case Some('u') => s"${amt.toLong / 100_000}u"
case Some('m') => s"${amt.toLong / 100_000_000}m"
case _ => s"${amt.toLong / 100_000_000_000L}"
}
}
}
}

View file

@ -83,17 +83,20 @@ class Bolt11InvoiceSpec extends AnyFunSuite {
}
test("check minimal unit is used") {
assert('p' == Amount.unit(1 msat))
assert('p' == Amount.unit(99 msat))
assert('n' == Amount.unit(100 msat))
assert('p' == Amount.unit(101 msat))
assert('n' == Amount.unit((1 sat).toMilliSatoshi))
assert('u' == Amount.unit((100 sat).toMilliSatoshi))
assert('n' == Amount.unit((101 sat).toMilliSatoshi))
assert('u' == Amount.unit((1155400 sat).toMilliSatoshi))
assert('m' == Amount.unit((1 millibtc).toMilliSatoshi))
assert('m' == Amount.unit((10 millibtc).toMilliSatoshi))
assert('m' == Amount.unit((1 btc).toMilliSatoshi))
assert(Amount.unit(1 msat).contains('p'))
assert(Amount.unit(99 msat).contains('p'))
assert(Amount.unit(100 msat).contains('n'))
assert(Amount.unit(101 msat).contains('p'))
assert(Amount.unit((1 sat).toMilliSatoshi).contains('n'))
assert(Amount.unit((100 sat).toMilliSatoshi).contains('u'))
assert(Amount.unit((101 sat).toMilliSatoshi).contains('n'))
assert(Amount.unit((1155400 sat).toMilliSatoshi).contains('u'))
assert(Amount.unit((1 millibtc).toMilliSatoshi).contains('m'))
assert(Amount.unit((10 millibtc).toMilliSatoshi).contains('m'))
assert(Amount.unit((1 btc).toMilliSatoshi).isEmpty)
assert(Amount.unit((1.1 btc).toMilliSatoshi).contains('m'))
assert(Amount.unit((2 btc).toMilliSatoshi).isEmpty)
assert(Amount.unit((10 btc).toMilliSatoshi).isEmpty)
}
test("decode empty amount") {
@ -470,13 +473,26 @@ class Bolt11InvoiceSpec extends AnyFunSuite {
assert(Bolt11Invoice.fromString(input.toUpperCase()).get.toString == input)
}
test("Pay 1 BTC without multiplier") {
test("Pay 1 BTC with multiplier") {
val ref = "lnbc1000m1pdkmqhusp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygspp5n2ees808r98m0rh4472yyth0c5fptzcxmexcjznrzmq8xald0cgqdqsf4ujqarfwqsxymmccqp2pv37ezvhth477nu0yhhjlcry372eef57qmldhreqnr0kx82jkupp3n7nw42u3kdyyjskdr8jhjy2vugr3skdmy8ersft36969xplkxsp2v7c58"
val Success(invoice) = Bolt11Invoice.fromString(ref)
assert(invoice.amount_opt.contains(100000000000L msat))
assert(invoice.amount_opt.contains(100_000_000_000L msat))
assert(features2bits(invoice.features) == BitVector.empty)
}
test("Pay 1 BTC without multiplier") {
val testCases = Seq(
100_000_000_000L.msat -> "lnbcrt11pj8wdh7sp5p2052f28az75s3eauqjskcwrzrjujf7rfqspsvk6hgppywytrdzspp5670t00mwakdy0l5w3lnw4rhdgnv4ctep974am6jp0zma627fhdfsdqqxqyjw5qcqp29qyysgqh2ce2cmptj33l35a9pt2l603aa34jpj8p35s302l0lhuujmtmkghrmkadv456h3rpsxjpschnpt5ugzltqsjtauvnfy799aufapav6gp202th5",
100_000_000_000_000L.msat -> "lnbcrt10001pj8wd3rsp5cv2vayxnm7d4783r0477rstzpkl7n4ftmalgu9v8akzf0nhqrs3qpp5vednenalh0v6gzxpzrdxf9cepv4274vc0tax5389cjq0zv9qvs9sdqqxqyjw5qcqp29qyysgqk5f8um72jlnw9unjltdgxw9e2fvec0cxq05tcwuen2jpu42q4p9pt2djk2ysu62nkpg49km59wrexm0wt3msevz53fr2tfnqxf5sdnqpu8th97",
)
testCases.foreach { case (amount, ref) =>
val Success(invoice) = Bolt11Invoice.fromString(ref)
assert(invoice.amount_opt.contains(amount))
val encoded = invoice.toString
assert(encoded == ref)
}
}
test("supported invoice features") {
val nodeParams = TestConstants.Alice.nodeParams.copy(features = Features(knownFeatures.map(f => f -> Optional).toMap))
case class Result(allowMultiPart: Boolean, requirePaymentSecret: Boolean, areSupported: Boolean) // "supported" is based on the "it's okay to be odd" rule