diff --git a/packages/lnbits/package.json b/packages/lnbits/package.json new file mode 100644 index 0000000..ccb12dd --- /dev/null +++ b/packages/lnbits/package.json @@ -0,0 +1,31 @@ +{ + "name": "@bitSpire/lnbits", + "version": "0.1.0", + "description": "LNbits client over the nostr-native-transport (kind-21000, NIP-44 v2)", + "type": "module", + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + } + }, + "scripts": { + "build": "tsc", + "dev": "tsc --watch", + "test": "vitest run", + "test:watch": "vitest", + "typecheck": "tsc --noEmit", + "lint": "eslint src/" + }, + "dependencies": { + "@bitSpire/nostr-client": "workspace:*", + "nostr-tools": "^2.10.0" + }, + "devDependencies": { + "@types/node": "^22.0.0", + "typescript": "^5.7.0", + "vitest": "^2.1.0" + } +} diff --git a/packages/lnbits/src/client.ts b/packages/lnbits/src/client.ts new file mode 100644 index 0000000..b820b95 --- /dev/null +++ b/packages/lnbits/src/client.ts @@ -0,0 +1,472 @@ +/** + * LnbitsClient — talks to an LNbits instance via its + * nostr-native-transport (kind-21000 NIP-44 v2 RPC). + * + * Wire-compatible peer of LightningPubClient (@bitSpire/lightning) so the + * machine app's services/lightning.ts can swap one for the other with no + * shape changes to its public consumers. Differences vs LightningPubClient: + * + * - Auth: LNbits derives the calling identity from the event signature. + * There is no `authIdentifier`/`appId` field; the signing pubkey IS + * the credential, and the LNbits server auto-creates an account on + * first contact (with `prvkey=NULL` — see issue aiolabs/lnbits#9). + * - Subscriptions: LNbits has a single `subscribe_payments` RPC that + * handles both "watch this hash" and "watch this wallet" via filter + * fields. It also covers tag/link_id for LNURL settlement events. + * - CLINK/ndebit/noffer is NOT in LNbits. Anything that used kind + * 21001/21002/21003 must use a different flow under LNbits — for the + * ATM that means LNURL-withdraw + subscribe_payments for cash-in. + * + * See docs/devs/nostr-transport.md in aiolabs/lnbits for the full surface. + */ + +import { + type NostrClient, + type MachineIdentity, + type Event as NostrEvent, + encryptContentV2, + decryptContentV2, +} from '@bitSpire/nostr-client' +import { finalizeEvent } from 'nostr-tools' + +import type { + LnbitsConfig, + LnbitsRpcRequest, + LnbitsRpcResponse, + LnbitsPayment, + CreateInvoiceBody, + PayInvoiceBody, + WalletInfo, + SubscribePaymentsBody, + SubscribeAck, + PaymentPushCallback, + SubscriptionCloseCallback, + CreateWithdrawLinkBody, + LnbitsWithdrawLink, + UniqueHashesResponse, +} from './types.js' + +const LNBITS_KIND_RPC = 21000 + +/** Active streaming subscription state held client-side. */ +interface ActiveSubscription { + subscriptionId: string + requestId: string + /** Underlying nostr relay subscription id (so we can close it). */ + relaySubId: string + onPush: PaymentPushCallback + onClose?: SubscriptionCloseCallback +} + +export class LnbitsClient { + private readonly config: Required + private nostr: NostrClient | null = null + private identity: MachineIdentity | null = null + private requestCounter = 0 + private readonly pending = new Map< + string, + { resolve: (r: LnbitsRpcResponse) => void; reject: (e: Error) => void; timer: ReturnType } + >() + private readonly subscriptions = new Map() + private relaySubIdForReplies?: string + + constructor(config: LnbitsConfig) { + this.config = { + timeout: 30_000, + ...config, + } + } + + initialize(nostr: NostrClient, identity: MachineIdentity): void { + this.nostr = nostr + this.identity = identity + this.startReplyListener() + } + + // ============================================================================ + // Wallet + // ============================================================================ + + /** + * Returns this caller's wallet via `get_wallet`. If `walletId` is omitted, + * uses the first wallet from `list_wallets` (the default wallet, auto- + * created on first contact under LNBITS_DEMO_MODE or via web flow). + */ + async getWallet(walletId?: string): Promise { + if (walletId) { + const data = await this.sendRpc('get_wallet', { walletId }) + return data + } + const wallets = await this.listWallets() + if (wallets.length === 0) { + throw new Error('LnbitsClient.getWallet: no wallets for this account') + } + return wallets[0]! + } + + /** + * Returns balance in **satoshis** (the LightningPubClient surface used + * sats; preserve that here to keep services/lightning.ts mechanical). + * Internally LNbits stores msat. + */ + async getBalance(walletId?: string): Promise<{ balanceSats: number }> { + const wallet = await this.getWallet(walletId) + return { balanceSats: Math.floor(wallet.balance / 1000) } + } + + /** Enumerate every wallet owned by the calling account. */ + async listWallets(): Promise { + const data = await this.sendRpc('list_wallets', {}) + return data ?? [] + } + + // ============================================================================ + // Invoices + // ============================================================================ + + async createInvoice(walletId: string, body: CreateInvoiceBody): Promise { + const data = await this.sendRpc('create_invoice', { walletId, body }) + return data + } + + async payInvoice(walletId: string, body: PayInvoiceBody): Promise { + const data = await this.sendRpc('pay_invoice', { walletId, body }) + return data + } + + /** Point-lookup of a payment by hash. AUTH_NONE — hashes are hard to guess. */ + async getPayment(paymentHash: string): Promise { + const data = await this.sendRpc('get_payment', { + body: { payment_hash: paymentHash }, + }) + return data ?? null + } + + async decodePayment(paymentRequest: string): Promise> { + const data = await this.sendRpc>('decode_payment', { + body: { payment_request: paymentRequest }, + }) + return data + } + + // ============================================================================ + // Subscriptions + // ============================================================================ + + /** + * Open a long-lived subscription that pushes settlement events to `onPush` + * whenever a Payment matches the filter. Returns the server-issued + * subscription_id (also tracked client-side so `unsubscribe` works). + * + * Filter must specify at least one of `wallet_id`/`payment_hash`/`tag+link_id` + * — empty filters are rejected server-side with status:"ERROR". + */ + async subscribePayments( + walletId: string | undefined, + filter: SubscribePaymentsBody, + onPush: PaymentPushCallback, + onClose?: SubscriptionCloseCallback, + ): Promise { + if (!this.nostr || !this.identity) { + throw new Error('LnbitsClient.subscribePayments: client not initialized') + } + const requestId = this.nextRequestId('sub') + + // Pre-register the push handlers BEFORE the ack arrives so we don't + // race against an extremely fast first push (rare but possible). + const placeholderSub: ActiveSubscription = { + subscriptionId: '', + requestId, + relaySubId: this.relaySubIdForReplies ?? '', + onPush, + onClose, + } + // We don't have subscriptionId yet — index by requestId temporarily. + // The reply listener will move it under the real subscriptionId once + // the ack comes back. + this.subscriptions.set(`pending:${requestId}`, placeholderSub) + + try { + const ack = await this.sendRpc('subscribe_payments', { + walletId, + body: filter, + requestId, + }) + const subId = ack.subscription_id + placeholderSub.subscriptionId = subId + this.subscriptions.delete(`pending:${requestId}`) + this.subscriptions.set(subId, placeholderSub) + return subId + } catch (err) { + this.subscriptions.delete(`pending:${requestId}`) + throw err + } + } + + /** + * Convenience: watch a single payment by hash until it settles. Resolves + * with the Payment object on success; rejects on TTL close without a + * settlement. Mirrors LightningPubClient.watchInvoice's resolve-on-paid + * shape so services/lightning.ts can swap mechanically. + */ + async watchInvoice( + walletId: string, + paymentHash: string, + timeoutMs = 5 * 60_000, + ): Promise { + return new Promise((resolve, reject) => { + let resolved = false + const subId = this.subscribePayments( + walletId, + { payment_hash: paymentHash, max_seconds: Math.ceil(timeoutMs / 1000) }, + (payment) => { + if (payment.payment_hash !== paymentHash) return + if (payment.status !== 'success') return + if (resolved) return + resolved = true + void this.unsubscribe(walletId, subIdRef).catch(() => {}) + resolve(payment) + }, + (reason) => { + if (resolved) return + resolved = true + reject(new Error(`watchInvoice: subscription closed (${reason}) before settlement`)) + }, + ) + let subIdRef = '' + subId.then((id) => (subIdRef = id)).catch(reject) + }) + } + + /** + * Watch all settlements on a wallet. Useful for live balance updates. + * Returns a `cancel()` function that closes the subscription server-side. + */ + async watchWallet( + walletId: string, + onPush: PaymentPushCallback, + onClose?: SubscriptionCloseCallback, + ): Promise<() => void> { + const subId = await this.subscribePayments(walletId, { max_seconds: 600 }, onPush, onClose) + return () => { + void this.unsubscribe(walletId, subId).catch(() => {}) + } + } + + /** + * Cancel a server-side subscription. The wire mechanic is asymmetric: + * the handler emits the `closed` push FIRST and the dispatcher ACK SECOND, + * so the relay sees two events. We discard the close push (the local + * subscription state is already torn down by the time it arrives) and + * await only the ACK. + */ + async unsubscribe(walletId: string | undefined, subscriptionId: string): Promise { + const sub = this.subscriptions.get(subscriptionId) + if (sub) this.subscriptions.delete(subscriptionId) + try { + const data = await this.sendRpc<{ ok: boolean }>('unsubscribe', { + walletId, + body: { subscription_id: subscriptionId }, + }) + return data.ok + } catch { + return false + } + } + + // ============================================================================ + // LNURL-withdraw (cash-in for the ATM) + // ============================================================================ + + async createWithdrawLink(walletId: string, body: CreateWithdrawLinkBody): Promise { + const data = await this.sendRpc('lnurlw_create_link', { walletId, body }) + return data + } + + async getWithdrawLink(walletId: string, id: string): Promise { + const data = await this.sendRpc('lnurlw_get_link', { walletId, body: { id } }) + return data + } + + async listWithdrawLinks( + walletId: string | undefined, + body: { limit?: number; offset?: number } = {}, + ): Promise<{ data: LnbitsWithdrawLink[]; total: number }> { + return this.sendRpc<{ data: LnbitsWithdrawLink[]; total: number }>('lnurlw_list_links', { + walletId, + body, + }) + } + + /** + * For an `is_unique=true` link, returns the per-use `id_unique_hash` + * values for each unredeemed slot. Each one becomes the trailing path + * component of `/withdraw/api/v1/lnurl//` + * — the URL a customer wallet GETs to redeem that specific sub-link. + */ + async getWithdrawLinkUniqueHashes(walletId: string, id: string): Promise { + return this.sendRpc('lnurlw_unique_hashes', { + walletId, + body: { id }, + }) + } + + async updateWithdrawLink( + walletId: string, + id: string, + patch: Partial, + ): Promise { + return this.sendRpc('lnurlw_update_link', { + walletId, + body: { id, ...patch }, + }) + } + + async deleteWithdrawLink(walletId: string, id: string): Promise<{ ok: boolean }> { + return this.sendRpc<{ ok: boolean }>('lnurlw_delete_link', { walletId, body: { id } }) + } + + // ============================================================================ + // Internal — RPC plumbing + // ============================================================================ + + private nextRequestId(prefix = 'req'): string { + this.requestCounter += 1 + return `${prefix}-${this.requestCounter}-${Math.random().toString(36).slice(2, 8)}` + } + + /** + * Publish an RPC event and resolve with the first matching reply. + * The reply listener (started by initialize()) matches by `request_id`. + */ + private async sendRpc( + rpcName: string, + args: { + walletId?: string + body?: unknown + query?: unknown + requestId?: string + }, + ): Promise { + if (!this.nostr || !this.identity) { + throw new Error(`LnbitsClient.${rpcName}: client not initialized`) + } + const requestId = args.requestId ?? this.nextRequestId(rpcName) + const request: LnbitsRpcRequest = { + rpc_name: rpcName, + request_id: requestId, + } + if (args.walletId !== undefined) request.wallet_id = args.walletId + if (args.body !== undefined) request.body = args.body as Record + if (args.query !== undefined) request.query = args.query as Record + + const plaintext = JSON.stringify(request) + const encrypted = encryptContentV2(this.identity, this.config.serverPubkey, plaintext) + + // Build + sign the kind-21000 event ourselves. The server reads our + // pubkey directly off the signature, so there's no separate + // authIdentifier in the envelope (unlike LightningPubClient). + const event = finalizeEvent( + { + kind: LNBITS_KIND_RPC, + content: encrypted, + tags: [['p', this.config.serverPubkey]], + created_at: Math.floor(Date.now() / 1000), + }, + this.identity.privateKey, + ) + + // The pending entry MUST be registered before publish so we don't race + // an extremely fast reply. + const result = new Promise((resolve, reject) => { + const timer = setTimeout(() => { + this.pending.delete(requestId) + reject(new Error(`LnbitsClient.${rpcName}: timeout after ${this.config.timeout}ms`)) + }, this.config.timeout) + + this.pending.set(requestId, { + resolve: (response) => { + clearTimeout(timer) + this.pending.delete(requestId) + if (response.status === 'ERROR') { + reject(new Error(response.error ?? `${rpcName}: server returned ERROR`)) + return + } + resolve(response.data as T) + }, + reject: (err) => { + clearTimeout(timer) + this.pending.delete(requestId) + reject(err) + }, + timer, + }) + }) + + await this.nostr.publish(event) + return result + } + + /** + * One-time setup: subscribe on the relay for kind-21000 events tagged to + * our pubkey from the LNbits server. Every reply (one-shot ACK or + * subscription push) flows through this single listener; we dispatch + * based on `request_id` and `subscription_id`. + */ + private startReplyListener(): void { + if (!this.nostr || !this.identity) return + const myPubkey = this.identity.publicKey + const since = Math.floor(Date.now() / 1000) - 5 + + this.relaySubIdForReplies = this.nostr.subscribe( + [ + { + kinds: [LNBITS_KIND_RPC], + authors: [this.config.serverPubkey], + '#p': [myPubkey], + since, + }, + ], + { + onEvent: (ev: NostrEvent) => this.handleReply(ev), + }, + ) + } + + private handleReply(ev: NostrEvent): void { + if (!this.identity) return + let plaintext: string + try { + plaintext = decryptContentV2(this.identity, this.config.serverPubkey, ev.content) + } catch { + return // not our peer or wrong key + } + let parsed: LnbitsRpcResponse + try { + parsed = JSON.parse(plaintext) as LnbitsRpcResponse + } catch { + return + } + + // Pending one-shot RPC ack — match by request_id. + const pending = this.pending.get(parsed.request_id) + if (pending) { + pending.resolve(parsed) + } + + // Subscription push — match by subscription_id at the top level. + const subId = parsed.subscription_id ?? null + if (subId) { + const sub = this.subscriptions.get(subId) + if (sub) { + const data = (parsed.data ?? {}) as { payment?: LnbitsPayment; closed?: boolean; reason?: 'ttl' | 'unsubscribed' } + if (data.closed === true) { + this.subscriptions.delete(subId) + sub.onClose?.(data.reason ?? 'ttl') + } else if (data.payment) { + sub.onPush(data.payment) + } + } + } + } +} diff --git a/packages/lnbits/src/index.ts b/packages/lnbits/src/index.ts new file mode 100644 index 0000000..34463dc --- /dev/null +++ b/packages/lnbits/src/index.ts @@ -0,0 +1,71 @@ +/** + * @bitSpire/lnbits — public surface. + * + * LNbits client that speaks the nostr-native-transport (kind-21000, + * NIP-44 v2) shipped on aiolabs/lnbits@nostr-native-transport. Designed + * as a drop-in peer of @bitSpire/lightning's LightningPubClient so the + * machine app can swap backends with minimal call-site churn. + * + * The transport is fully documented at + * docs/devs/nostr-transport.md + * in the LNbits repo. See packages/lnbits/src/types.ts for the wire + * envelope and packages/lnbits/src/client.ts for the API surface. + * + * @example + * ```ts + * import { LnbitsClient } from '@bitSpire/lnbits' + * import { NostrClient, loadIdentityFromHex } from '@bitSpire/nostr-client' + * + * const nostr = new NostrClient({ relays: ['wss://relay.example/'] }) + * await nostr.connect() + * const identity = loadIdentityFromHex(process.env.VITE_ATM_PRIVATE_KEY!) + * + * const client = new LnbitsClient({ + * serverPubkey: process.env.VITE_LNBITS_SERVER_PUBKEY!, + * relays: ['wss://relay.example/'], + * }) + * client.initialize(nostr, identity) + * + * // Discover the auto-created default wallet + * const [wallet] = await client.listWallets() + * + * // Create an invoice + watch for settlement + * const invoice = await client.createInvoice(wallet.id, { amount: 100, memo: 'cash-out' }) + * const paid = await client.watchInvoice(wallet.id, invoice.payment_hash) + * console.log('paid:', paid.preimage) + * + * // Cash-in via LNURL-withdraw + subscribe + * const link = await client.createWithdrawLink(wallet.id, { + * title: 'cash-in session', + * min_withdrawable: 100, max_withdrawable: 100, + * uses: 1, wait_time: 1, is_unique: false, + * }) + * await client.subscribePayments( + * wallet.id, + * { tag: 'withdraw', link_id: link.id, max_seconds: 300 }, + * (push) => console.log('link claimed:', push.payment_hash), + * ) + * ``` + */ + +export { LnbitsClient } from './client.js' +export type { + LnbitsConfig, + LnbitsRpcRequest, + LnbitsRpcResponse, + LnbitsPayment, + CreateInvoiceBody, + PayInvoiceBody, + WalletInfo, + SubscribePaymentsBody, + SubscribeAck, + SubscribePush, + SubscribeClose, + PaymentPushCallback, + SubscriptionCloseCallback, + CreateWithdrawLinkBody, + LnbitsWithdrawLink, + UniqueHashEntry, + UniqueHashesResponse, + LnbitsPayLink, +} from './types.js' diff --git a/packages/lnbits/src/types.ts b/packages/lnbits/src/types.ts new file mode 100644 index 0000000..6372182 --- /dev/null +++ b/packages/lnbits/src/types.ts @@ -0,0 +1,232 @@ +/** + * @bitSpire/lnbits — type definitions for the LNbits nostr-native-transport. + * + * Wire envelope mirrors `lnbits/core/services/nostr_transport/models.py` in + * the aiolabs/lnbits@nostr-native-transport branch. All RPC traffic is + * NIP-44 v2 encrypted kind-21000 events on a relay; the inner JSON has + * the shape below. + */ + +// ============================================================================ +// Wire envelope (decrypted plaintext inside kind-21000 events) +// ============================================================================ + +/** + * Inbound request — what the client publishes to the LNbits server's pubkey. + */ +export interface LnbitsRpcRequest { + rpc_name: string + request_id: string + /** Required for AUTH_WALLET RPCs; selects which of the caller's wallets to act on. */ + wallet_id?: string + body?: Record + /** Used by a small number of RPCs (e.g. list_payments filters). */ + query?: Record +} + +/** + * Outbound response — what the server emits back on the same relay. + * + * For one-shot RPCs the client awaits a single response with this shape. + * For subscribe_payments the server emits an immediate ack (with status:OK + * and data.subscription_id) followed by one push response per matching + * Payment, each carrying the same subscription_id at top level. The final + * push when the subscription closes has data.closed=true. + */ +export interface LnbitsRpcResponse { + status: 'OK' | 'ERROR' + request_id: string + /** Non-null on subscription push events. Null on regular acks. */ + subscription_id?: string | null + data?: T + error?: string +} + +// ============================================================================ +// Client configuration +// ============================================================================ + +export interface LnbitsConfig { + /** Server's nostr-transport pubkey (hex). Published in the server's startup log. */ + serverPubkey: string + /** Relays where the server is publishing/subscribing. Same set the server uses. */ + relays: string[] + /** Per-call timeout in ms (default 30_000). */ + timeout?: number +} + +// ============================================================================ +// Payment object — server-side shape, returned by every payment-touching RPC +// ============================================================================ + +/** + * Mirrors lnbits.core.models.payments.Payment.dict(). + * Negative `amount` ⇒ outgoing. Positive ⇒ incoming. Units are msat. + */ +export interface LnbitsPayment { + checking_id: string + payment_hash: string + wallet_id: string + amount: number // msat + fee: number // msat + bolt11: string + payment_request: string + status: 'pending' | 'success' | 'failed' + memo?: string | null + expiry?: string + preimage?: string | null + time: string + created_at: string + updated_at: string + extra?: Record +} + +// ============================================================================ +// Payment-RPC inputs / outputs (the subset relevant to the ATM) +// ============================================================================ + +export interface CreateInvoiceBody { + amount: number + memo?: string + unit?: 'sat' | 'msat' + expiry?: number + extra?: Record +} + +export interface PayInvoiceBody { + bolt11: string + max_sat?: number + extra?: Record + description?: string +} + +export interface WalletInfo { + id: string + name: string + /** Balance in millisat. */ + balance: number +} + +// ============================================================================ +// Subscriptions +// ============================================================================ + +export interface SubscribePaymentsBody { + payment_hash?: string + tag?: string + link_id?: string + /** Server-side clamped to [1, 600]. Default 300. */ + max_seconds?: number +} + +export interface SubscribeAck { + subscription_id: string + /** Unix epoch seconds. */ + expires_at: number +} + +/** Push event payload — every settlement that matches the filter. */ +export interface SubscribePush { + payment: LnbitsPayment +} + +/** Final close push. */ +export interface SubscribeClose { + subscription_id: string + closed: true + reason: 'ttl' | 'unsubscribed' +} + +// ============================================================================ +// LNURL-withdraw (the `withdraw` extension's transport surface) +// ============================================================================ + +export interface CreateWithdrawLinkBody { + title: string + min_withdrawable: number + max_withdrawable: number + uses: number + wait_time: number + is_unique?: boolean + webhook_url?: string + webhook_headers?: string + webhook_body?: string + custom_url?: string + enabled?: boolean +} + +/** Full WithdrawLink as the LNbits side returns it. */ +export interface LnbitsWithdrawLink { + id: string + wallet: string + title: string + min_withdrawable: number + max_withdrawable: number + uses: number + used: number + wait_time: number + is_unique: boolean + unique_hash: string + k1: string + open_time: number + usescsv: string + webhook_url?: string | null + webhook_headers?: string | null + webhook_body?: string | null + custom_url?: string | null + enabled?: boolean + lnurl?: string | null + lnurl_url?: string | null +} + +export interface UniqueHashEntry { + index: string + id_unique_hash: string +} + +export interface UniqueHashesResponse { + link_id: string + unique_hash: string + is_unique: boolean + unredeemed_hashes: UniqueHashEntry[] +} + +// ============================================================================ +// LNURL-pay (the `lnurlp` extension's transport surface) — kept for parity +// even though the ATM use case primarily exercises lnurlw. The shape is +// looser than the withdraw side; mirror what lnurlp.crud returns. +// ============================================================================ + +export interface LnbitsPayLink { + id: string + wallet: string + description: string + min: number + max: number + served_meta: number + served_pr: number + comment_chars: number + currency?: string | null + domain?: string | null + username?: string | null + zaps?: boolean + disposable?: boolean + webhook_url?: string | null + webhook_headers?: string | null + webhook_body?: string | null + success_text?: string | null + success_url?: string | null + fiat_base_multiplier?: number + created_at?: string + updated_at?: string +} + +// ============================================================================ +// Callbacks +// ============================================================================ + +/** Called once per subscription push event (after the initial ack). */ +export type PaymentPushCallback = (payment: LnbitsPayment) => void + +/** Called when the subscription has been closed (by TTL or explicit unsubscribe). */ +export type SubscriptionCloseCallback = (reason: 'ttl' | 'unsubscribed') => void diff --git a/packages/lnbits/tsconfig.json b/packages/lnbits/tsconfig.json new file mode 100644 index 0000000..d770c39 --- /dev/null +++ b/packages/lnbits/tsconfig.json @@ -0,0 +1,22 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "strictNullChecks": true, + "noUncheckedIndexedAccess": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "isolatedModules": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist", "**/*.test.ts"] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 79f2308..b2b55f8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -206,6 +206,25 @@ importers: specifier: ^2.1.0 version: 2.1.9(@types/node@22.19.7)(lightningcss@1.30.2) + packages/lnbits: + dependencies: + '@bitSpire/nostr-client': + specifier: workspace:* + version: link:../nostr-client + nostr-tools: + specifier: ^2.10.0 + version: 2.19.4(typescript@5.9.3) + devDependencies: + '@types/node': + specifier: ^22.0.0 + version: 22.19.7 + typescript: + specifier: ^5.7.0 + version: 5.9.3 + vitest: + specifier: ^2.1.0 + version: 2.1.9(@types/node@22.19.7)(lightningcss@1.30.2) + packages/nostr-client: dependencies: '@noble/curves': diff --git a/tsconfig.json b/tsconfig.json index 94775f3..47e52dc 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -26,6 +26,7 @@ "@bitSpire/clink": ["./packages/clink/src"], "@bitSpire/state-machine": ["./packages/state-machine/src"], "@bitSpire/lightning": ["./packages/lightning/src"], + "@bitSpire/lnbits": ["./packages/lnbits/src"], "@bitSpire/cashu": ["./packages/cashu/src"], "@bitSpire/ui-shared": ["./packages/ui-shared/src"], "@bitSpire/hal": ["./packages/hal"]