From 30f26d96389767bfbc51ada385f0891add2f4a89 Mon Sep 17 00:00:00 2001 From: David Kaplan Date: Fri, 16 Jan 2026 15:43:41 -0500 Subject: [PATCH] feat(sdk-core): add Lightning invoice support to OFC tokens Implement support for Bolt11 Lightning Network invoices in the OfcToken class, allowing users to send to Lightning addresses. Added validation for Lightning invoice addresses and proper amount handling. - Created new lightning.ts module with isBolt11Invoice validation function - Extended checkRecipient method in OfcToken class to handle Lightning invoices - Added validation for Lightning invoice amounts and backing coins - Added unit tests for the Lightning invoice validation BTC-2775 TICKET: BTC-2775 --- modules/sdk-core/src/coins/ofcToken.ts | 35 +++++++++++++++++++++++++ modules/sdk-core/src/lightning.ts | 11 ++++++++ modules/sdk-core/test/unit/lightning.ts | 26 ++++++++++++++++++ 3 files changed, 72 insertions(+) create mode 100644 modules/sdk-core/src/lightning.ts create mode 100644 modules/sdk-core/test/unit/lightning.ts diff --git a/modules/sdk-core/src/coins/ofcToken.ts b/modules/sdk-core/src/coins/ofcToken.ts index 84ffbb4406..8769635f75 100644 --- a/modules/sdk-core/src/coins/ofcToken.ts +++ b/modules/sdk-core/src/coins/ofcToken.ts @@ -8,7 +8,10 @@ import { CoinConstructor, SignTransactionOptions as BaseSignTransactionOptions, SignedTransaction, + ITransactionRecipient, } from '../'; +import { isBolt11Invoice, LIGHTNING_INVOICE } from '../lightning'; + import { Ofc } from './ofc'; export interface SignTransactionOptions extends BaseSignTransactionOptions { @@ -21,6 +24,7 @@ export interface SignTransactionOptions extends BaseSignTransactionOptions { export { OfcTokenConfig }; const publicIdRegex = /^[a-f\d]{32}$/i; + export class OfcToken extends Ofc { public readonly tokenConfig: OfcTokenConfig; @@ -65,6 +69,37 @@ export class OfcToken extends Ofc { return this.tokenConfig.type; } + checkRecipient(recipient: ITransactionRecipient): void { + if (isBolt11Invoice(recipient.address)) { + // should throw error if this isnt bitcoin (mainnet or testnet) + if (this.backingCoin !== 'btc' && this.backingCoin !== 'tbtc') { + throw new Error(`invalid argument - lightning invoice is only supported for bitcoin`); + } + + // amount for bolt11 invoices is either 'invoice' or a non-zero bigint + if (recipient.amount === LIGHTNING_INVOICE) { + return; + } + // try to parse the amount as a bigint + let amount: bigint; + try { + amount = BigInt(recipient.amount); + } catch (e) { + throw new Error( + `invalid argument ${recipient.amount} for amount - lightning invoice amount must be >= 0 or ${LIGHTNING_INVOICE}` + ); + } + if (amount > 0n) { + return; + } + throw new Error( + `invalid argument for amount - lightning invoice amount must be a non-zero bigint or ${LIGHTNING_INVOICE}` + ); + } + + super.checkRecipient(recipient); + } + /** * Flag for sending value of 0 * @returns {boolean} True if okay to send 0 value, false otherwise diff --git a/modules/sdk-core/src/lightning.ts b/modules/sdk-core/src/lightning.ts new file mode 100644 index 0000000000..c6543476df --- /dev/null +++ b/modules/sdk-core/src/lightning.ts @@ -0,0 +1,11 @@ +export const LIGHTNING_INVOICE = 'invoice'; + +export function isBolt11Invoice(value: unknown): value is string { + if (typeof value !== 'string') { + return false; + } + if (value.startsWith('lnbc') || value.startsWith('lntb')) { + return true; + } + return false; +} diff --git a/modules/sdk-core/test/unit/lightning.ts b/modules/sdk-core/test/unit/lightning.ts new file mode 100644 index 0000000000..7d4402a59d --- /dev/null +++ b/modules/sdk-core/test/unit/lightning.ts @@ -0,0 +1,26 @@ +import 'should'; + +import { isBolt11Invoice } from '../../src/lightning'; + +describe('lightning', () => { + describe('isBolt11Invoice', () => { + it('should return true for a valid bolt11 invoice', () => { + const invoice = + 'lnbc500n1p3zv5vkpp5x0thcaz8wep54clc2xt5895azjdzmthyskzzh9yslggy74qtvl6sdpdg3hkuct5d9hkugrxdaezqjn0dphk2fmnypkk2mtsdahkccqzpgxqyz5vqsp5v80q4vq4pwakq2l0hcqgtelgajsymv4ud4jdcrqtnzhvet55qlus9qyyssquqh2wl2m866qs5n72c5vg6wmqx9vzwhs5ypualq4mcu76h2tdkcq3jtjwtggfff7xwtdqxlnwqk8cxpzryjghrmmq3syraswp9vjr7cqry9l96'; + + isBolt11Invoice(invoice).should.equal(true); + }); + + it('should return false for non-string values', () => { + isBolt11Invoice(undefined).should.equal(false); + isBolt11Invoice(null as any).should.equal(false); + isBolt11Invoice(123 as any).should.equal(false); + isBolt11Invoice({} as any).should.equal(false); + }); + + it('should return false for invalid invoice strings', () => { + isBolt11Invoice('').should.equal(false); + isBolt11Invoice('not-an-invoice').should.equal(false); + }); + }); +});