diff --git a/package.json b/package.json index 8d98310..2a638cc 100644 --- a/package.json +++ b/package.json @@ -40,8 +40,9 @@ }, "dependencies": { "crypto-js": "^4.1.1", - "nostr-tools": "1.13.1", - "events": "^3.3.0" + "events": "^3.3.0", + "light-bolt11-decoder": "^3.0.0", + "nostr-tools": "1.13.1" }, "devDependencies": { "@commitlint/cli": "^17.7.1", diff --git a/src/decodeInvoice.test.ts b/src/decodeInvoice.test.ts new file mode 100644 index 0000000..487d006 --- /dev/null +++ b/src/decodeInvoice.test.ts @@ -0,0 +1,32 @@ +import { decodeInvoice } from "./decodeInvoice"; + +const invoice = + "lnbc10n1pj4xmazpp5ns890al37jpreen4rlpl6fsw2hlp9n9hm0ts4dvwvcxq8atf4v6qhp50kncf9zk35xg4lxewt4974ry6mudygsztsz8qn3ar8pn3mtpe50scqzzsxqyz5vqsp5k508kdmvfpuac6lvn9wumr9x4mcpnh2t6jyp5kkxcjhueq4xjxqq9qyyssq0m88mwgknhkqfsa9u8e9dp8v93xlm0lqggslzj8mpsnx3mdzm8z5k9ns7g299pfm9zwm4crs00a364cmpraxr54jw5cf2qx9vycucggqz2ggul"; + +describe("decodeInvoice", () => { + test("get amount from invoice", () => { + const decodedInvoice = decodeInvoice(invoice); + const amountSection = decodedInvoice.sections.find( + (section) => section.name === "amount", + ); + if (amountSection?.name !== "amount") { + throw new Error("did not find amount section"); + } + const value = amountSection.value; + expect(value).toEqual("1000"); + }); + + test("get payment hash from invoice", () => { + const decodedInvoice = decodeInvoice(invoice); + const paymentHashSection = decodedInvoice.sections.find( + (section) => section.name === "payment_hash", + ); + if (paymentHashSection?.name !== "payment_hash") { + throw new Error("did not find payment_hash section"); + } + const value = paymentHashSection.value; + expect(value).toEqual( + "9c0e57f7f1f4823ce6751fc3fd260e55fe12ccb7dbd70ab58e660c03f569ab34", + ); + }); +}); diff --git a/src/decodeInvoice.ts b/src/decodeInvoice.ts new file mode 100644 index 0000000..246868e --- /dev/null +++ b/src/decodeInvoice.ts @@ -0,0 +1,13 @@ +import { decode } from "light-bolt11-decoder"; +export function decodeInvoice(invoice: string) { + if (!invoice) { + throw new Error("No invoice provided"); + } + + try { + return decode(invoice); + } catch (error) { + console.error("Failed to decode invoice", error); + throw error; + } +} diff --git a/src/index.ts b/src/index.ts index f8e7ed0..85f53c2 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,3 +2,4 @@ export * as auth from "./auth"; export * as types from "./types"; export * as webln from "./webln"; export { Client } from "./client"; +export * from "./decodeInvoice"; diff --git a/src/light-bolt11-decoder.d.ts b/src/light-bolt11-decoder.d.ts new file mode 100644 index 0000000..31eac3e --- /dev/null +++ b/src/light-bolt11-decoder.d.ts @@ -0,0 +1,90 @@ +// TODO: remove when https://github.com/nbd-wtf/light-bolt11-decoder/pull/4 is merged +declare module "light-bolt11-decoder" { + type NetworkSection = { + name: "coin_network"; + letters: string; + value?: { + bech32: string; + pubKeyHash: number; + scriptHash: number; + validWitnessVersions: number[]; + }; + }; + + type FeatureBits = { + option_data_loss_protect: string; + initial_routing_sync: string; + option_upfront_shutdown_script: string; + gossip_queries: string; + var_onion_optin: string; + gossip_queries_ex: string; + option_static_remotekey: string; + payment_secret: string; + basic_mpp: string; + option_support_large_channel: string; + extra_bits: { + start_bit: number; + bits: unknown[]; + has_required: boolean; + }; + }; + + type RouteHint = { + pubkey: string; + short_channel_id: string; + fee_base_msat: number; + fee_proportional_millionths: number; + cltv_expiry_delta: number; + }; + + type RouteHintSection = { + name: "route_hint"; + tag: "r"; + letters: string; + value: RouteHint[]; + }; + + type FeatureBitsSection = { + name: "feature_bits"; + tag: "9"; + letters: string; + value: FeatureBits; + }; + + type Section = + | { name: "paymentRequest"; value: string } + | { name: "expiry"; value: number } + | { name: "checksum"; letters: string } + | NetworkSection + | { name: "amount"; letters: string; value: string } + | { name: "separator"; letters: string } + | { name: "timestamp"; letters: string; value: number } + | { name: "payment_hash"; tag: "p"; letters: string; value: string } + | { name: "description"; tag: "d"; letters: string; value: string } + | { name: "payment_secret"; tag: "s"; letters: string; value: string } + | { + name: "min_final_cltv_expiry"; + tag: "c"; + letters: string; + value: number; + } + | FeatureBitsSection + | RouteHintSection + | { name: "signature"; letters: string; value: string }; + + type PaymentJSON = { + paymentRequest: string; + sections: Section[]; + expiry: number; + route_hints: RouteHint[][]; + }; + + type DecodedInvoice = { + paymentRequest: string; + sections: Section[]; + expiry: number; + route_hints: RouteHint[][]; + }; + + function decode(invoice: string): DecodedInvoice; +} diff --git a/yarn.lock b/yarn.lock index 8d3718f..8a70ac1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5412,6 +5412,13 @@ levn@^0.4.1: prelude-ls "^1.2.1" type-check "~0.4.0" +light-bolt11-decoder@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/light-bolt11-decoder/-/light-bolt11-decoder-3.0.0.tgz#f644576120426c9ef65621bde254f11016055044" + integrity sha512-AKvOigD2pmC8ktnn2TIqdJu0K0qk6ukUmTvHwF3JNkm8uWCqt18Ijn33A/a7gaRZ4PghJ59X+8+MXrzLKdBTmQ== + dependencies: + "@scure/base" "1.1.1" + lilconfig@2.1.0, lilconfig@^2.0.3, lilconfig@^2.0.5: version "2.1.0" resolved "https://registry.yarnpkg.com/lilconfig/-/lilconfig-2.1.0.tgz#78e23ac89ebb7e1bfbf25b18043de756548e7f52"