feat(lightning): add withdraw link lifecycle management (delete/update/invalidate)
- Add deleteWithdrawLink and updateWithdrawLink RPC methods to LightningPubClient - Extract shared WithdrawLink type, add Delete/Update request/response types - Track linkId in LNURL sessions for server-side cleanup - Invalidate previous LNURL session on new link creation - Auto-delete expired links on the server Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
e48073397f
commit
99ae5ab3de
4 changed files with 132 additions and 37 deletions
|
|
@ -227,6 +227,8 @@ export { validateDebitSession, findActiveSessionByAmount, markSessionPaid, getSe
|
||||||
/** Active LNURL-withdraw session */
|
/** Active LNURL-withdraw session */
|
||||||
interface LnurlSession {
|
interface LnurlSession {
|
||||||
sessionId: string
|
sessionId: string
|
||||||
|
/** Link ID for management operations (delete/update) */
|
||||||
|
linkId: string
|
||||||
uniqueHash: string
|
uniqueHash: string
|
||||||
satsAmount: number
|
satsAmount: number
|
||||||
status: 'active' | 'claimed' | 'expired'
|
status: 'active' | 'claimed' | 'expired'
|
||||||
|
|
@ -240,11 +242,17 @@ const lnurlSessions = new Map<string, LnurlSession>()
|
||||||
/**
|
/**
|
||||||
* Register a new LNURL-withdraw session
|
* Register a new LNURL-withdraw session
|
||||||
*/
|
*/
|
||||||
function registerLnurlSession(sessionId: string, uniqueHash: string, satsAmount: number): void {
|
function registerLnurlSession(
|
||||||
|
sessionId: string,
|
||||||
|
linkId: string,
|
||||||
|
uniqueHash: string,
|
||||||
|
satsAmount: number
|
||||||
|
): void {
|
||||||
console.log('[LNURL Session] Registering:', uniqueHash, 'for', satsAmount, 'sats')
|
console.log('[LNURL Session] Registering:', uniqueHash, 'for', satsAmount, 'sats')
|
||||||
|
|
||||||
lnurlSessions.set(uniqueHash, {
|
lnurlSessions.set(uniqueHash, {
|
||||||
sessionId,
|
sessionId,
|
||||||
|
linkId,
|
||||||
uniqueHash,
|
uniqueHash,
|
||||||
satsAmount,
|
satsAmount,
|
||||||
status: 'active',
|
status: 'active',
|
||||||
|
|
@ -258,6 +266,10 @@ function registerLnurlSession(sessionId: string, uniqueHash: string, satsAmount:
|
||||||
console.log('[LNURL Session] Expiring:', uniqueHash)
|
console.log('[LNURL Session] Expiring:', uniqueHash)
|
||||||
session.status = 'expired'
|
session.status = 'expired'
|
||||||
if (session.cleanup) session.cleanup()
|
if (session.cleanup) session.cleanup()
|
||||||
|
// Delete the link on the server so it can't be claimed
|
||||||
|
lightningPub.deleteWithdrawLink(session.linkId).catch((err) => {
|
||||||
|
console.warn('[LNURL Session] Failed to delete expired link:', err)
|
||||||
|
})
|
||||||
// Clean up after another minute
|
// Clean up after another minute
|
||||||
setTimeout(() => lnurlSessions.delete(uniqueHash), 60000)
|
setTimeout(() => lnurlSessions.delete(uniqueHash), 60000)
|
||||||
}
|
}
|
||||||
|
|
@ -315,6 +327,26 @@ function startLnurlCompletionPolling(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invalidate an active LNURL session by cash-in sessionId.
|
||||||
|
* Stops polling and deletes the link on the server.
|
||||||
|
*/
|
||||||
|
function invalidateLnurlSessionBySessionId(sessionId: string): void {
|
||||||
|
for (const [hash, session] of lnurlSessions.entries()) {
|
||||||
|
if (session.sessionId === sessionId && session.status === 'active') {
|
||||||
|
console.log('[LNURL Session] Invalidating previous session:', hash)
|
||||||
|
session.status = 'expired'
|
||||||
|
if (session.cleanup) session.cleanup()
|
||||||
|
if (session.linkId) {
|
||||||
|
lightningPub.deleteWithdrawLink(session.linkId).catch((err) => {
|
||||||
|
console.warn('[LNURL Session] Failed to delete old link:', err)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
lnurlSessions.delete(hash)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// Debit Approval Service
|
// Debit Approval Service
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
@ -1059,6 +1091,12 @@ function createATMServices(
|
||||||
console.log('[ATM Service] Using Nostr RPC (NIP-44 encrypted)')
|
console.log('[ATM Service] Using Nostr RPC (NIP-44 encrypted)')
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// Invalidate any previous LNURL session for this cash-in session
|
||||||
|
// (shouldn't happen — LNURL is generated once — but guard against it)
|
||||||
|
if (context.cashInSessionId) {
|
||||||
|
invalidateLnurlSessionBySessionId(context.cashInSessionId)
|
||||||
|
}
|
||||||
|
|
||||||
// Call the withdraw extension via Nostr RPC (encrypted)
|
// Call the withdraw extension via Nostr RPC (encrypted)
|
||||||
const response = await lightningPub.createWithdrawLink({
|
const response = await lightningPub.createWithdrawLink({
|
||||||
title: `ATM Cash-In ${context.cashInSessionId?.slice(0, 8) || 'session'}`,
|
title: `ATM Cash-In ${context.cashInSessionId?.slice(0, 8) || 'session'}`,
|
||||||
|
|
@ -1074,10 +1112,17 @@ function createATMServices(
|
||||||
throw new Error('RPC did not return LNURL in response')
|
throw new Error('RPC did not return LNURL in response')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!response.link?.id) {
|
||||||
|
console.warn(
|
||||||
|
'[ATM Service] Withdraw link response missing id — delete/update unavailable'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// Register session for tracking completion
|
// Register session for tracking completion
|
||||||
if (context.cashInSessionId) {
|
if (context.cashInSessionId) {
|
||||||
registerLnurlSession(
|
registerLnurlSession(
|
||||||
context.cashInSessionId,
|
context.cashInSessionId,
|
||||||
|
response.link.id || '',
|
||||||
response.link.unique_hash,
|
response.link.unique_hash,
|
||||||
context.satsAmount
|
context.satsAmount
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,9 @@ import {
|
||||||
type CreateWithdrawLinkParams,
|
type CreateWithdrawLinkParams,
|
||||||
type CreateWithdrawLinkResponse,
|
type CreateWithdrawLinkResponse,
|
||||||
type GetWithdrawLinkResponse,
|
type GetWithdrawLinkResponse,
|
||||||
|
type DeleteWithdrawLinkResponse,
|
||||||
|
type UpdateWithdrawLinkParams,
|
||||||
|
type UpdateWithdrawLinkResponse,
|
||||||
isRPCError,
|
isRPCError,
|
||||||
} from './types.js'
|
} from './types.js'
|
||||||
|
|
||||||
|
|
@ -320,6 +323,33 @@ export class LightningPubClient {
|
||||||
return response
|
return response
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete a withdraw link via the withdraw extension
|
||||||
|
*
|
||||||
|
* Permanently removes the link so it can no longer be claimed.
|
||||||
|
* Uses the link's `id` (not `unique_hash`).
|
||||||
|
*/
|
||||||
|
async deleteWithdrawLink(linkId: string): Promise<DeleteWithdrawLinkResponse> {
|
||||||
|
console.log('[LightningPub] Deleting withdraw link:', linkId)
|
||||||
|
const response = await this.sendRPC<DeleteWithdrawLinkResponse>('withdraw.deleteLink', {
|
||||||
|
id: linkId,
|
||||||
|
})
|
||||||
|
console.log('[LightningPub] Withdraw link deleted:', linkId)
|
||||||
|
return response
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update a withdraw link via the withdraw extension
|
||||||
|
*
|
||||||
|
* Can modify title, amounts, uses, or wait_time.
|
||||||
|
* Uses the link's `id` (not `unique_hash`).
|
||||||
|
*/
|
||||||
|
async updateWithdrawLink(params: UpdateWithdrawLinkParams): Promise<UpdateWithdrawLinkResponse> {
|
||||||
|
console.log('[LightningPub] Updating withdraw link:', params.id)
|
||||||
|
const response = await this.sendRPC<UpdateWithdrawLinkResponse>('withdraw.updateLink', params)
|
||||||
|
return response
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get payment state for an invoice
|
* Get payment state for an invoice
|
||||||
*
|
*
|
||||||
|
|
|
||||||
|
|
@ -71,9 +71,13 @@ export {
|
||||||
type PayInvoiceResponse,
|
type PayInvoiceResponse,
|
||||||
// LNURL types
|
// LNURL types
|
||||||
type LnurlLinkResponse,
|
type LnurlLinkResponse,
|
||||||
|
type WithdrawLink,
|
||||||
type CreateWithdrawLinkParams,
|
type CreateWithdrawLinkParams,
|
||||||
type CreateWithdrawLinkResponse,
|
type CreateWithdrawLinkResponse,
|
||||||
type GetWithdrawLinkResponse,
|
type GetWithdrawLinkResponse,
|
||||||
|
type DeleteWithdrawLinkResponse,
|
||||||
|
type UpdateWithdrawLinkParams,
|
||||||
|
type UpdateWithdrawLinkResponse,
|
||||||
// Exchange types
|
// Exchange types
|
||||||
type ExchangeRate,
|
type ExchangeRate,
|
||||||
// Config
|
// Config
|
||||||
|
|
|
||||||
|
|
@ -203,48 +203,64 @@ export interface CreateWithdrawLinkParams {
|
||||||
wait_time?: number
|
wait_time?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Withdraw link fields shared across responses */
|
||||||
|
export interface WithdrawLink {
|
||||||
|
/** Link ID (used for update/delete operations) */
|
||||||
|
id: string
|
||||||
|
/** Unique hash identifier (used in LNURL URLs) */
|
||||||
|
unique_hash: string
|
||||||
|
/** Bech32-encoded LNURL string */
|
||||||
|
lnurl: string
|
||||||
|
/** Title of the link */
|
||||||
|
title: string
|
||||||
|
/** Minimum withdrawable in sats */
|
||||||
|
min_withdrawable: number
|
||||||
|
/** Maximum withdrawable in sats */
|
||||||
|
max_withdrawable: number
|
||||||
|
/** Number of uses allowed */
|
||||||
|
uses: number
|
||||||
|
/** Number of times used */
|
||||||
|
used?: number
|
||||||
|
/** Wait time between uses */
|
||||||
|
wait_time: number
|
||||||
|
/** Whether the link has been fully used */
|
||||||
|
is_spent?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
/** Response from withdraw.createLink RPC */
|
/** Response from withdraw.createLink RPC */
|
||||||
export interface CreateWithdrawLinkResponse {
|
export interface CreateWithdrawLinkResponse {
|
||||||
/** The created withdraw link */
|
link: WithdrawLink
|
||||||
link: {
|
|
||||||
/** Unique hash identifier for the link */
|
|
||||||
unique_hash: string
|
|
||||||
/** Bech32-encoded LNURL string */
|
|
||||||
lnurl: string
|
|
||||||
/** Title of the link */
|
|
||||||
title: string
|
|
||||||
/** Minimum withdrawable in sats */
|
|
||||||
min_withdrawable: number
|
|
||||||
/** Maximum withdrawable in sats */
|
|
||||||
max_withdrawable: number
|
|
||||||
/** Number of uses remaining */
|
|
||||||
uses: number
|
|
||||||
/** Wait time between uses */
|
|
||||||
wait_time: number
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Response from withdraw.getLink RPC */
|
/** Response from withdraw.getLink RPC */
|
||||||
export interface GetWithdrawLinkResponse {
|
export interface GetWithdrawLinkResponse {
|
||||||
/** The withdraw link details */
|
link: WithdrawLink
|
||||||
link: {
|
}
|
||||||
/** Unique hash identifier */
|
|
||||||
unique_hash: string
|
/** Response from withdraw.deleteLink RPC */
|
||||||
/** Bech32-encoded LNURL string */
|
export interface DeleteWithdrawLinkResponse {
|
||||||
lnurl: string
|
success: boolean
|
||||||
/** Title of the link */
|
}
|
||||||
title: string
|
|
||||||
/** Minimum withdrawable in sats */
|
/** Parameters for withdraw.updateLink RPC */
|
||||||
min_withdrawable: number
|
export interface UpdateWithdrawLinkParams {
|
||||||
/** Maximum withdrawable in sats */
|
/** Link ID to update */
|
||||||
max_withdrawable: number
|
id: string
|
||||||
/** Number of uses remaining (0 = spent) */
|
/** New title */
|
||||||
uses: number
|
title?: string
|
||||||
/** Wait time between uses */
|
/** New minimum withdrawable in sats */
|
||||||
wait_time: number
|
min_withdrawable?: number
|
||||||
/** Whether the link has been used/spent */
|
/** New maximum withdrawable in sats */
|
||||||
is_spent?: boolean
|
max_withdrawable?: number
|
||||||
}
|
/** New number of uses (cannot reduce below current used count) */
|
||||||
|
uses?: number
|
||||||
|
/** New wait time between uses */
|
||||||
|
wait_time?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Response from withdraw.updateLink RPC */
|
||||||
|
export interface UpdateWithdrawLinkResponse {
|
||||||
|
link: WithdrawLink
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue