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
|
* - Handling offer requests and generating invoices
|
||||||
* - Processing debit requests
|
* - Processing debit requests
|
||||||
* - Receiving management commands
|
* - Receiving management commands
|
||||||
|
*
|
||||||
|
* Uses NIP-44v2 encryption for all messages.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { Event, UnsignedEvent } from 'nostr-tools'
|
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 { encryptContent, decryptJSON } from '@lamassu/nostr-client'
|
||||||
import {
|
import {
|
||||||
CLINKEventKind,
|
CLINKEventKind,
|
||||||
|
CLINKErrorCode,
|
||||||
type CLINKOffer,
|
type CLINKOffer,
|
||||||
type OfferRequest,
|
type OfferRequest,
|
||||||
type OfferResponse,
|
type OfferResponse,
|
||||||
|
type OfferSuccessResponse,
|
||||||
|
type OfferErrorResponse,
|
||||||
type DebitRequest,
|
type DebitRequest,
|
||||||
type DebitResponse,
|
type DebitResponse,
|
||||||
type ManagementDelegation,
|
type DebitSuccessResponse,
|
||||||
|
type DebitFailureResponse,
|
||||||
|
type ManagementRequest,
|
||||||
|
type ManagementResponse,
|
||||||
|
type ServiceBeacon,
|
||||||
type GenerateInvoice,
|
type GenerateInvoice,
|
||||||
type PayInvoice,
|
type PayInvoice,
|
||||||
|
isOfferError,
|
||||||
|
isDebitFailure,
|
||||||
} from './types.js'
|
} from './types.js'
|
||||||
import { encodeNoffer } from './noffer.js'
|
import { encodeNoffer, decodeNoffer } from './noffer.js'
|
||||||
|
|
||||||
/** CLINK client options */
|
/** CLINK client options */
|
||||||
export interface CLINKClientOptions {
|
export interface CLINKClientOptions {
|
||||||
|
|
@ -41,23 +52,23 @@ export interface CLINKClientOptions {
|
||||||
payInvoice?: PayInvoice
|
payInvoice?: PayInvoice
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Offer request handler */
|
/** Offer request handler - returns invoice or error */
|
||||||
export type OfferRequestHandler = (
|
export type OfferRequestHandler = (
|
||||||
request: OfferRequest,
|
request: OfferRequest,
|
||||||
senderPubkey: string
|
senderPubkey: string
|
||||||
) => Promise<OfferResponse | null>
|
) => Promise<OfferResponse | null>
|
||||||
|
|
||||||
/** Debit request handler */
|
/** Debit request handler - returns success/failure */
|
||||||
export type DebitRequestHandler = (
|
export type DebitRequestHandler = (
|
||||||
request: DebitRequest,
|
request: DebitRequest,
|
||||||
senderPubkey: string
|
senderPubkey: string
|
||||||
) => Promise<DebitResponse>
|
) => Promise<DebitResponse>
|
||||||
|
|
||||||
/** Management command handler */
|
/** Management request handler */
|
||||||
export type ManagementHandler = (
|
export type ManagementHandler = (
|
||||||
delegation: ManagementDelegation,
|
request: ManagementRequest,
|
||||||
senderPubkey: string
|
senderPubkey: string
|
||||||
) => Promise<void>
|
) => Promise<ManagementResponse | null>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* CLINK protocol client for ATM payments
|
* CLINK protocol client for ATM payments
|
||||||
|
|
@ -90,40 +101,47 @@ export class CLINKClient {
|
||||||
*/
|
*/
|
||||||
createOffer(options: {
|
createOffer(options: {
|
||||||
priceType: CLINKOffer['priceType']
|
priceType: CLINKOffer['priceType']
|
||||||
amountMsat?: number
|
amountSats?: number
|
||||||
description?: string
|
description?: string
|
||||||
minAmountMsat?: number
|
minAmountSats?: number
|
||||||
maxAmountMsat?: number
|
maxAmountSats?: number
|
||||||
}): string {
|
}): string {
|
||||||
const offer: CLINKOffer = {
|
const offer: CLINKOffer = {
|
||||||
pubkey: this.identity.publicKey,
|
pubkey: this.identity.publicKey,
|
||||||
relays: this.relays,
|
relays: this.relays,
|
||||||
priceType: options.priceType,
|
priceType: options.priceType,
|
||||||
amountMsat: options.amountMsat,
|
amountSats: options.amountSats,
|
||||||
description: options.description,
|
description: options.description,
|
||||||
minAmountMsat: options.minAmountMsat,
|
minAmountSats: options.minAmountSats,
|
||||||
maxAmountMsat: options.maxAmountMsat,
|
maxAmountSats: options.maxAmountSats,
|
||||||
}
|
}
|
||||||
|
|
||||||
return encodeNoffer(offer)
|
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 {
|
onOfferRequest(handler: OfferRequestHandler): void {
|
||||||
this.offerHandler = handler
|
this.offerHandler = handler
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set handler for incoming debit requests
|
* Set handler for incoming debit requests (Kind 21002)
|
||||||
*/
|
*/
|
||||||
onDebitRequest(handler: DebitRequestHandler): void {
|
onDebitRequest(handler: DebitRequestHandler): void {
|
||||||
this.debitHandler = handler
|
this.debitHandler = handler
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set handler for management commands
|
* Set handler for management commands (Kind 21003)
|
||||||
*/
|
*/
|
||||||
onManagement(handler: ManagementHandler): void {
|
onManagement(handler: ManagementHandler): void {
|
||||||
this.managementHandler = handler
|
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> {
|
async requestOffer(
|
||||||
const content = encryptContent(this.identity, targetPubkey, request)
|
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({
|
const event = this.createSignedEvent({
|
||||||
kind: CLINKEventKind.Offer,
|
kind: CLINKEventKind.Offer,
|
||||||
content,
|
content,
|
||||||
tags: [['p', targetPubkey]],
|
tags: [['p', offer.pubkey]],
|
||||||
created_at: Math.floor(Date.now() / 1000),
|
created_at: Math.floor(Date.now() / 1000),
|
||||||
})
|
})
|
||||||
|
|
||||||
await this.nostrClient.publish(event)
|
await this.nostrClient.publish(event)
|
||||||
|
|
||||||
// Wait for response
|
// 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 content = encryptContent(this.identity, targetPubkey, request)
|
||||||
|
|
||||||
const event = this.createSignedEvent({
|
const event = this.createSignedEvent({
|
||||||
|
|
@ -192,7 +241,50 @@ export class CLINKClient {
|
||||||
|
|
||||||
await this.nostrClient.publish(event)
|
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> {
|
private async handleOfferEvent(event: Event): Promise<void> {
|
||||||
if (!this.offerHandler) return
|
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> {
|
private async handleDebitEvent(event: Event): Promise<void> {
|
||||||
if (!this.debitHandler) return
|
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> {
|
private async handleManageEvent(event: Event): Promise<void> {
|
||||||
// Only accept from operator
|
// Only accept from operator
|
||||||
|
|
@ -281,15 +373,31 @@ export class CLINKClient {
|
||||||
|
|
||||||
if (!this.managementHandler) return
|
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
|
* 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) => {
|
return new Promise((resolve, reject) => {
|
||||||
const timeout = setTimeout(() => {
|
const timeout = setTimeout(() => {
|
||||||
this.nostrClient.unsubscribe(subId)
|
this.nostrClient.unsubscribe(subId)
|
||||||
|
|
@ -302,6 +410,7 @@ export class CLINKClient {
|
||||||
kinds: [kind],
|
kinds: [kind],
|
||||||
authors: [fromPubkey],
|
authors: [fromPubkey],
|
||||||
'#p': [this.identity.publicKey],
|
'#p': [this.identity.publicKey],
|
||||||
|
'#e': [requestEventId],
|
||||||
since: Math.floor(Date.now() / 1000) - 5,
|
since: Math.floor(Date.now() / 1000) - 5,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|
@ -329,3 +438,42 @@ export class CLINKClient {
|
||||||
return finalizeEvent(event, this.identity.privateKey)
|
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 protocol implementation for Nostr-native Lightning payments.
|
||||||
*
|
*
|
||||||
* CLINK enables static payment codes (noffers) that work over Nostr,
|
* CLINK enables:
|
||||||
* providing a decentralized alternative to BOLT12 offers.
|
* - 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
|
* @example
|
||||||
* ```typescript
|
* ```typescript
|
||||||
* import { CLINKClient, encodeNoffer, decodeNoffer } from '@lamassu/clink'
|
* import { CLINKClient, encodeNoffer, createOfferSuccess } from '@lamassu/clink'
|
||||||
*
|
*
|
||||||
* // Create a client
|
* // Create a client
|
||||||
* const clink = new CLINKClient({
|
* const clink = new CLINKClient({
|
||||||
|
|
@ -26,42 +32,66 @@
|
||||||
*
|
*
|
||||||
* // Handle incoming offer requests
|
* // Handle incoming offer requests
|
||||||
* clink.onOfferRequest(async (request, senderPubkey) => {
|
* clink.onOfferRequest(async (request, senderPubkey) => {
|
||||||
* const invoice = await generateInvoice(request.amountMsat)
|
* const bolt11 = await generateInvoice(request.amount_sats)
|
||||||
* return {
|
* return createOfferSuccess(bolt11)
|
||||||
* invoice,
|
|
||||||
* amountMsat: request.amountMsat,
|
|
||||||
* expiresAt: Date.now() / 1000 + 600,
|
|
||||||
* }
|
|
||||||
* })
|
* })
|
||||||
*
|
*
|
||||||
* clink.startListening()
|
* 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
|
// Types
|
||||||
export {
|
export {
|
||||||
|
// Event kinds
|
||||||
CLINKEventKind,
|
CLINKEventKind,
|
||||||
NofferTLV,
|
CLINKErrorCode,
|
||||||
type PriceType,
|
// Offer types (Kind 21001)
|
||||||
type CLINKOffer,
|
|
||||||
type OfferRequest,
|
type OfferRequest,
|
||||||
type OfferResponse,
|
type OfferResponse,
|
||||||
|
type OfferSuccessResponse,
|
||||||
|
type OfferErrorResponse,
|
||||||
|
// Debit types (Kind 21002)
|
||||||
type DebitRequest,
|
type DebitRequest,
|
||||||
type DebitResponse,
|
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 ManagementAction,
|
||||||
type ManagementDelegation,
|
type OfferConfig,
|
||||||
|
// Beacon types (Kind 30078)
|
||||||
|
type ServiceBeacon,
|
||||||
|
// Noffer types
|
||||||
|
type CLINKOffer,
|
||||||
|
type PriceType,
|
||||||
|
NofferTLV,
|
||||||
|
// Function types
|
||||||
type GenerateInvoice,
|
type GenerateInvoice,
|
||||||
type PayInvoice,
|
type PayInvoice,
|
||||||
|
// Type guards
|
||||||
|
isOfferError,
|
||||||
|
isDebitFailure,
|
||||||
|
isManagementFailure,
|
||||||
} from './types.js'
|
} 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)
|
const priceTypeByte = encodePriceType(offer.priceType)
|
||||||
writeTLV(tlvData, NofferTLV.PriceType, [priceTypeByte])
|
writeTLV(tlvData, NofferTLV.PriceType, [priceTypeByte])
|
||||||
|
|
||||||
// Amount (TLV 3) - if present
|
// Amount (TLV 3) - if present (in satoshis)
|
||||||
if (offer.amountMsat !== undefined) {
|
if (offer.amountSats !== undefined) {
|
||||||
const amountBytes = encodeUint64BE(offer.amountMsat)
|
const amountBytes = encodeUint64BE(offer.amountSats)
|
||||||
writeTLV(tlvData, NofferTLV.Amount, amountBytes)
|
writeTLV(tlvData, NofferTLV.Amount, amountBytes)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -71,7 +71,7 @@ export function decodeNoffer(noffer: string): CLINKOffer {
|
||||||
let pubkey: string | undefined
|
let pubkey: string | undefined
|
||||||
const relays: string[] = []
|
const relays: string[] = []
|
||||||
let priceType: PriceType = 'spontaneous'
|
let priceType: PriceType = 'spontaneous'
|
||||||
let amountMsat: number | undefined
|
let amountSats: number | undefined
|
||||||
let description: string | undefined
|
let description: string | undefined
|
||||||
|
|
||||||
let offset = 0
|
let offset = 0
|
||||||
|
|
@ -102,7 +102,7 @@ export function decodeNoffer(noffer: string): CLINKOffer {
|
||||||
priceType = decodePriceType(value[0] ?? 2)
|
priceType = decodePriceType(value[0] ?? 2)
|
||||||
break
|
break
|
||||||
case NofferTLV.Amount:
|
case NofferTLV.Amount:
|
||||||
amountMsat = decodeUint64BE(value)
|
amountSats = decodeUint64BE(value)
|
||||||
break
|
break
|
||||||
case NofferTLV.Description:
|
case NofferTLV.Description:
|
||||||
description = new TextDecoder().decode(value)
|
description = new TextDecoder().decode(value)
|
||||||
|
|
@ -123,7 +123,7 @@ export function decodeNoffer(noffer: string): CLINKOffer {
|
||||||
pubkey,
|
pubkey,
|
||||||
relays,
|
relays,
|
||||||
priceType,
|
priceType,
|
||||||
amountMsat,
|
amountSats,
|
||||||
description,
|
description,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,22 +5,236 @@
|
||||||
* static payment codes (noffers) over Nostr relays.
|
* static payment codes (noffers) over Nostr relays.
|
||||||
*
|
*
|
||||||
* Event kinds:
|
* Event kinds:
|
||||||
|
* - 21000: Generic RPC request/response
|
||||||
* - 21001: Offer request/response
|
* - 21001: Offer request/response
|
||||||
* - 21002: Debit 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 */
|
/** CLINK event kinds */
|
||||||
export enum CLINKEventKind {
|
export enum CLINKEventKind {
|
||||||
|
/** Generic RPC request/response */
|
||||||
|
RPC = 21000,
|
||||||
/** Offer request/response */
|
/** Offer request/response */
|
||||||
Offer = 21001,
|
Offer = 21001,
|
||||||
/** Debit request/response (authorized payments) */
|
/** Debit request/response (authorized payments) */
|
||||||
Debit = 21002,
|
Debit = 21002,
|
||||||
/** Management delegation */
|
/** Management request/response */
|
||||||
Manage = 21003,
|
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'
|
export type PriceType = 'fixed' | 'variable' | 'spontaneous'
|
||||||
|
|
||||||
/** CLINK Offer (noffer) configuration */
|
/** CLINK Offer (noffer) configuration */
|
||||||
|
|
@ -31,81 +245,14 @@ export interface CLINKOffer {
|
||||||
relays: string[]
|
relays: string[]
|
||||||
/** Pricing model */
|
/** Pricing model */
|
||||||
priceType: PriceType
|
priceType: PriceType
|
||||||
/** Amount in millisatoshis (for fixed price) */
|
/** Amount in satoshis (for fixed price) */
|
||||||
amountMsat?: number
|
amountSats?: number
|
||||||
/** Human-readable description */
|
/** Human-readable description */
|
||||||
description?: string
|
description?: string
|
||||||
/** Minimum amount in msats (for variable/spontaneous) */
|
/** Minimum amount in sats (for variable/spontaneous) */
|
||||||
minAmountMsat?: number
|
minAmountSats?: number
|
||||||
/** Maximum amount in msats (for variable/spontaneous) */
|
/** Maximum amount in sats (for variable/spontaneous) */
|
||||||
maxAmountMsat?: number
|
maxAmountSats?: 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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** noffer TLV types */
|
/** noffer TLV types */
|
||||||
|
|
@ -116,14 +263,18 @@ export enum NofferTLV {
|
||||||
Relay = 1,
|
Relay = 1,
|
||||||
/** Price type (1 byte: 0=fixed, 1=variable, 2=spontaneous) */
|
/** Price type (1 byte: 0=fixed, 1=variable, 2=spontaneous) */
|
||||||
PriceType = 2,
|
PriceType = 2,
|
||||||
/** Amount in msats (8 bytes, big-endian) */
|
/** Amount in sats (8 bytes, big-endian) */
|
||||||
Amount = 3,
|
Amount = 3,
|
||||||
/** Description (variable length string) */
|
/** Description (variable length string) */
|
||||||
Description = 4,
|
Description = 4,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Function types for service injection
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
/** Invoice generation function type */
|
/** Invoice generation function type */
|
||||||
export type GenerateInvoice = (amountMsat: number) => Promise<string>
|
export type GenerateInvoice = (amountSats: number, description?: string) => Promise<string>
|
||||||
|
|
||||||
/** Payment function type */
|
/** Payment function type */
|
||||||
export type PayInvoice = (invoice: string) => Promise<{ preimage: string } | { error: string }>
|
export type PayInvoice = (invoice: string) => Promise<{ preimage: string } | { error: string }>
|
||||||
|
|
|
||||||
|
|
@ -5,52 +5,41 @@
|
||||||
* account system that wraps LND.
|
* account system that wraps LND.
|
||||||
*
|
*
|
||||||
* Lightning.Pub provides:
|
* Lightning.Pub provides:
|
||||||
* - Account management (sublayers)
|
* - Account management
|
||||||
* - Invoice generation
|
* - Invoice generation via RPC (kind 21000)
|
||||||
* - Payment processing
|
* - Payment processing via RPC (kind 21000)
|
||||||
* - CLINK protocol support
|
* - 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 { finalizeEvent } from 'nostr-tools'
|
||||||
import type { MachineIdentity, NostrClient } from '@lamassu/nostr-client'
|
import type { MachineIdentity, NostrClient } from '@lamassu/nostr-client'
|
||||||
import { encryptContent, decryptJSON } from '@lamassu/nostr-client'
|
import { encryptContent, decryptJSON } from '@lamassu/nostr-client'
|
||||||
import type {
|
import {
|
||||||
LightningPubConfig,
|
LightningPubEventKind,
|
||||||
AccountInfo,
|
type LightningPubConfig,
|
||||||
Invoice,
|
type RPCRequest,
|
||||||
PaymentResult,
|
type RPCResponse,
|
||||||
ExchangeRate,
|
type AccountInfo,
|
||||||
InvoiceCallback,
|
type BalanceResponse,
|
||||||
|
type Invoice,
|
||||||
|
type CreateInvoiceParams,
|
||||||
|
type CreateInvoiceResponse,
|
||||||
|
type LookupInvoiceResponse,
|
||||||
|
type PaymentResult,
|
||||||
|
type PayInvoiceResponse,
|
||||||
|
type ExchangeRate,
|
||||||
|
type InvoiceCallback,
|
||||||
|
isRPCError,
|
||||||
} from './types.js'
|
} 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
|
* Lightning.Pub client for ATM operations
|
||||||
|
*
|
||||||
|
* Uses Kind 21000 for RPC communication with Lightning.Pub service.
|
||||||
*/
|
*/
|
||||||
export class LightningPubClient {
|
export class LightningPubClient {
|
||||||
private config: Required<LightningPubConfig>
|
private config: Required<LightningPubConfig>
|
||||||
|
|
@ -58,10 +47,12 @@ export class LightningPubClient {
|
||||||
private identity: MachineIdentity | null = null
|
private identity: MachineIdentity | null = null
|
||||||
private invoiceCallbacks: Map<string, InvoiceCallback> = new Map()
|
private invoiceCallbacks: Map<string, InvoiceCallback> = new Map()
|
||||||
private subscriptionId?: string
|
private subscriptionId?: string
|
||||||
|
private requestCounter = 0
|
||||||
|
|
||||||
constructor(config: LightningPubConfig) {
|
constructor(config: LightningPubConfig) {
|
||||||
this.config = {
|
this.config = {
|
||||||
timeout: 30000,
|
timeout: 30000,
|
||||||
|
appId: 'lamassu-atm',
|
||||||
...config,
|
...config,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -79,51 +70,39 @@ export class LightningPubClient {
|
||||||
* Get account information
|
* Get account information
|
||||||
*/
|
*/
|
||||||
async getAccountInfo(): Promise<AccountInfo> {
|
async getAccountInfo(): Promise<AccountInfo> {
|
||||||
const response = await this.sendRequest<AccountInfo>({
|
const response = await this.sendRPC<AccountInfo>('GetInfo', {})
|
||||||
method: 'get_info',
|
|
||||||
params: {},
|
|
||||||
})
|
|
||||||
|
|
||||||
return response
|
return {
|
||||||
|
pubkey: response.pubkey ?? this.config.accountPubkey,
|
||||||
|
balanceSats: response.balanceSats ?? 0,
|
||||||
|
maxSendSats: response.maxSendSats ?? 0,
|
||||||
|
maxReceiveSats: response.maxReceiveSats ?? 0,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get current balance
|
* Get current balance
|
||||||
*/
|
*/
|
||||||
async getBalance(): Promise<{ balanceSats: number }> {
|
async getBalance(): Promise<{ balanceSats: number }> {
|
||||||
const response = await this.sendRequest<{ balance: number }>({
|
const response = await this.sendRPC<BalanceResponse>('GetBalance', {})
|
||||||
method: 'get_balance',
|
return { balanceSats: response.balance ?? 0 }
|
||||||
params: {},
|
|
||||||
})
|
|
||||||
|
|
||||||
return { balanceSats: response.balance }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a Lightning invoice
|
* Create a Lightning invoice
|
||||||
*/
|
*/
|
||||||
async createInvoice(params: {
|
async createInvoice(params: CreateInvoiceParams): Promise<Invoice> {
|
||||||
amountMsat: number
|
const response = await this.sendRPC<CreateInvoiceResponse>('NewInvoice', {
|
||||||
description?: string
|
amount: params.amountSats,
|
||||||
expirySecs?: number
|
memo: params.description || 'ATM Payment',
|
||||||
}): Promise<Invoice> {
|
expiry: params.expirySecs || 3600,
|
||||||
const response = await this.sendRequest<{
|
private: params.privateHints ?? false,
|
||||||
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,
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
paymentRequest: response.payment_request,
|
paymentRequest: response.payment_request,
|
||||||
paymentHash: response.payment_hash,
|
paymentHash: response.payment_hash,
|
||||||
amountMsat: params.amountMsat,
|
amountSats: params.amountSats,
|
||||||
description: params.description,
|
description: params.description,
|
||||||
createdAt: Math.floor(Date.now() / 1000),
|
createdAt: Math.floor(Date.now() / 1000),
|
||||||
expiresAt: response.expires_at,
|
expiresAt: response.expires_at,
|
||||||
|
|
@ -136,20 +115,14 @@ export class LightningPubClient {
|
||||||
*/
|
*/
|
||||||
async payInvoice(paymentRequest: string): Promise<PaymentResult> {
|
async payInvoice(paymentRequest: string): Promise<PaymentResult> {
|
||||||
try {
|
try {
|
||||||
const response = await this.sendRequest<{
|
const response = await this.sendRPC<PayInvoiceResponse>('PayInvoice', {
|
||||||
preimage: string
|
invoice: paymentRequest,
|
||||||
fee_paid?: number
|
|
||||||
}>({
|
|
||||||
method: 'pay_invoice',
|
|
||||||
params: {
|
|
||||||
invoice: paymentRequest,
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
preimage: response.preimage,
|
preimage: response.preimage,
|
||||||
feeMsat: response.fee_paid,
|
feeSats: response.fee_paid,
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return {
|
return {
|
||||||
|
|
@ -164,25 +137,14 @@ export class LightningPubClient {
|
||||||
*/
|
*/
|
||||||
async lookupInvoice(paymentHash: string): Promise<Invoice | null> {
|
async lookupInvoice(paymentHash: string): Promise<Invoice | null> {
|
||||||
try {
|
try {
|
||||||
const response = await this.sendRequest<{
|
const response = await this.sendRPC<LookupInvoiceResponse>('LookupInvoice', {
|
||||||
payment_request: string
|
payment_hash: paymentHash,
|
||||||
amount: number
|
|
||||||
description: string
|
|
||||||
created_at: number
|
|
||||||
expires_at: number
|
|
||||||
settled: boolean
|
|
||||||
preimage?: string
|
|
||||||
}>({
|
|
||||||
method: 'lookup_invoice',
|
|
||||||
params: {
|
|
||||||
payment_hash: paymentHash,
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
paymentRequest: response.payment_request,
|
paymentRequest: response.payment_request,
|
||||||
paymentHash,
|
paymentHash,
|
||||||
amountMsat: response.amount,
|
amountSats: response.amount,
|
||||||
description: response.description,
|
description: response.description,
|
||||||
createdAt: response.created_at,
|
createdAt: response.created_at,
|
||||||
expiresAt: response.expires_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): {
|
decodeInvoice(paymentRequest: string): {
|
||||||
amountMsat: number | null
|
amountSats: number | null
|
||||||
paymentHash: string
|
paymentHash: string
|
||||||
description: string
|
description: string
|
||||||
expiresAt: number
|
expiresAt: number
|
||||||
} {
|
} {
|
||||||
// Basic invoice parsing (for production, use bolt11 library)
|
// Basic invoice parsing
|
||||||
// This is a simplified version
|
|
||||||
const amountMatch = paymentRequest.match(/lnbc(\d+)([munp]?)/)
|
const amountMatch = paymentRequest.match(/lnbc(\d+)([munp]?)/)
|
||||||
let amountMsat: number | null = null
|
let amountSats: number | null = null
|
||||||
|
|
||||||
if (amountMatch) {
|
if (amountMatch) {
|
||||||
const [, amount, multiplier] = amountMatch
|
const [, amount, multiplier] = amountMatch
|
||||||
const baseAmount = parseInt(amount ?? '0', 10)
|
const baseAmount = parseInt(amount ?? '0', 10)
|
||||||
switch (multiplier) {
|
switch (multiplier) {
|
||||||
case 'm':
|
case 'm':
|
||||||
amountMsat = baseAmount * 100_000_000
|
amountSats = baseAmount * 100_000 // milli-BTC to sats
|
||||||
break
|
break
|
||||||
case 'u':
|
case 'u':
|
||||||
amountMsat = baseAmount * 100_000
|
amountSats = baseAmount * 100 // micro-BTC to sats
|
||||||
break
|
break
|
||||||
case 'n':
|
case 'n':
|
||||||
amountMsat = baseAmount * 100
|
amountSats = Math.floor(baseAmount / 10) // nano-BTC to sats
|
||||||
break
|
break
|
||||||
case 'p':
|
case 'p':
|
||||||
amountMsat = baseAmount / 10
|
amountSats = Math.floor(baseAmount / 10_000) // pico-BTC to sats
|
||||||
break
|
break
|
||||||
default:
|
default:
|
||||||
amountMsat = baseAmount * 100_000_000_000
|
amountSats = baseAmount * 100_000_000 // BTC to sats
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
amountMsat,
|
amountSats,
|
||||||
paymentHash: '', // Would need proper parsing
|
paymentHash: '', // Would need proper parsing
|
||||||
description: '',
|
description: '',
|
||||||
expiresAt: Math.floor(Date.now() / 1000) + 3600,
|
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> {
|
async getExchangeRate(currency: string): Promise<ExchangeRate> {
|
||||||
// For now, return a mock rate
|
// Mock rates - in production, fetch from service
|
||||||
// In production, integrate with price feeds
|
|
||||||
const mockRates: Record<string, number> = {
|
const mockRates: Record<string, number> = {
|
||||||
USD: 2500, // sats per dollar (example)
|
USD: 2500, // sats per dollar (example)
|
||||||
EUR: 2700,
|
EUR: 2700,
|
||||||
|
|
@ -291,7 +255,7 @@ export class LightningPubClient {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Start listening for responses
|
* Start listening for RPC responses
|
||||||
*/
|
*/
|
||||||
private startListening(): void {
|
private startListening(): void {
|
||||||
if (!this.nostrClient || !this.identity) return
|
if (!this.nostrClient || !this.identity) return
|
||||||
|
|
@ -300,8 +264,9 @@ export class LightningPubClient {
|
||||||
this.subscriptionId = this.nostrClient.subscribe(
|
this.subscriptionId = this.nostrClient.subscribe(
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
kinds: [LIGHTNING_PUB_KINDS.RESPONSE],
|
kinds: [LightningPubEventKind.RPC],
|
||||||
'#p': [this.identity.publicKey],
|
'#p': [this.identity.publicKey],
|
||||||
|
authors: [this.config.accountPubkey],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
{
|
{
|
||||||
|
|
@ -313,25 +278,35 @@ export class LightningPubClient {
|
||||||
/**
|
/**
|
||||||
* Handle incoming response events
|
* Handle incoming response events
|
||||||
*/
|
*/
|
||||||
private handleResponse(event: Event): void {
|
private handleResponse(_event: Event): void {
|
||||||
// Handle async responses if needed
|
// Responses are handled by waitForResponse
|
||||||
// For invoice watching, we use polling instead
|
// 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) {
|
if (!this.nostrClient || !this.identity) {
|
||||||
throw new Error('Client not initialized')
|
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)
|
const content = encryptContent(this.identity, this.config.accountPubkey, request)
|
||||||
|
|
||||||
// finalizeEvent derives pubkey from the secret key
|
// finalizeEvent derives pubkey from the secret key
|
||||||
const event = finalizeEvent(
|
const event = finalizeEvent(
|
||||||
{
|
{
|
||||||
kind: LIGHTNING_PUB_KINDS.REQUEST,
|
kind: LightningPubEventKind.RPC,
|
||||||
content,
|
content,
|
||||||
tags: [['p', this.config.accountPubkey]],
|
tags: [['p', this.config.accountPubkey]],
|
||||||
created_at: Math.floor(Date.now() / 1000),
|
created_at: Math.floor(Date.now() / 1000),
|
||||||
|
|
@ -342,13 +317,13 @@ export class LightningPubClient {
|
||||||
await this.nostrClient.publish(event)
|
await this.nostrClient.publish(event)
|
||||||
|
|
||||||
// Wait for response
|
// Wait for response
|
||||||
return this.waitForResponse<T>(event.id)
|
return this.waitForResponse<T>(event.id, requestId)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Wait for a response to a specific request
|
* 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) => {
|
return new Promise((resolve, reject) => {
|
||||||
if (!this.nostrClient || !this.identity) {
|
if (!this.nostrClient || !this.identity) {
|
||||||
reject(new Error('Client not initialized'))
|
reject(new Error('Client not initialized'))
|
||||||
|
|
@ -363,9 +338,11 @@ export class LightningPubClient {
|
||||||
const subId = this.nostrClient.subscribe(
|
const subId = this.nostrClient.subscribe(
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
kinds: [LIGHTNING_PUB_KINDS.RESPONSE],
|
kinds: [LightningPubEventKind.RPC],
|
||||||
'#e': [requestId],
|
authors: [this.config.accountPubkey],
|
||||||
'#p': [this.identity!.publicKey],
|
'#p': [this.identity!.publicKey],
|
||||||
|
'#e': [eventId],
|
||||||
|
since: Math.floor(Date.now() / 1000) - 5,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
{
|
{
|
||||||
|
|
@ -374,12 +351,18 @@ export class LightningPubClient {
|
||||||
this.nostrClient!.unsubscribe(subId)
|
this.nostrClient!.unsubscribe(subId)
|
||||||
|
|
||||||
try {
|
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) {
|
if (isRPCError(response)) {
|
||||||
reject(new Error(response.error.message))
|
reject(new Error(response.reason))
|
||||||
} else {
|
} else {
|
||||||
resolve(response.result as T)
|
// Extract result from response (excluding status)
|
||||||
|
const { status, ...result } = response
|
||||||
|
resolve(result as T)
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
reject(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
|
* Disconnect and clean up
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -4,19 +4,21 @@
|
||||||
* Lightning.Pub client for Nostr-native Lightning operations.
|
* Lightning.Pub client for Nostr-native Lightning operations.
|
||||||
*
|
*
|
||||||
* Lightning.Pub is an account system that wraps LND and provides
|
* Lightning.Pub is an account system that wraps LND and provides
|
||||||
* CLINK protocol support. This package handles:
|
* CLINK protocol support. This package handles the RPC layer:
|
||||||
* - Invoice generation
|
* - Invoice generation via Kind 21000
|
||||||
* - Payment processing
|
* - Payment processing via Kind 21000
|
||||||
* - Balance queries
|
* - Balance queries
|
||||||
* - Exchange rate fetching
|
* - Exchange rate fetching
|
||||||
*
|
*
|
||||||
|
* For CLINK payment operations (offers, debits, management),
|
||||||
|
* use @lamassu/clink instead.
|
||||||
|
*
|
||||||
* @example
|
* @example
|
||||||
* ```typescript
|
* ```typescript
|
||||||
* import { LightningPubClient } from '@lamassu/lightning'
|
* import { LightningPubClient } from '@lamassu/lightning'
|
||||||
*
|
*
|
||||||
* const client = new LightningPubClient({
|
* const client = new LightningPubClient({
|
||||||
* serviceUrl: 'https://lightning.pub',
|
* accountPubkey: 'hex-pubkey...',
|
||||||
* accountPubkey: 'npub1...',
|
|
||||||
* relays: ['wss://relay.example.com'],
|
* relays: ['wss://relay.example.com'],
|
||||||
* })
|
* })
|
||||||
*
|
*
|
||||||
|
|
@ -25,7 +27,7 @@
|
||||||
*
|
*
|
||||||
* // Create an invoice
|
* // Create an invoice
|
||||||
* const invoice = await client.createInvoice({
|
* const invoice = await client.createInvoice({
|
||||||
* amountMsat: 100000, // 100 sats
|
* amountSats: 100,
|
||||||
* description: 'ATM withdrawal',
|
* description: 'ATM withdrawal',
|
||||||
* })
|
* })
|
||||||
*
|
*
|
||||||
|
|
@ -46,15 +48,32 @@
|
||||||
export { LightningPubClient } from './client.js'
|
export { LightningPubClient } from './client.js'
|
||||||
|
|
||||||
// Types
|
// Types
|
||||||
export type {
|
export {
|
||||||
LightningPubConfig,
|
// Event kinds
|
||||||
AccountInfo,
|
LightningPubEventKind,
|
||||||
Invoice,
|
// RPC types
|
||||||
InvoiceStatus,
|
type RPCRequest,
|
||||||
PaymentResult,
|
type RPCResponse,
|
||||||
ExchangeRate,
|
type RPCSuccessResponse,
|
||||||
ChannelInfo,
|
type RPCErrorResponse,
|
||||||
NodeInfo,
|
isRPCError,
|
||||||
InvoiceCallback,
|
// Account types
|
||||||
PaymentCallback,
|
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'
|
} from './types.js'
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,67 @@
|
||||||
*
|
*
|
||||||
* Lightning.Pub is a Nostr-native account system that wraps LND
|
* Lightning.Pub is a Nostr-native account system that wraps LND
|
||||||
* and provides CLINK payment support.
|
* 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 */
|
/** Lightning.Pub account information */
|
||||||
export interface AccountInfo {
|
export interface AccountInfo {
|
||||||
/** Account public key */
|
/** Account public key */
|
||||||
|
|
@ -17,6 +76,16 @@ export interface AccountInfo {
|
||||||
maxReceiveSats: number
|
maxReceiveSats: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Balance response */
|
||||||
|
export interface BalanceResponse {
|
||||||
|
/** Balance in satoshis */
|
||||||
|
balance: number
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Invoices
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
/** Invoice status */
|
/** Invoice status */
|
||||||
export type InvoiceStatus = 'pending' | 'paid' | 'expired' | 'cancelled'
|
export type InvoiceStatus = 'pending' | 'paid' | 'expired' | 'cancelled'
|
||||||
|
|
||||||
|
|
@ -26,8 +95,8 @@ export interface Invoice {
|
||||||
paymentRequest: string
|
paymentRequest: string
|
||||||
/** Payment hash */
|
/** Payment hash */
|
||||||
paymentHash: string
|
paymentHash: string
|
||||||
/** Amount in millisatoshis */
|
/** Amount in satoshis */
|
||||||
amountMsat: number
|
amountSats: number
|
||||||
/** Invoice description */
|
/** Invoice description */
|
||||||
description?: string
|
description?: string
|
||||||
/** Creation timestamp */
|
/** Creation timestamp */
|
||||||
|
|
@ -40,18 +109,74 @@ export interface Invoice {
|
||||||
preimage?: string
|
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 */
|
/** Payment result */
|
||||||
export interface PaymentResult {
|
export interface PaymentResult {
|
||||||
/** Whether payment succeeded */
|
/** Whether payment succeeded */
|
||||||
success: boolean
|
success: boolean
|
||||||
/** Payment preimage (proof of payment) */
|
/** Payment preimage (proof of payment) */
|
||||||
preimage?: string
|
preimage?: string
|
||||||
/** Fee paid in millisatoshis */
|
/** Fee paid in satoshis */
|
||||||
feeMsat?: number
|
feeSats?: number
|
||||||
/** Error message if failed */
|
/** Error message if failed */
|
||||||
error?: string
|
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 */
|
/** Exchange rate information */
|
||||||
export interface ExchangeRate {
|
export interface ExchangeRate {
|
||||||
/** Fiat currency code */
|
/** Fiat currency code */
|
||||||
|
|
@ -64,47 +189,25 @@ export interface ExchangeRate {
|
||||||
source: string
|
source: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Client Configuration
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
/** Lightning.Pub client configuration */
|
/** Lightning.Pub client configuration */
|
||||||
export interface LightningPubConfig {
|
export interface LightningPubConfig {
|
||||||
/** Lightning.Pub service URL */
|
/** Lightning.Pub account pubkey */
|
||||||
serviceUrl: string
|
|
||||||
/** Account pubkey on Lightning.Pub */
|
|
||||||
accountPubkey: string
|
accountPubkey: string
|
||||||
/** Nostr relays for communication */
|
/** Nostr relays for communication */
|
||||||
relays: string[]
|
relays: string[]
|
||||||
/** Request timeout in ms */
|
/** Request timeout in ms (default: 30000) */
|
||||||
timeout?: number
|
timeout?: number
|
||||||
|
/** Application identifier */
|
||||||
|
appId?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Channel information */
|
// ============================================================================
|
||||||
export interface ChannelInfo {
|
// Callbacks
|
||||||
/** 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
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Invoice callback for watching payments */
|
/** Invoice callback for watching payments */
|
||||||
export type InvoiceCallback = (invoice: Invoice) => void
|
export type InvoiceCallback = (invoice: Invoice) => void
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue