Align CLINK and Lightning.Pub packages with actual protocol specs
CLINK Protocol (@lamassu/clink):
- Add CLINKErrorCode enum (codes 1-6) matching Lightning.Pub
- Update OfferRequest to use { offer, amount_sats, payer_data, ... }
- Update OfferResponse to { bolt11 } or { code, error, range }
- Update DebitRequest to { amount_sats, memo, rules }
- Update DebitResponse to { res: 'ok'|'GFY', preimage?, error?, code? }
- Add ManagementRequest/Response with proper action types
- Add ServiceBeacon type for Kind 30078 discovery
- Add DebitRules for recurring payment authorization
- Change all amounts from millisatoshis to satoshis
- Add helper functions: createOfferSuccess, createOfferError, etc.
- Add type guards: isOfferError, isDebitFailure, isManagementFailure
Lightning.Pub RPC (@lamassu/lightning):
- Change from NIP-47 kinds (23194/23195) to Kind 21000 RPC
- Add proper RPC request format: { rpcName, body, authIdentifier, requestId, appId }
- Add RPC response handling: { status: 'OK'|'ERROR', reason?, ...result }
- Add LightningPubEventKind enum
- Add isRPCError type guard
- Remove unused types (ChannelInfo, NodeInfo)
- Update all amounts from millisatoshis to satoshis
This aligns our implementation with the actual Lightning.Pub source code
at /home/padreug/Work/tries/2026-01-22-lamassu-refactor-packages/Lightning.Pub
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
84eb605741
commit
d5a77c4311
7 changed files with 754 additions and 312 deletions
|
|
@ -6,6 +6,8 @@
|
|||
* - Handling offer requests and generating invoices
|
||||
* - Processing debit requests
|
||||
* - Receiving management commands
|
||||
*
|
||||
* Uses NIP-44v2 encryption for all messages.
|
||||
*/
|
||||
|
||||
import type { Event, UnsignedEvent } from 'nostr-tools'
|
||||
|
|
@ -14,16 +16,25 @@ import type { MachineIdentity, NostrClient } from '@lamassu/nostr-client'
|
|||
import { encryptContent, decryptJSON } from '@lamassu/nostr-client'
|
||||
import {
|
||||
CLINKEventKind,
|
||||
CLINKErrorCode,
|
||||
type CLINKOffer,
|
||||
type OfferRequest,
|
||||
type OfferResponse,
|
||||
type OfferSuccessResponse,
|
||||
type OfferErrorResponse,
|
||||
type DebitRequest,
|
||||
type DebitResponse,
|
||||
type ManagementDelegation,
|
||||
type DebitSuccessResponse,
|
||||
type DebitFailureResponse,
|
||||
type ManagementRequest,
|
||||
type ManagementResponse,
|
||||
type ServiceBeacon,
|
||||
type GenerateInvoice,
|
||||
type PayInvoice,
|
||||
isOfferError,
|
||||
isDebitFailure,
|
||||
} from './types.js'
|
||||
import { encodeNoffer } from './noffer.js'
|
||||
import { encodeNoffer, decodeNoffer } from './noffer.js'
|
||||
|
||||
/** CLINK client options */
|
||||
export interface CLINKClientOptions {
|
||||
|
|
@ -41,23 +52,23 @@ export interface CLINKClientOptions {
|
|||
payInvoice?: PayInvoice
|
||||
}
|
||||
|
||||
/** Offer request handler */
|
||||
/** Offer request handler - returns invoice or error */
|
||||
export type OfferRequestHandler = (
|
||||
request: OfferRequest,
|
||||
senderPubkey: string
|
||||
) => Promise<OfferResponse | null>
|
||||
|
||||
/** Debit request handler */
|
||||
/** Debit request handler - returns success/failure */
|
||||
export type DebitRequestHandler = (
|
||||
request: DebitRequest,
|
||||
senderPubkey: string
|
||||
) => Promise<DebitResponse>
|
||||
|
||||
/** Management command handler */
|
||||
/** Management request handler */
|
||||
export type ManagementHandler = (
|
||||
delegation: ManagementDelegation,
|
||||
request: ManagementRequest,
|
||||
senderPubkey: string
|
||||
) => Promise<void>
|
||||
) => Promise<ManagementResponse | null>
|
||||
|
||||
/**
|
||||
* CLINK protocol client for ATM payments
|
||||
|
|
@ -90,40 +101,47 @@ export class CLINKClient {
|
|||
*/
|
||||
createOffer(options: {
|
||||
priceType: CLINKOffer['priceType']
|
||||
amountMsat?: number
|
||||
amountSats?: number
|
||||
description?: string
|
||||
minAmountMsat?: number
|
||||
maxAmountMsat?: number
|
||||
minAmountSats?: number
|
||||
maxAmountSats?: number
|
||||
}): string {
|
||||
const offer: CLINKOffer = {
|
||||
pubkey: this.identity.publicKey,
|
||||
relays: this.relays,
|
||||
priceType: options.priceType,
|
||||
amountMsat: options.amountMsat,
|
||||
amountSats: options.amountSats,
|
||||
description: options.description,
|
||||
minAmountMsat: options.minAmountMsat,
|
||||
maxAmountMsat: options.maxAmountMsat,
|
||||
minAmountSats: options.minAmountSats,
|
||||
maxAmountSats: options.maxAmountSats,
|
||||
}
|
||||
|
||||
return encodeNoffer(offer)
|
||||
}
|
||||
|
||||
/**
|
||||
* Set handler for incoming offer requests
|
||||
* Decode a noffer string
|
||||
*/
|
||||
decodeOffer(noffer: string): CLINKOffer {
|
||||
return decodeNoffer(noffer)
|
||||
}
|
||||
|
||||
/**
|
||||
* Set handler for incoming offer requests (Kind 21001)
|
||||
*/
|
||||
onOfferRequest(handler: OfferRequestHandler): void {
|
||||
this.offerHandler = handler
|
||||
}
|
||||
|
||||
/**
|
||||
* Set handler for incoming debit requests
|
||||
* Set handler for incoming debit requests (Kind 21002)
|
||||
*/
|
||||
onDebitRequest(handler: DebitRequestHandler): void {
|
||||
this.debitHandler = handler
|
||||
}
|
||||
|
||||
/**
|
||||
* Set handler for management commands
|
||||
* Set handler for management commands (Kind 21003)
|
||||
*/
|
||||
onManagement(handler: ManagementHandler): void {
|
||||
this.managementHandler = handler
|
||||
|
|
@ -159,28 +177,59 @@ export class CLINKClient {
|
|||
}
|
||||
|
||||
/**
|
||||
* Send an offer request to another pubkey
|
||||
* Request an invoice from a noffer (Kind 21001)
|
||||
*/
|
||||
async requestOffer(targetPubkey: string, request: OfferRequest): Promise<OfferResponse> {
|
||||
const content = encryptContent(this.identity, targetPubkey, request)
|
||||
async requestOffer(
|
||||
noffer: string,
|
||||
amountSats: number,
|
||||
options?: {
|
||||
payerData?: Record<string, string>
|
||||
description?: string
|
||||
expiresInSeconds?: number
|
||||
}
|
||||
): Promise<OfferResponse> {
|
||||
const offer = decodeNoffer(noffer)
|
||||
|
||||
const request: OfferRequest = {
|
||||
offer: noffer,
|
||||
amount_sats: amountSats,
|
||||
payer_data: options?.payerData,
|
||||
description: options?.description,
|
||||
expires_in_seconds: options?.expiresInSeconds,
|
||||
}
|
||||
|
||||
const content = encryptContent(this.identity, offer.pubkey, request)
|
||||
|
||||
const event = this.createSignedEvent({
|
||||
kind: CLINKEventKind.Offer,
|
||||
content,
|
||||
tags: [['p', targetPubkey]],
|
||||
tags: [['p', offer.pubkey]],
|
||||
created_at: Math.floor(Date.now() / 1000),
|
||||
})
|
||||
|
||||
await this.nostrClient.publish(event)
|
||||
|
||||
// Wait for response
|
||||
return this.waitForResponse<OfferResponse>(targetPubkey, CLINKEventKind.Offer)
|
||||
return this.waitForResponse<OfferResponse>(offer.pubkey, CLINKEventKind.Offer, event.id)
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a debit request
|
||||
* Send a debit request (Kind 21002)
|
||||
*/
|
||||
async requestDebit(targetPubkey: string, request: DebitRequest): Promise<DebitResponse> {
|
||||
async requestDebit(
|
||||
targetPubkey: string,
|
||||
amountSats: number,
|
||||
options?: {
|
||||
memo?: string
|
||||
rules?: DebitRequest['rules']
|
||||
}
|
||||
): Promise<DebitResponse> {
|
||||
const request: DebitRequest = {
|
||||
amount_sats: amountSats,
|
||||
memo: options?.memo,
|
||||
rules: options?.rules,
|
||||
}
|
||||
|
||||
const content = encryptContent(this.identity, targetPubkey, request)
|
||||
|
||||
const event = this.createSignedEvent({
|
||||
|
|
@ -192,7 +241,50 @@ export class CLINKClient {
|
|||
|
||||
await this.nostrClient.publish(event)
|
||||
|
||||
return this.waitForResponse<DebitResponse>(targetPubkey, CLINKEventKind.Debit)
|
||||
return this.waitForResponse<DebitResponse>(targetPubkey, CLINKEventKind.Debit, event.id)
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a management request (Kind 21003)
|
||||
*/
|
||||
async sendManagementRequest(
|
||||
targetPubkey: string,
|
||||
request: ManagementRequest
|
||||
): Promise<ManagementResponse> {
|
||||
const content = encryptContent(this.identity, targetPubkey, request)
|
||||
|
||||
const event = this.createSignedEvent({
|
||||
kind: CLINKEventKind.Manage,
|
||||
content,
|
||||
tags: [['p', targetPubkey]],
|
||||
created_at: Math.floor(Date.now() / 1000),
|
||||
})
|
||||
|
||||
await this.nostrClient.publish(event)
|
||||
|
||||
return this.waitForResponse<ManagementResponse>(targetPubkey, CLINKEventKind.Manage, event.id)
|
||||
}
|
||||
|
||||
/**
|
||||
* Discover service beacon (Kind 30078)
|
||||
*/
|
||||
async discoverService(servicePubkey: string): Promise<ServiceBeacon | null> {
|
||||
const events = await this.nostrClient.queryEvents([
|
||||
{
|
||||
kinds: [CLINKEventKind.Beacon],
|
||||
authors: [servicePubkey],
|
||||
'#d': ['Lightning.Pub'],
|
||||
limit: 1,
|
||||
},
|
||||
])
|
||||
|
||||
if (events.length === 0) return null
|
||||
|
||||
try {
|
||||
return JSON.parse(events[0]?.content ?? '{}') as ServiceBeacon
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -217,7 +309,7 @@ export class CLINKClient {
|
|||
}
|
||||
|
||||
/**
|
||||
* Handle offer request/response
|
||||
* Handle offer request (Kind 21001)
|
||||
*/
|
||||
private async handleOfferEvent(event: Event): Promise<void> {
|
||||
if (!this.offerHandler) return
|
||||
|
|
@ -244,7 +336,7 @@ export class CLINKClient {
|
|||
}
|
||||
|
||||
/**
|
||||
* Handle debit request
|
||||
* Handle debit request (Kind 21002)
|
||||
*/
|
||||
private async handleDebitEvent(event: Event): Promise<void> {
|
||||
if (!this.debitHandler) return
|
||||
|
|
@ -270,7 +362,7 @@ export class CLINKClient {
|
|||
}
|
||||
|
||||
/**
|
||||
* Handle management command
|
||||
* Handle management command (Kind 21003)
|
||||
*/
|
||||
private async handleManageEvent(event: Event): Promise<void> {
|
||||
// Only accept from operator
|
||||
|
|
@ -281,15 +373,31 @@ export class CLINKClient {
|
|||
|
||||
if (!this.managementHandler) return
|
||||
|
||||
const delegation = decryptJSON<ManagementDelegation>(this.identity, event.pubkey, event.content)
|
||||
const request = decryptJSON<ManagementRequest>(this.identity, event.pubkey, event.content)
|
||||
|
||||
await this.managementHandler(delegation, event.pubkey)
|
||||
const response = await this.managementHandler(request, event.pubkey)
|
||||
if (!response) return
|
||||
|
||||
// Send encrypted response
|
||||
const content = encryptContent(this.identity, event.pubkey, response)
|
||||
|
||||
const responseEvent = this.createSignedEvent({
|
||||
kind: CLINKEventKind.Manage,
|
||||
content,
|
||||
tags: [
|
||||
['p', event.pubkey],
|
||||
['e', event.id],
|
||||
],
|
||||
created_at: Math.floor(Date.now() / 1000),
|
||||
})
|
||||
|
||||
await this.nostrClient.publish(responseEvent)
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for a response event
|
||||
*/
|
||||
private waitForResponse<T>(fromPubkey: string, kind: number): Promise<T> {
|
||||
private waitForResponse<T>(fromPubkey: string, kind: number, requestEventId: string): Promise<T> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const timeout = setTimeout(() => {
|
||||
this.nostrClient.unsubscribe(subId)
|
||||
|
|
@ -302,6 +410,7 @@ export class CLINKClient {
|
|||
kinds: [kind],
|
||||
authors: [fromPubkey],
|
||||
'#p': [this.identity.publicKey],
|
||||
'#e': [requestEventId],
|
||||
since: Math.floor(Date.now() / 1000) - 5,
|
||||
},
|
||||
],
|
||||
|
|
@ -329,3 +438,42 @@ export class CLINKClient {
|
|||
return finalizeEvent(event, this.identity.privateKey)
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Helper functions for creating responses
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Create an offer success response
|
||||
*/
|
||||
export function createOfferSuccess(bolt11: string): OfferSuccessResponse {
|
||||
return { bolt11 }
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an offer error response
|
||||
*/
|
||||
export function createOfferError(
|
||||
code: CLINKErrorCode,
|
||||
error: string,
|
||||
range?: { min: number; max: number }
|
||||
): OfferErrorResponse {
|
||||
return { code, error, range }
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a debit success response
|
||||
*/
|
||||
export function createDebitSuccess(preimage: string): DebitSuccessResponse {
|
||||
return { res: 'ok', preimage }
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a debit failure response
|
||||
*/
|
||||
export function createDebitFailure(code: CLINKErrorCode, error: string): DebitFailureResponse {
|
||||
return { res: 'GFY', code, error }
|
||||
}
|
||||
|
||||
// Re-export type guards
|
||||
export { isOfferError, isDebitFailure }
|
||||
|
|
|
|||
|
|
@ -3,12 +3,18 @@
|
|||
*
|
||||
* CLINK protocol implementation for Nostr-native Lightning payments.
|
||||
*
|
||||
* CLINK enables static payment codes (noffers) that work over Nostr,
|
||||
* providing a decentralized alternative to BOLT12 offers.
|
||||
* CLINK enables:
|
||||
* - Static payment codes (noffers) for receiving payments
|
||||
* - Offer requests/responses for invoice generation (Kind 21001)
|
||||
* - Debit authorization for outgoing payments (Kind 21002)
|
||||
* - Management delegation for remote control (Kind 21003)
|
||||
* - Service discovery via beacons (Kind 30078)
|
||||
*
|
||||
* All CLINK messages use NIP-44v2 encryption.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { CLINKClient, encodeNoffer, decodeNoffer } from '@lamassu/clink'
|
||||
* import { CLINKClient, encodeNoffer, createOfferSuccess } from '@lamassu/clink'
|
||||
*
|
||||
* // Create a client
|
||||
* const clink = new CLINKClient({
|
||||
|
|
@ -26,42 +32,66 @@
|
|||
*
|
||||
* // Handle incoming offer requests
|
||||
* clink.onOfferRequest(async (request, senderPubkey) => {
|
||||
* const invoice = await generateInvoice(request.amountMsat)
|
||||
* return {
|
||||
* invoice,
|
||||
* amountMsat: request.amountMsat,
|
||||
* expiresAt: Date.now() / 1000 + 600,
|
||||
* }
|
||||
* const bolt11 = await generateInvoice(request.amount_sats)
|
||||
* return createOfferSuccess(bolt11)
|
||||
* })
|
||||
*
|
||||
* clink.startListening()
|
||||
* ```
|
||||
*/
|
||||
|
||||
// Client
|
||||
export { CLINKClient } from './client.js'
|
||||
export type {
|
||||
CLINKClientOptions,
|
||||
OfferRequestHandler,
|
||||
DebitRequestHandler,
|
||||
ManagementHandler,
|
||||
} from './client.js'
|
||||
|
||||
// noffer encoding/decoding
|
||||
export { encodeNoffer, decodeNoffer, isValidNoffer } from './noffer.js'
|
||||
|
||||
// Types
|
||||
export {
|
||||
// Event kinds
|
||||
CLINKEventKind,
|
||||
NofferTLV,
|
||||
type PriceType,
|
||||
type CLINKOffer,
|
||||
CLINKErrorCode,
|
||||
// Offer types (Kind 21001)
|
||||
type OfferRequest,
|
||||
type OfferResponse,
|
||||
type OfferSuccessResponse,
|
||||
type OfferErrorResponse,
|
||||
// Debit types (Kind 21002)
|
||||
type DebitRequest,
|
||||
type DebitResponse,
|
||||
type DebitSuccessResponse,
|
||||
type DebitFailureResponse,
|
||||
type DebitRules,
|
||||
type DebitInterval,
|
||||
// Management types (Kind 21003)
|
||||
type ManagementRequest,
|
||||
type ManagementResponse,
|
||||
type ManagementSuccessResponse,
|
||||
type ManagementFailureResponse,
|
||||
type ManagementAction,
|
||||
type ManagementDelegation,
|
||||
type OfferConfig,
|
||||
// Beacon types (Kind 30078)
|
||||
type ServiceBeacon,
|
||||
// Noffer types
|
||||
type CLINKOffer,
|
||||
type PriceType,
|
||||
NofferTLV,
|
||||
// Function types
|
||||
type GenerateInvoice,
|
||||
type PayInvoice,
|
||||
// Type guards
|
||||
isOfferError,
|
||||
isDebitFailure,
|
||||
isManagementFailure,
|
||||
} from './types.js'
|
||||
|
||||
// Noffer encoding/decoding
|
||||
export { encodeNoffer, decodeNoffer, isValidNoffer } from './noffer.js'
|
||||
|
||||
// Client
|
||||
export {
|
||||
CLINKClient,
|
||||
type CLINKClientOptions,
|
||||
type OfferRequestHandler,
|
||||
type DebitRequestHandler,
|
||||
type ManagementHandler,
|
||||
// Response helpers
|
||||
createOfferSuccess,
|
||||
createOfferError,
|
||||
createDebitSuccess,
|
||||
createDebitFailure,
|
||||
} from './client.js'
|
||||
|
|
|
|||
|
|
@ -34,9 +34,9 @@ export function encodeNoffer(offer: CLINKOffer): string {
|
|||
const priceTypeByte = encodePriceType(offer.priceType)
|
||||
writeTLV(tlvData, NofferTLV.PriceType, [priceTypeByte])
|
||||
|
||||
// Amount (TLV 3) - if present
|
||||
if (offer.amountMsat !== undefined) {
|
||||
const amountBytes = encodeUint64BE(offer.amountMsat)
|
||||
// Amount (TLV 3) - if present (in satoshis)
|
||||
if (offer.amountSats !== undefined) {
|
||||
const amountBytes = encodeUint64BE(offer.amountSats)
|
||||
writeTLV(tlvData, NofferTLV.Amount, amountBytes)
|
||||
}
|
||||
|
||||
|
|
@ -71,7 +71,7 @@ export function decodeNoffer(noffer: string): CLINKOffer {
|
|||
let pubkey: string | undefined
|
||||
const relays: string[] = []
|
||||
let priceType: PriceType = 'spontaneous'
|
||||
let amountMsat: number | undefined
|
||||
let amountSats: number | undefined
|
||||
let description: string | undefined
|
||||
|
||||
let offset = 0
|
||||
|
|
@ -102,7 +102,7 @@ export function decodeNoffer(noffer: string): CLINKOffer {
|
|||
priceType = decodePriceType(value[0] ?? 2)
|
||||
break
|
||||
case NofferTLV.Amount:
|
||||
amountMsat = decodeUint64BE(value)
|
||||
amountSats = decodeUint64BE(value)
|
||||
break
|
||||
case NofferTLV.Description:
|
||||
description = new TextDecoder().decode(value)
|
||||
|
|
@ -123,7 +123,7 @@ export function decodeNoffer(noffer: string): CLINKOffer {
|
|||
pubkey,
|
||||
relays,
|
||||
priceType,
|
||||
amountMsat,
|
||||
amountSats,
|
||||
description,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,22 +5,236 @@
|
|||
* static payment codes (noffers) over Nostr relays.
|
||||
*
|
||||
* Event kinds:
|
||||
* - 21000: Generic RPC request/response
|
||||
* - 21001: Offer request/response
|
||||
* - 21002: Debit request/response
|
||||
* - 21003: Management delegation
|
||||
* - 21003: Management request/response
|
||||
* - 30078: Service beacon (replaceable)
|
||||
*
|
||||
* All CLINK messages use NIP-44v2 encryption.
|
||||
*/
|
||||
|
||||
/** CLINK event kinds */
|
||||
export enum CLINKEventKind {
|
||||
/** Generic RPC request/response */
|
||||
RPC = 21000,
|
||||
/** Offer request/response */
|
||||
Offer = 21001,
|
||||
/** Debit request/response (authorized payments) */
|
||||
Debit = 21002,
|
||||
/** Management delegation */
|
||||
/** Management request/response */
|
||||
Manage = 21003,
|
||||
/** Service beacon (replaceable event) */
|
||||
Beacon = 30078,
|
||||
}
|
||||
|
||||
/** Offer price type */
|
||||
/** CLINK error codes */
|
||||
export enum CLINKErrorCode {
|
||||
/** Request was denied (warning) */
|
||||
RequestDenied = 1,
|
||||
/** Temporary failure, may retry */
|
||||
TemporaryFailure = 2,
|
||||
/** Request has expired */
|
||||
ExpiredRequest = 3,
|
||||
/** Rate limited */
|
||||
RateLimited = 4,
|
||||
/** Invalid amount (out of range) */
|
||||
InvalidAmount = 5,
|
||||
/** Invalid request format */
|
||||
InvalidRequest = 6,
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Kind 21001: Offer (Noffer)
|
||||
// ============================================================================
|
||||
|
||||
/** Offer request (Kind 21001) - sent by payer to service */
|
||||
export interface OfferRequest {
|
||||
/** Offer identifier from service beacon or noffer string */
|
||||
offer: string
|
||||
/** Amount in satoshis */
|
||||
amount_sats: number
|
||||
/** Custom payer metadata */
|
||||
payer_data?: Record<string, string>
|
||||
/** Payment description */
|
||||
description?: string
|
||||
/** Custom expiry in seconds */
|
||||
expires_in_seconds?: number
|
||||
/** Whether this is a zap payment */
|
||||
zap?: boolean
|
||||
}
|
||||
|
||||
/** Offer success response (Kind 21001) */
|
||||
export interface OfferSuccessResponse {
|
||||
/** BOLT-11 Lightning invoice */
|
||||
bolt11: string
|
||||
}
|
||||
|
||||
/** Offer error response (Kind 21001) */
|
||||
export interface OfferErrorResponse {
|
||||
/** Error code */
|
||||
code: CLINKErrorCode
|
||||
/** Human-readable error message */
|
||||
error: string
|
||||
/** Acceptable amount range (for InvalidAmount errors) */
|
||||
range?: { min: number; max: number }
|
||||
}
|
||||
|
||||
/** Offer response - either success or error */
|
||||
export type OfferResponse = OfferSuccessResponse | OfferErrorResponse
|
||||
|
||||
/** Type guard for offer error response */
|
||||
export function isOfferError(response: OfferResponse): response is OfferErrorResponse {
|
||||
return 'code' in response && 'error' in response
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Kind 21002: Debit (Ndebit)
|
||||
// ============================================================================
|
||||
|
||||
/** Debit frequency interval */
|
||||
export type DebitInterval = 'day' | 'week' | 'month'
|
||||
|
||||
/** Debit rules for recurring payments */
|
||||
export interface DebitRules {
|
||||
/** Expiration rule */
|
||||
expiration?: {
|
||||
/** Unix timestamp when authorization expires */
|
||||
expires_at_unix: number
|
||||
}
|
||||
/** Frequency rule for recurring debits */
|
||||
frequency?: {
|
||||
/** Number of intervals allowed */
|
||||
number_of_intervals: number
|
||||
/** Interval type */
|
||||
interval: DebitInterval
|
||||
/** Maximum amount per interval in sats */
|
||||
amount: number
|
||||
}
|
||||
}
|
||||
|
||||
/** Debit request (Kind 21002) - sent by service to request payment authorization */
|
||||
export interface DebitRequest {
|
||||
/** Amount in satoshis */
|
||||
amount_sats: number
|
||||
/** Optional memo/description */
|
||||
memo?: string
|
||||
/** Rules for recurring debits */
|
||||
rules?: DebitRules
|
||||
}
|
||||
|
||||
/** Debit success response (Kind 21002) */
|
||||
export interface DebitSuccessResponse {
|
||||
/** Success indicator */
|
||||
res: 'ok'
|
||||
/** Payment preimage (proof of payment) */
|
||||
preimage: string
|
||||
}
|
||||
|
||||
/** Debit failure response (Kind 21002) */
|
||||
export interface DebitFailureResponse {
|
||||
/** Failure indicator (Generic Failure) */
|
||||
res: 'GFY'
|
||||
/** Error message */
|
||||
error: string
|
||||
/** Error code */
|
||||
code: CLINKErrorCode
|
||||
}
|
||||
|
||||
/** Debit response - either success or failure */
|
||||
export type DebitResponse = DebitSuccessResponse | DebitFailureResponse
|
||||
|
||||
/** Type guard for debit failure */
|
||||
export function isDebitFailure(response: DebitResponse): response is DebitFailureResponse {
|
||||
return response.res === 'GFY'
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Kind 21003: Management (Nmanage)
|
||||
// ============================================================================
|
||||
|
||||
/** Management action types */
|
||||
export type ManagementAction = 'create' | 'update' | 'delete' | 'list' | 'get'
|
||||
|
||||
/** Offer configuration for create/update */
|
||||
export interface OfferConfig {
|
||||
/** Human-readable label */
|
||||
label?: string
|
||||
/** Fixed price in satoshis */
|
||||
price_sats?: number
|
||||
/** Callback URL for payment notifications */
|
||||
callback_url?: string
|
||||
/** Expected payer data fields */
|
||||
payer_data?: string[]
|
||||
/** Use blinded paths for privacy */
|
||||
blind?: boolean
|
||||
}
|
||||
|
||||
/** Management request (Kind 21003) */
|
||||
export interface ManagementRequest {
|
||||
/** Action to perform */
|
||||
action: ManagementAction
|
||||
/** App user identifier/pointer */
|
||||
pointer?: string
|
||||
/** Offer configuration (for create/update) */
|
||||
offer?: OfferConfig
|
||||
/** Offer ID (for update/delete/get) */
|
||||
offer_id?: string
|
||||
}
|
||||
|
||||
/** Management success response (Kind 21003) */
|
||||
export interface ManagementSuccessResponse {
|
||||
/** Action that was performed */
|
||||
action: ManagementAction
|
||||
/** Result data (varies by action) */
|
||||
result: unknown
|
||||
}
|
||||
|
||||
/** Management failure response (Kind 21003) */
|
||||
export interface ManagementFailureResponse {
|
||||
/** Failure indicator */
|
||||
res: 'GFY'
|
||||
/** Error message */
|
||||
error: string
|
||||
/** Error code */
|
||||
code: CLINKErrorCode
|
||||
/** Retry after (seconds) for rate limiting */
|
||||
retry_after?: number
|
||||
}
|
||||
|
||||
/** Management response */
|
||||
export type ManagementResponse = ManagementSuccessResponse | ManagementFailureResponse
|
||||
|
||||
/** Type guard for management failure */
|
||||
export function isManagementFailure(
|
||||
response: ManagementResponse
|
||||
): response is ManagementFailureResponse {
|
||||
return 'res' in response && response.res === 'GFY'
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Kind 30078: Service Beacon
|
||||
// ============================================================================
|
||||
|
||||
/** Service beacon content (Kind 30078) */
|
||||
export interface ServiceBeacon {
|
||||
/** Beacon type */
|
||||
type: 'service' | 'provider'
|
||||
/** Service name */
|
||||
name: string
|
||||
/** Service avatar URL */
|
||||
avatarUrl?: string
|
||||
/** Fee structure */
|
||||
fees?: Record<string, number>
|
||||
/** Optional relay URL for additional communication */
|
||||
nextRelay?: string
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Noffer (Static Payment Code) encoding
|
||||
// ============================================================================
|
||||
|
||||
/** Offer price type for noffer encoding */
|
||||
export type PriceType = 'fixed' | 'variable' | 'spontaneous'
|
||||
|
||||
/** CLINK Offer (noffer) configuration */
|
||||
|
|
@ -31,81 +245,14 @@ export interface CLINKOffer {
|
|||
relays: string[]
|
||||
/** Pricing model */
|
||||
priceType: PriceType
|
||||
/** Amount in millisatoshis (for fixed price) */
|
||||
amountMsat?: number
|
||||
/** Amount in satoshis (for fixed price) */
|
||||
amountSats?: number
|
||||
/** Human-readable description */
|
||||
description?: string
|
||||
/** Minimum amount in msats (for variable/spontaneous) */
|
||||
minAmountMsat?: number
|
||||
/** Maximum amount in msats (for variable/spontaneous) */
|
||||
maxAmountMsat?: number
|
||||
}
|
||||
|
||||
/** Offer request (Kind 21001) */
|
||||
export interface OfferRequest {
|
||||
/** Requested amount in millisatoshis */
|
||||
amountMsat: number
|
||||
/** Fiat amount (for ATM context) */
|
||||
fiatAmount?: number
|
||||
/** Fiat currency code */
|
||||
fiatCurrency?: string
|
||||
/** Sender's callback pubkey for response */
|
||||
senderPubkey: string
|
||||
/** Optional message */
|
||||
message?: string
|
||||
}
|
||||
|
||||
/** Offer response (Kind 21001) */
|
||||
export interface OfferResponse {
|
||||
/** BOLT11 invoice to pay */
|
||||
invoice: string
|
||||
/** Amount in millisatoshis */
|
||||
amountMsat: number
|
||||
/** Optional description */
|
||||
description?: string
|
||||
/** Expiry timestamp */
|
||||
expiresAt: number
|
||||
}
|
||||
|
||||
/** Debit request (Kind 21002) */
|
||||
export interface DebitRequest {
|
||||
/** Amount to debit in millisatoshis */
|
||||
amountMsat: number
|
||||
/** Recipient's pubkey */
|
||||
recipientPubkey: string
|
||||
/** Optional description */
|
||||
description?: string
|
||||
/** Unique request ID for idempotency */
|
||||
requestId: string
|
||||
}
|
||||
|
||||
/** Debit response (Kind 21002) */
|
||||
export interface DebitResponse {
|
||||
/** Whether the debit was successful */
|
||||
success: boolean
|
||||
/** Payment preimage (proof of payment) */
|
||||
preimage?: string
|
||||
/** Error message if failed */
|
||||
error?: string
|
||||
/** Matching request ID */
|
||||
requestId: string
|
||||
}
|
||||
|
||||
/** Management action (Kind 21003) */
|
||||
export type ManagementAction =
|
||||
| { type: 'disable' }
|
||||
| { type: 'enable' }
|
||||
| { type: 'set_limits'; minMsat: number; maxMsat: number }
|
||||
| { type: 'revoke' }
|
||||
|
||||
/** Management delegation (Kind 21003) */
|
||||
export interface ManagementDelegation {
|
||||
/** Target machine/account pubkey */
|
||||
targetPubkey: string
|
||||
/** Delegated action */
|
||||
action: ManagementAction
|
||||
/** Timestamp */
|
||||
timestamp: number
|
||||
/** Minimum amount in sats (for variable/spontaneous) */
|
||||
minAmountSats?: number
|
||||
/** Maximum amount in sats (for variable/spontaneous) */
|
||||
maxAmountSats?: number
|
||||
}
|
||||
|
||||
/** noffer TLV types */
|
||||
|
|
@ -116,14 +263,18 @@ export enum NofferTLV {
|
|||
Relay = 1,
|
||||
/** Price type (1 byte: 0=fixed, 1=variable, 2=spontaneous) */
|
||||
PriceType = 2,
|
||||
/** Amount in msats (8 bytes, big-endian) */
|
||||
/** Amount in sats (8 bytes, big-endian) */
|
||||
Amount = 3,
|
||||
/** Description (variable length string) */
|
||||
Description = 4,
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Function types for service injection
|
||||
// ============================================================================
|
||||
|
||||
/** Invoice generation function type */
|
||||
export type GenerateInvoice = (amountMsat: number) => Promise<string>
|
||||
export type GenerateInvoice = (amountSats: number, description?: string) => Promise<string>
|
||||
|
||||
/** Payment function type */
|
||||
export type PayInvoice = (invoice: string) => Promise<{ preimage: string } | { error: string }>
|
||||
|
|
|
|||
|
|
@ -5,52 +5,41 @@
|
|||
* account system that wraps LND.
|
||||
*
|
||||
* Lightning.Pub provides:
|
||||
* - Account management (sublayers)
|
||||
* - Invoice generation
|
||||
* - Payment processing
|
||||
* - CLINK protocol support
|
||||
* - Account management
|
||||
* - Invoice generation via RPC (kind 21000)
|
||||
* - Payment processing via RPC (kind 21000)
|
||||
* - CLINK protocol support (kinds 21001-21003)
|
||||
*
|
||||
* This client handles the RPC layer. For CLINK payments,
|
||||
* use @lamassu/clink.
|
||||
*/
|
||||
|
||||
import type { Event } from 'nostr-tools'
|
||||
import type { Event, UnsignedEvent } from 'nostr-tools'
|
||||
import { finalizeEvent } from 'nostr-tools'
|
||||
import type { MachineIdentity, NostrClient } from '@lamassu/nostr-client'
|
||||
import { encryptContent, decryptJSON } from '@lamassu/nostr-client'
|
||||
import type {
|
||||
LightningPubConfig,
|
||||
AccountInfo,
|
||||
Invoice,
|
||||
PaymentResult,
|
||||
ExchangeRate,
|
||||
InvoiceCallback,
|
||||
import {
|
||||
LightningPubEventKind,
|
||||
type LightningPubConfig,
|
||||
type RPCRequest,
|
||||
type RPCResponse,
|
||||
type AccountInfo,
|
||||
type BalanceResponse,
|
||||
type Invoice,
|
||||
type CreateInvoiceParams,
|
||||
type CreateInvoiceResponse,
|
||||
type LookupInvoiceResponse,
|
||||
type PaymentResult,
|
||||
type PayInvoiceResponse,
|
||||
type ExchangeRate,
|
||||
type InvoiceCallback,
|
||||
isRPCError,
|
||||
} from './types.js'
|
||||
|
||||
/** Event kinds for Lightning.Pub communication */
|
||||
const LIGHTNING_PUB_KINDS = {
|
||||
REQUEST: 23194, // NIP-47 compatible
|
||||
RESPONSE: 23195,
|
||||
}
|
||||
|
||||
/** Request types */
|
||||
type RequestMethod =
|
||||
| 'get_info'
|
||||
| 'get_balance'
|
||||
| 'make_invoice'
|
||||
| 'pay_invoice'
|
||||
| 'lookup_invoice'
|
||||
| 'list_transactions'
|
||||
|
||||
interface Request {
|
||||
method: RequestMethod
|
||||
params: Record<string, unknown>
|
||||
}
|
||||
|
||||
interface Response<T = unknown> {
|
||||
result?: T
|
||||
error?: { code: number; message: string }
|
||||
}
|
||||
|
||||
/**
|
||||
* Lightning.Pub client for ATM operations
|
||||
*
|
||||
* Uses Kind 21000 for RPC communication with Lightning.Pub service.
|
||||
*/
|
||||
export class LightningPubClient {
|
||||
private config: Required<LightningPubConfig>
|
||||
|
|
@ -58,10 +47,12 @@ export class LightningPubClient {
|
|||
private identity: MachineIdentity | null = null
|
||||
private invoiceCallbacks: Map<string, InvoiceCallback> = new Map()
|
||||
private subscriptionId?: string
|
||||
private requestCounter = 0
|
||||
|
||||
constructor(config: LightningPubConfig) {
|
||||
this.config = {
|
||||
timeout: 30000,
|
||||
appId: 'lamassu-atm',
|
||||
...config,
|
||||
}
|
||||
}
|
||||
|
|
@ -79,51 +70,39 @@ export class LightningPubClient {
|
|||
* Get account information
|
||||
*/
|
||||
async getAccountInfo(): Promise<AccountInfo> {
|
||||
const response = await this.sendRequest<AccountInfo>({
|
||||
method: 'get_info',
|
||||
params: {},
|
||||
})
|
||||
const response = await this.sendRPC<AccountInfo>('GetInfo', {})
|
||||
|
||||
return response
|
||||
return {
|
||||
pubkey: response.pubkey ?? this.config.accountPubkey,
|
||||
balanceSats: response.balanceSats ?? 0,
|
||||
maxSendSats: response.maxSendSats ?? 0,
|
||||
maxReceiveSats: response.maxReceiveSats ?? 0,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current balance
|
||||
*/
|
||||
async getBalance(): Promise<{ balanceSats: number }> {
|
||||
const response = await this.sendRequest<{ balance: number }>({
|
||||
method: 'get_balance',
|
||||
params: {},
|
||||
})
|
||||
|
||||
return { balanceSats: response.balance }
|
||||
const response = await this.sendRPC<BalanceResponse>('GetBalance', {})
|
||||
return { balanceSats: response.balance ?? 0 }
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a Lightning invoice
|
||||
*/
|
||||
async createInvoice(params: {
|
||||
amountMsat: number
|
||||
description?: string
|
||||
expirySecs?: number
|
||||
}): Promise<Invoice> {
|
||||
const response = await this.sendRequest<{
|
||||
payment_request: string
|
||||
payment_hash: string
|
||||
expires_at: number
|
||||
}>({
|
||||
method: 'make_invoice',
|
||||
params: {
|
||||
amount: params.amountMsat,
|
||||
description: params.description || 'ATM Payment',
|
||||
expiry: params.expirySecs || 600,
|
||||
},
|
||||
async createInvoice(params: CreateInvoiceParams): Promise<Invoice> {
|
||||
const response = await this.sendRPC<CreateInvoiceResponse>('NewInvoice', {
|
||||
amount: params.amountSats,
|
||||
memo: params.description || 'ATM Payment',
|
||||
expiry: params.expirySecs || 3600,
|
||||
private: params.privateHints ?? false,
|
||||
})
|
||||
|
||||
return {
|
||||
paymentRequest: response.payment_request,
|
||||
paymentHash: response.payment_hash,
|
||||
amountMsat: params.amountMsat,
|
||||
amountSats: params.amountSats,
|
||||
description: params.description,
|
||||
createdAt: Math.floor(Date.now() / 1000),
|
||||
expiresAt: response.expires_at,
|
||||
|
|
@ -136,20 +115,14 @@ export class LightningPubClient {
|
|||
*/
|
||||
async payInvoice(paymentRequest: string): Promise<PaymentResult> {
|
||||
try {
|
||||
const response = await this.sendRequest<{
|
||||
preimage: string
|
||||
fee_paid?: number
|
||||
}>({
|
||||
method: 'pay_invoice',
|
||||
params: {
|
||||
invoice: paymentRequest,
|
||||
},
|
||||
const response = await this.sendRPC<PayInvoiceResponse>('PayInvoice', {
|
||||
invoice: paymentRequest,
|
||||
})
|
||||
|
||||
return {
|
||||
success: true,
|
||||
preimage: response.preimage,
|
||||
feeMsat: response.fee_paid,
|
||||
feeSats: response.fee_paid,
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
|
|
@ -164,25 +137,14 @@ export class LightningPubClient {
|
|||
*/
|
||||
async lookupInvoice(paymentHash: string): Promise<Invoice | null> {
|
||||
try {
|
||||
const response = await this.sendRequest<{
|
||||
payment_request: string
|
||||
amount: number
|
||||
description: string
|
||||
created_at: number
|
||||
expires_at: number
|
||||
settled: boolean
|
||||
preimage?: string
|
||||
}>({
|
||||
method: 'lookup_invoice',
|
||||
params: {
|
||||
payment_hash: paymentHash,
|
||||
},
|
||||
const response = await this.sendRPC<LookupInvoiceResponse>('LookupInvoice', {
|
||||
payment_hash: paymentHash,
|
||||
})
|
||||
|
||||
return {
|
||||
paymentRequest: response.payment_request,
|
||||
paymentHash,
|
||||
amountMsat: response.amount,
|
||||
amountSats: response.amount,
|
||||
description: response.description,
|
||||
createdAt: response.created_at,
|
||||
expiresAt: response.expires_at,
|
||||
|
|
@ -228,42 +190,43 @@ export class LightningPubClient {
|
|||
}
|
||||
|
||||
/**
|
||||
* Decode a BOLT11 invoice
|
||||
* Decode a BOLT11 invoice (basic parsing)
|
||||
*
|
||||
* For production use, consider using a proper bolt11 library.
|
||||
*/
|
||||
decodeInvoice(paymentRequest: string): {
|
||||
amountMsat: number | null
|
||||
amountSats: number | null
|
||||
paymentHash: string
|
||||
description: string
|
||||
expiresAt: number
|
||||
} {
|
||||
// Basic invoice parsing (for production, use bolt11 library)
|
||||
// This is a simplified version
|
||||
// Basic invoice parsing
|
||||
const amountMatch = paymentRequest.match(/lnbc(\d+)([munp]?)/)
|
||||
let amountMsat: number | null = null
|
||||
let amountSats: number | null = null
|
||||
|
||||
if (amountMatch) {
|
||||
const [, amount, multiplier] = amountMatch
|
||||
const baseAmount = parseInt(amount ?? '0', 10)
|
||||
switch (multiplier) {
|
||||
case 'm':
|
||||
amountMsat = baseAmount * 100_000_000
|
||||
amountSats = baseAmount * 100_000 // milli-BTC to sats
|
||||
break
|
||||
case 'u':
|
||||
amountMsat = baseAmount * 100_000
|
||||
amountSats = baseAmount * 100 // micro-BTC to sats
|
||||
break
|
||||
case 'n':
|
||||
amountMsat = baseAmount * 100
|
||||
amountSats = Math.floor(baseAmount / 10) // nano-BTC to sats
|
||||
break
|
||||
case 'p':
|
||||
amountMsat = baseAmount / 10
|
||||
amountSats = Math.floor(baseAmount / 10_000) // pico-BTC to sats
|
||||
break
|
||||
default:
|
||||
amountMsat = baseAmount * 100_000_000_000
|
||||
amountSats = baseAmount * 100_000_000 // BTC to sats
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
amountMsat,
|
||||
amountSats,
|
||||
paymentHash: '', // Would need proper parsing
|
||||
description: '',
|
||||
expiresAt: Math.floor(Date.now() / 1000) + 3600,
|
||||
|
|
@ -271,11 +234,12 @@ export class LightningPubClient {
|
|||
}
|
||||
|
||||
/**
|
||||
* Get exchange rate (via external service or Lightning.Pub)
|
||||
* Get exchange rate (mock implementation)
|
||||
*
|
||||
* For production, integrate with price feeds or Lightning.Pub's rate service.
|
||||
*/
|
||||
async getExchangeRate(currency: string): Promise<ExchangeRate> {
|
||||
// For now, return a mock rate
|
||||
// In production, integrate with price feeds
|
||||
// Mock rates - in production, fetch from service
|
||||
const mockRates: Record<string, number> = {
|
||||
USD: 2500, // sats per dollar (example)
|
||||
EUR: 2700,
|
||||
|
|
@ -291,7 +255,7 @@ export class LightningPubClient {
|
|||
}
|
||||
|
||||
/**
|
||||
* Start listening for responses
|
||||
* Start listening for RPC responses
|
||||
*/
|
||||
private startListening(): void {
|
||||
if (!this.nostrClient || !this.identity) return
|
||||
|
|
@ -300,8 +264,9 @@ export class LightningPubClient {
|
|||
this.subscriptionId = this.nostrClient.subscribe(
|
||||
[
|
||||
{
|
||||
kinds: [LIGHTNING_PUB_KINDS.RESPONSE],
|
||||
kinds: [LightningPubEventKind.RPC],
|
||||
'#p': [this.identity.publicKey],
|
||||
authors: [this.config.accountPubkey],
|
||||
},
|
||||
],
|
||||
{
|
||||
|
|
@ -313,25 +278,35 @@ export class LightningPubClient {
|
|||
/**
|
||||
* Handle incoming response events
|
||||
*/
|
||||
private handleResponse(event: Event): void {
|
||||
// Handle async responses if needed
|
||||
// For invoice watching, we use polling instead
|
||||
private handleResponse(_event: Event): void {
|
||||
// Responses are handled by waitForResponse
|
||||
// This is for async notifications if needed
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a request to Lightning.Pub
|
||||
* Send an RPC request to Lightning.Pub
|
||||
*/
|
||||
private async sendRequest<T>(request: Request): Promise<T> {
|
||||
private async sendRPC<T>(rpcName: string, body: unknown): Promise<T> {
|
||||
if (!this.nostrClient || !this.identity) {
|
||||
throw new Error('Client not initialized')
|
||||
}
|
||||
|
||||
const requestId = this.generateRequestId()
|
||||
|
||||
const request: RPCRequest = {
|
||||
rpcName,
|
||||
body,
|
||||
authIdentifier: this.identity.publicKey,
|
||||
requestId,
|
||||
appId: this.config.appId,
|
||||
}
|
||||
|
||||
const content = encryptContent(this.identity, this.config.accountPubkey, request)
|
||||
|
||||
// finalizeEvent derives pubkey from the secret key
|
||||
const event = finalizeEvent(
|
||||
{
|
||||
kind: LIGHTNING_PUB_KINDS.REQUEST,
|
||||
kind: LightningPubEventKind.RPC,
|
||||
content,
|
||||
tags: [['p', this.config.accountPubkey]],
|
||||
created_at: Math.floor(Date.now() / 1000),
|
||||
|
|
@ -342,13 +317,13 @@ export class LightningPubClient {
|
|||
await this.nostrClient.publish(event)
|
||||
|
||||
// Wait for response
|
||||
return this.waitForResponse<T>(event.id)
|
||||
return this.waitForResponse<T>(event.id, requestId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for a response to a specific request
|
||||
*/
|
||||
private waitForResponse<T>(requestId: string): Promise<T> {
|
||||
private waitForResponse<T>(eventId: string, requestId: string): Promise<T> {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!this.nostrClient || !this.identity) {
|
||||
reject(new Error('Client not initialized'))
|
||||
|
|
@ -363,9 +338,11 @@ export class LightningPubClient {
|
|||
const subId = this.nostrClient.subscribe(
|
||||
[
|
||||
{
|
||||
kinds: [LIGHTNING_PUB_KINDS.RESPONSE],
|
||||
'#e': [requestId],
|
||||
kinds: [LightningPubEventKind.RPC],
|
||||
authors: [this.config.accountPubkey],
|
||||
'#p': [this.identity!.publicKey],
|
||||
'#e': [eventId],
|
||||
since: Math.floor(Date.now() / 1000) - 5,
|
||||
},
|
||||
],
|
||||
{
|
||||
|
|
@ -374,12 +351,18 @@ export class LightningPubClient {
|
|||
this.nostrClient!.unsubscribe(subId)
|
||||
|
||||
try {
|
||||
const response = decryptJSON<Response<T>>(this.identity!, event.pubkey, event.content)
|
||||
const response = decryptJSON<RPCResponse<T>>(
|
||||
this.identity!,
|
||||
this.config.accountPubkey,
|
||||
event.content
|
||||
)
|
||||
|
||||
if (response.error) {
|
||||
reject(new Error(response.error.message))
|
||||
if (isRPCError(response)) {
|
||||
reject(new Error(response.reason))
|
||||
} else {
|
||||
resolve(response.result as T)
|
||||
// Extract result from response (excluding status)
|
||||
const { status, ...result } = response
|
||||
resolve(result as T)
|
||||
}
|
||||
} catch (e) {
|
||||
reject(e)
|
||||
|
|
@ -390,6 +373,14 @@ export class LightningPubClient {
|
|||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a unique request ID
|
||||
*/
|
||||
private generateRequestId(): string {
|
||||
this.requestCounter++
|
||||
return `${Date.now()}-${this.requestCounter}`
|
||||
}
|
||||
|
||||
/**
|
||||
* Disconnect and clean up
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -4,19 +4,21 @@
|
|||
* Lightning.Pub client for Nostr-native Lightning operations.
|
||||
*
|
||||
* Lightning.Pub is an account system that wraps LND and provides
|
||||
* CLINK protocol support. This package handles:
|
||||
* - Invoice generation
|
||||
* - Payment processing
|
||||
* CLINK protocol support. This package handles the RPC layer:
|
||||
* - Invoice generation via Kind 21000
|
||||
* - Payment processing via Kind 21000
|
||||
* - Balance queries
|
||||
* - Exchange rate fetching
|
||||
*
|
||||
* For CLINK payment operations (offers, debits, management),
|
||||
* use @lamassu/clink instead.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { LightningPubClient } from '@lamassu/lightning'
|
||||
*
|
||||
* const client = new LightningPubClient({
|
||||
* serviceUrl: 'https://lightning.pub',
|
||||
* accountPubkey: 'npub1...',
|
||||
* accountPubkey: 'hex-pubkey...',
|
||||
* relays: ['wss://relay.example.com'],
|
||||
* })
|
||||
*
|
||||
|
|
@ -25,7 +27,7 @@
|
|||
*
|
||||
* // Create an invoice
|
||||
* const invoice = await client.createInvoice({
|
||||
* amountMsat: 100000, // 100 sats
|
||||
* amountSats: 100,
|
||||
* description: 'ATM withdrawal',
|
||||
* })
|
||||
*
|
||||
|
|
@ -46,15 +48,32 @@
|
|||
export { LightningPubClient } from './client.js'
|
||||
|
||||
// Types
|
||||
export type {
|
||||
LightningPubConfig,
|
||||
AccountInfo,
|
||||
Invoice,
|
||||
InvoiceStatus,
|
||||
PaymentResult,
|
||||
ExchangeRate,
|
||||
ChannelInfo,
|
||||
NodeInfo,
|
||||
InvoiceCallback,
|
||||
PaymentCallback,
|
||||
export {
|
||||
// Event kinds
|
||||
LightningPubEventKind,
|
||||
// RPC types
|
||||
type RPCRequest,
|
||||
type RPCResponse,
|
||||
type RPCSuccessResponse,
|
||||
type RPCErrorResponse,
|
||||
isRPCError,
|
||||
// Account types
|
||||
type AccountInfo,
|
||||
type BalanceResponse,
|
||||
// Invoice types
|
||||
type Invoice,
|
||||
type InvoiceStatus,
|
||||
type CreateInvoiceParams,
|
||||
type CreateInvoiceResponse,
|
||||
type LookupInvoiceResponse,
|
||||
// Payment types
|
||||
type PaymentResult,
|
||||
type PayInvoiceResponse,
|
||||
// Exchange types
|
||||
type ExchangeRate,
|
||||
// Config
|
||||
type LightningPubConfig,
|
||||
// Callbacks
|
||||
type InvoiceCallback,
|
||||
type PaymentCallback,
|
||||
} from './types.js'
|
||||
|
|
|
|||
|
|
@ -3,8 +3,67 @@
|
|||
*
|
||||
* Lightning.Pub is a Nostr-native account system that wraps LND
|
||||
* and provides CLINK payment support.
|
||||
*
|
||||
* Communication uses Kind 21000 for generic RPC requests.
|
||||
* Payment operations use CLINK protocol (kinds 21001-21003).
|
||||
*/
|
||||
|
||||
/** Lightning.Pub event kinds */
|
||||
export enum LightningPubEventKind {
|
||||
/** Generic RPC request/response */
|
||||
RPC = 21000,
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// RPC Request/Response
|
||||
// ============================================================================
|
||||
|
||||
/** Generic RPC request format (Kind 21000) */
|
||||
export interface RPCRequest {
|
||||
/** Method name (e.g., "NewInvoice", "GetBalance") */
|
||||
rpcName: string
|
||||
/** URL params */
|
||||
params?: Record<string, string>
|
||||
/** Query params */
|
||||
query?: Record<string, string>
|
||||
/** Request body */
|
||||
body?: unknown
|
||||
/** Pubkey of requester (for validation) */
|
||||
authIdentifier: string
|
||||
/** Unique request identifier */
|
||||
requestId: string
|
||||
/** Application identifier */
|
||||
appId?: string
|
||||
}
|
||||
|
||||
/** RPC success response */
|
||||
export interface RPCSuccessResponse<T = unknown> {
|
||||
/** Success status */
|
||||
status: 'OK'
|
||||
/** Result data - spread into the response object */
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
/** RPC error response */
|
||||
export interface RPCErrorResponse {
|
||||
/** Error status */
|
||||
status: 'ERROR'
|
||||
/** Error reason */
|
||||
reason: string
|
||||
}
|
||||
|
||||
/** RPC response - either success or error */
|
||||
export type RPCResponse<T = unknown> = (RPCSuccessResponse<T> & T) | RPCErrorResponse
|
||||
|
||||
/** Type guard for RPC error */
|
||||
export function isRPCError(response: RPCResponse): response is RPCErrorResponse {
|
||||
return response.status === 'ERROR'
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Account & Balance
|
||||
// ============================================================================
|
||||
|
||||
/** Lightning.Pub account information */
|
||||
export interface AccountInfo {
|
||||
/** Account public key */
|
||||
|
|
@ -17,6 +76,16 @@ export interface AccountInfo {
|
|||
maxReceiveSats: number
|
||||
}
|
||||
|
||||
/** Balance response */
|
||||
export interface BalanceResponse {
|
||||
/** Balance in satoshis */
|
||||
balance: number
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Invoices
|
||||
// ============================================================================
|
||||
|
||||
/** Invoice status */
|
||||
export type InvoiceStatus = 'pending' | 'paid' | 'expired' | 'cancelled'
|
||||
|
||||
|
|
@ -26,8 +95,8 @@ export interface Invoice {
|
|||
paymentRequest: string
|
||||
/** Payment hash */
|
||||
paymentHash: string
|
||||
/** Amount in millisatoshis */
|
||||
amountMsat: number
|
||||
/** Amount in satoshis */
|
||||
amountSats: number
|
||||
/** Invoice description */
|
||||
description?: string
|
||||
/** Creation timestamp */
|
||||
|
|
@ -40,18 +109,74 @@ export interface Invoice {
|
|||
preimage?: string
|
||||
}
|
||||
|
||||
/** Create invoice request params */
|
||||
export interface CreateInvoiceParams {
|
||||
/** Amount in satoshis */
|
||||
amountSats: number
|
||||
/** Invoice description/memo */
|
||||
description?: string
|
||||
/** Expiry in seconds (default: 3600) */
|
||||
expirySecs?: number
|
||||
/** Include private route hints */
|
||||
privateHints?: boolean
|
||||
}
|
||||
|
||||
/** Create invoice response (from RPC) */
|
||||
export interface CreateInvoiceResponse {
|
||||
/** BOLT-11 payment request */
|
||||
payment_request: string
|
||||
/** Payment hash (hex) */
|
||||
payment_hash: string
|
||||
/** Expiry timestamp */
|
||||
expires_at: number
|
||||
}
|
||||
|
||||
/** Lookup invoice response (from RPC) */
|
||||
export interface LookupInvoiceResponse {
|
||||
/** BOLT-11 payment request */
|
||||
payment_request: string
|
||||
/** Amount in satoshis */
|
||||
amount: number
|
||||
/** Invoice description */
|
||||
description: string
|
||||
/** Creation timestamp */
|
||||
created_at: number
|
||||
/** Expiry timestamp */
|
||||
expires_at: number
|
||||
/** Whether invoice has been paid */
|
||||
settled: boolean
|
||||
/** Payment preimage (if paid) */
|
||||
preimage?: string
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Payments
|
||||
// ============================================================================
|
||||
|
||||
/** Payment result */
|
||||
export interface PaymentResult {
|
||||
/** Whether payment succeeded */
|
||||
success: boolean
|
||||
/** Payment preimage (proof of payment) */
|
||||
preimage?: string
|
||||
/** Fee paid in millisatoshis */
|
||||
feeMsat?: number
|
||||
/** Fee paid in satoshis */
|
||||
feeSats?: number
|
||||
/** Error message if failed */
|
||||
error?: string
|
||||
}
|
||||
|
||||
/** Pay invoice response (from RPC) */
|
||||
export interface PayInvoiceResponse {
|
||||
/** Payment preimage */
|
||||
preimage: string
|
||||
/** Fee paid in satoshis */
|
||||
fee_paid?: number
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Exchange Rates
|
||||
// ============================================================================
|
||||
|
||||
/** Exchange rate information */
|
||||
export interface ExchangeRate {
|
||||
/** Fiat currency code */
|
||||
|
|
@ -64,47 +189,25 @@ export interface ExchangeRate {
|
|||
source: string
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Client Configuration
|
||||
// ============================================================================
|
||||
|
||||
/** Lightning.Pub client configuration */
|
||||
export interface LightningPubConfig {
|
||||
/** Lightning.Pub service URL */
|
||||
serviceUrl: string
|
||||
/** Account pubkey on Lightning.Pub */
|
||||
/** Lightning.Pub account pubkey */
|
||||
accountPubkey: string
|
||||
/** Nostr relays for communication */
|
||||
relays: string[]
|
||||
/** Request timeout in ms */
|
||||
/** Request timeout in ms (default: 30000) */
|
||||
timeout?: number
|
||||
/** Application identifier */
|
||||
appId?: string
|
||||
}
|
||||
|
||||
/** Channel information */
|
||||
export interface ChannelInfo {
|
||||
/** Channel ID */
|
||||
channelId: string
|
||||
/** Remote pubkey */
|
||||
remotePubkey: string
|
||||
/** Local balance in sats */
|
||||
localBalanceSats: number
|
||||
/** Remote balance in sats */
|
||||
remoteBalanceSats: number
|
||||
/** Channel capacity in sats */
|
||||
capacitySats: number
|
||||
/** Whether channel is active */
|
||||
active: boolean
|
||||
}
|
||||
|
||||
/** Node information */
|
||||
export interface NodeInfo {
|
||||
/** Node public key */
|
||||
pubkey: string
|
||||
/** Node alias */
|
||||
alias: string
|
||||
/** Number of channels */
|
||||
numChannels: number
|
||||
/** Total capacity in sats */
|
||||
totalCapacitySats: number
|
||||
/** Synced to chain */
|
||||
syncedToChain: boolean
|
||||
}
|
||||
// ============================================================================
|
||||
// Callbacks
|
||||
// ============================================================================
|
||||
|
||||
/** Invoice callback for watching payments */
|
||||
export type InvoiceCallback = (invoice: Invoice) => void
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue