From 5e2d455b728acedb5d6220f5d156190b73068601 Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Wed, 29 Jan 2025 12:47:07 -0300 Subject: [PATCH] nip60: a helper to get amount from a bolt11 invoice. --- nip60/helpers.go | 56 +++++++++++++++++++++++++++++++++++++++++++ nip60/helpers_test.go | 37 ++++++++++++++++++++++++++++ 2 files changed, 93 insertions(+) create mode 100644 nip60/helpers_test.go diff --git a/nip60/helpers.go b/nip60/helpers.go index e7db1d9..c941fd7 100644 --- a/nip60/helpers.go +++ b/nip60/helpers.go @@ -7,6 +7,8 @@ import ( "encoding/json" "errors" "fmt" + "strconv" + "strings" "github.com/btcsuite/btcd/btcec/v2" "github.com/btcsuite/btcd/btcec/v2/schnorr" @@ -201,3 +203,57 @@ func parseKeysetKeys(keys nut01.KeysMap) (map[uint64]*btcec.PublicKey, error) { } return parsedKeys, nil } + +func getSatoshisAmountFromBolt11(bolt11 string) (uint64, error) { + if len(bolt11) < 50 { + return 0, fmt.Errorf("invalid invoice, too short") + } + bolt11 = bolt11[0:50] + idx := strings.LastIndex(bolt11, "1") + if idx == -1 { + return 0, fmt.Errorf("invalid invoice") + } + hrp := bolt11[0:idx] + amount, ok := strings.CutPrefix(hrp, "lnbc") + if !ok { + return 0, fmt.Errorf("invalid invoice") + } + if len(amount) < 1 { + return 0, nil + } + + // if last character is a digit, then the amount can just be interpreted as BTC + char := amount[len(amount)-1] + digit := char - '0' + isDigit := digit >= 0 && digit <= 9 + + cutPoint := len(amount) - 1 + if isDigit { + cutPoint++ + } + + // if not a digit, it must be part of the known units + num := amount[:cutPoint] + if len(num) < 1 { + return 0, nil + } + + am, err := strconv.ParseUint(num, 10, 64) + if err != nil { + return 0, err + } + + switch char { + case 'm': + return am * 100000, nil + case 'u': + return am * 100, nil + case 'n': + return am / 10, nil + case 'p': + return am / 10000, nil + default: + // is BTC + return am * 100000000, nil + } +} diff --git a/nip60/helpers_test.go b/nip60/helpers_test.go new file mode 100644 index 0000000..042aeea --- /dev/null +++ b/nip60/helpers_test.go @@ -0,0 +1,37 @@ +package nip60 + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestBolt11Parsing(t *testing.T) { + for _, tc := range []struct { + bolt11 string + sats uint64 + hasError bool + }{ + {"", 0, true}, + {"lnbc50n1pne523ypp57rn4l2ne673c093g72nm4pum2mkxjm0v2c0pjc9je909axyutdlqdq2f38xy6t5wvcqzpgxqrrssrzjq0nfr7qlprzkl2rke380tj0gkunm66pv7dtqt0396jrq02qz2fs98apyqqqqqqqqqgqqqqqqqqqqqzsqxgsp5rgp9fa9d8unmg949ztty48dgptke8f8zflt7af8d4yknamp2ymdq9p4gqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqpqysgqmatchtjjsyvk45mwwg2mvf8xshxhk2gveslvc27dda6dkp0kfc3hhs06e3c966h3z9j9aran0q8ncm6n458xdca3j5nsrke4saxrfmsqu7suv5", 5, false}, + {"lnbc10500n1pne5tn3pp53wha2waf6g8zac853ay43uscfm9xy3k6ntz0lft72vajyna8695qdq2f38xy6t5wvcqzpgxqrrssrzjq0nfr7qlprzkl2rke380tj0gkunm66pv7dtqt0396jrq02qz2fs98apyqqqqqqqqqgqqqqqqqqqqqzsqxgsp5vapxxnr2wd3780qwmexe24hfcd2m87hsnn8z9xt9u0kmlwq084aq9p4gqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqpqysgq7vljmxfwsxsmf773ad5fhghe4s840t4set4ypyan2tnzwsldxktquw9lzhtlzj3zldma79hgr6ftp7hst8jh0km3p63vtsfugadtmkgp8wtkzp", 1050, false}, + {"lnbc778970n1pne5t5jpp5yazceq5p3sr2dfvp283zqy4xd5jea9vwvc4ptw3m47qlkyzpzdtsdq2f38xy6t5wvcqzpgxqrrssrzjq0nfr7qlprzkl2rke380tj0gkunm66pv7dtqt0396jrq02qz2fs98apyqqqqqqqqqgqqqqqqqqqqqzsqxgsp5ssfackh9ys9ggc4h7ag3panc8wu3gqh84ghnkfd24khyy5pmjx6q9p4gqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqpqysgqdupshqq328498wk3evxh0ext6y5svfsnyauqvcse3l7u3lmk9s5yftmn7ec4l3d5aqd702je4g7aeng2nyjy0qcv23ue9ddn3ga3llsqzlnjv3", 77897, false}, + {"lnbc700u1pne5t4upp5qkxcf9xsmn3p2r55dm386vlp52cvtgjyrv450z6ft2kj6lsmm6asdq2f38xy6t5wvcqzpgxqrrssrzjq0nfr7qlprzkl2rke380tj0gkunm66pv7dtqt0396jrq02qz2fs98apyqqqqqqqqqgqqqqqqqqqqqzsqxgsp5r2ynl9fycj5eega7tzuae5nu8umwawv30glw2c6gh45ymaryxdjs9p4gqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqpqysgqx8d2dr05fmaelmfmxyd6snlyenx25pl6jaxlcm9vqvq89t2smwv8au8j24fwxjswz4j2vcvnmhkaja4dhxf9y49jvwgd7xasgp3tppcp27zlr0", 70000, false}, + {"lnbc2600u1pne5tkepp5q2g379zdf7k35ylf4r4f7hc47e9eazxlv3z55ydjnmtj8gt0vy8qdq2f38xy6t5wvcqzpgxqrrssrzjq0nfr7qlprzkl2rke380tj0gkunm66pv7dtqt0396jrq02qz2fs98apyqqqqqqqqqgqqqqqqqqqqqzsqxgsp5p45aam8azmegj747j8lwmdcjd6uj2f8mjpwh34qjye2h67g3azwq9p4gqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqpqysgq649a6ztfcqtalz3lfukesxep9tkaew90d80l6sds8cnppsqd0yx4jkttyuzw5k8erygzh36k6n3j5x4q3caqxsg3vy52j6va8879x4qqxlk7p5", 260000, false}, + {"lnbc400m1pne5thjpp57slr93076zkczq08nvufgp6td6yafr9su3hqr2w2jl64p8e7k4jsdq2f38xy6t5wvcqzpgxqrrssrzjq0nfr7qlprzkl2rke380tj0gkunm66pv7dtqt0396jrq02qz2fs98apyqqqqqqqqqgqqqqqqqqqqqzsqxgsp55grhezefgcgjsddk4lp4jc564pwu5cp0zddg5f6tygt7xe57g47s9p4gqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqpqysgqf0xtyv62a4v9sjze3ftgdhwpgskhe3hlvwyjnhvt74grzsmpftvzdrlnh02cfg00st26pv7vgkxpknny52s3cuccwpm29l5yarq5fcqpfn7eh9", 40000000, false}, + {"lnbc81pne5tcdpp59qnlzkzyjz0wy0z7fvjxe67u75cpknzfzahwqzm640ly0yrs982sdq2f38xy6t5wvcqzpgxqrrssrzjq0nfr7qlprzkl2rke380tj0gkunm66pv7dtqt0396jrq02qz2fs98apyqqqqqqqqqgqqqqqqqqqqqzsqxgsp5lrljv8ce4c64l77gj4gffg5gc8nlrnngk85twzvp35l92jwwravq9p4gqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqpqysgq5a8yyyreke9x5u4up22tzknum43hqyza9y808hmvftcvj7t7m5lzdrz53mhdh6snxxwd4r0x2cgw9crlzm4pdln80hphwfzpuj869nqq73w8r3", 800000000, false}, + {"lnbc5n1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq", 0, false}, + {"lnbc1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq", 0, false}, + } { + t.Run(fmt.Sprintf("%d = %s", tc.sats, tc.bolt11), func(t *testing.T) { + sats, err := getSatoshisAmountFromBolt11(tc.bolt11) + if tc.hasError { + require.Error(t, err) + } else { + require.NoError(t, err) + } + require.Equal(t, int(tc.sats), int(sats)) + }) + } +}