Skip to content

Commit

Permalink
Merge pull request #107 from getAlby/feat/decode-invoice
Browse files Browse the repository at this point in the history
feat: basic invoice decoding
  • Loading branch information
rolznz authored Nov 16, 2023
2 parents f668ce0 + 5ab42c8 commit 06b99c7
Show file tree
Hide file tree
Showing 5 changed files with 210 additions and 7 deletions.
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,20 @@ await l402.fetchWithL402(
);
```

### Basic invoice decoding

You can initialize an `Invoice` to decode a payment request.

```js
const { Invoice } = require("alby-tools");

const invoice = new Invoice({ pr });

const { paymentHash, satoshi, description, createdDate, expiryDate } = invoice;
```

> If you need more details about the invoice, use a dedicated BOLT11 decoding library.
### 💵 Fiat conversions

Helpers to convert sats values to fiat and fiat values to sats.
Expand Down
30 changes: 30 additions & 0 deletions src/invoice.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import Invoice from "./invoice";

const paymentRequestWithoutMemo =
"lnbc10n1pj4xmazpp5ns890al37jpreen4rlpl6fsw2hlp9n9hm0ts4dvwvcxq8atf4v6qhp50kncf9zk35xg4lxewt4974ry6mudygsztsz8qn3ar8pn3mtpe50scqzzsxqyz5vqsp5k508kdmvfpuac6lvn9wumr9x4mcpnh2t6jyp5kkxcjhueq4xjxqq9qyyssq0m88mwgknhkqfsa9u8e9dp8v93xlm0lqggslzj8mpsnx3mdzm8z5k9ns7g299pfm9zwm4crs00a364cmpraxr54jw5cf2qx9vycucggqz2ggul";

const paymentRequestWithMemo =
"lnbc10u1pj4t6w0pp54wm83znxp8xly6qzuff2z7u6585rnlcw9uduf2haa42qcz09f5wqdq023jhxapqd4jk6mccqzzsxqyz5vqsp5mlvjs8nktpz98s5dcrhsuelrz94kl2vjukvu789yzkewast6m00q9qyyssqupynqdv7e5y8nlul0trva5t97g7v3gwx7akhu2dvu4pn66eu2pr5zkcnegp8myz3wrpj9ht06pwyfn4dvpmnr96ejq6ygex43ymaffqq3gud4d";

describe("Invoice", () => {
test("decode invoice without description", () => {
const decodedInvoice = new Invoice({ pr: paymentRequestWithoutMemo });
expect(decodedInvoice.paymentHash).toBe(
"9c0e57f7f1f4823ce6751fc3fd260e55fe12ccb7dbd70ab58e660c03f569ab34",
);
expect(decodedInvoice.satoshi).toBe(1);
expect(decodedInvoice.expiry).toBe(86400);
expect(decodedInvoice.timestamp).toBe(1699966882);
expect(decodedInvoice.createdDate.toISOString()).toBe(
"2023-11-14T13:01:22.000Z",
);
expect(decodedInvoice.expiryDate.toISOString()).toBe(
"2023-11-15T13:01:22.000Z",
);
expect(decodedInvoice.description).toBeNull();
});
test("decode invoice with description", () => {
const decodedInvoice = new Invoice({ pr: paymentRequestWithMemo });
expect(decodedInvoice.description).toBe("Test memo");
});
});
23 changes: 21 additions & 2 deletions src/invoice.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { getHashFromInvoice } from "./utils/invoice";
import { decodeInvoice } from "./utils/invoice";
import Hex from "crypto-js/enc-hex.js";
import sha256 from "crypto-js/sha256.js";
import { InvoiceArgs } from "./types";
Expand All @@ -8,10 +8,29 @@ export default class Invoice {
paymentHash: string;
preimage: string | null;
verify: string | null;
satoshi: number;
expiry: number; // expiry in seconds (not a timestamp)
timestamp: number; // created date in seconds
createdDate: Date;
expiryDate: Date;
description: string | null;

constructor(args: InvoiceArgs) {
this.paymentRequest = args.pr;
this.paymentHash = getHashFromInvoice(this.paymentRequest) as string;
if (!this.paymentRequest) {
throw new Error("Invalid payment request");
}
const decodedInvoice = decodeInvoice(this.paymentRequest);
if (!decodedInvoice) {
throw new Error("Failed to decode payment request");
}
this.paymentHash = decodedInvoice.paymentHash;
this.satoshi = decodedInvoice.satoshi;
this.timestamp = decodedInvoice.timestamp;
this.expiry = decodedInvoice.expiry;
this.createdDate = new Date(this.timestamp * 1000);
this.expiryDate = new Date((this.timestamp + this.expiry) * 1000);
this.description = decodedInvoice.description ?? null;
this.verify = args.verify ?? null;
this.preimage = args.preimage ?? null;
}
Expand Down
60 changes: 55 additions & 5 deletions src/utils/invoice.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,68 @@
import { decode } from "light-bolt11-decoder";

export const getHashFromInvoice = (invoice) => {
if (!invoice) return null;
type DecodedInvoice = {
paymentHash: string;
satoshi: number;
timestamp: number;
expiry: number;
description: string | undefined;
};

export const decodeInvoice = (
paymentRequest: string,
): DecodedInvoice | null => {
if (!paymentRequest) return null;

try {
const decoded = decode(invoice);
const decoded = decode(paymentRequest);
if (!decoded || !decoded.sections) return null;

const hashTag = decoded.sections.find(
(value) => value.name === "payment_hash",
);
if (!hashTag || !hashTag.value) return null;

return hashTag.value.toString();
if (hashTag?.name !== "payment_hash" || !hashTag.value) return null;

const paymentHash = hashTag.value;

const amountTag = decoded.sections.find((value) => value.name === "amount");

if (amountTag?.name !== "amount" || amountTag.value === undefined)
return null;

const satoshi = parseInt(amountTag.value) / 1000; // millisats

const expiryTag = decoded.sections.find((value) => value.name === "expiry");

const timestampTag = decoded.sections.find(
(value) => value.name === "timestamp",
);

if (timestampTag?.name !== "timestamp" || !timestampTag.value) return null;

const timestamp = timestampTag.value;

if (expiryTag?.name !== "expiry" || expiryTag.value === undefined)
return null;

const expiry = expiryTag.value;

const descriptionTag = decoded.sections.find(
(value) => value.name === "description",
);

const description =
descriptionTag?.name === "description"
? descriptionTag?.value
: undefined;

return {
paymentHash,
satoshi,
timestamp,
expiry,
description,
};
} catch {
return null;
}
Expand Down
90 changes: 90 additions & 0 deletions src/utils/light-bolt11-decoder.d.ts
Original file line number Diff line number Diff line change
@@ -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;
}

0 comments on commit 06b99c7

Please sign in to comment.