This commit is contained in:
shocknet-justin 2025-12-12 14:35:31 -05:00
parent 8e01b284c7
commit 1eb3a1f6ea
6 changed files with 30 additions and 35 deletions

View file

@ -7,7 +7,7 @@ import { ApplicationUser } from '../storage/entity/ApplicationUser.js'
import { PubLogger, getLogger } from '../helpers/logger.js' import { PubLogger, getLogger } from '../helpers/logger.js'
import crypto from 'crypto' import crypto from 'crypto'
import { Application } from '../storage/entity/Application.js' import { Application } from '../storage/entity/Application.js'
import { ZapInfo, ClinkRequester } from '../storage/entity/UserReceivingInvoice.js' import { ZapInfo } from '../storage/entity/UserReceivingInvoice.js'
import { nofferEncode, ndebitEncode, OfferPriceType, nmanageEncode } from '@shocknet/clink-sdk' import { nofferEncode, ndebitEncode, OfferPriceType, nmanageEncode } from '@shocknet/clink-sdk'
import SettingsManager from './settingsManager.js' import SettingsManager from './settingsManager.js'
const TOKEN_EXPIRY_TIME = 2 * 60 * 1000 // 2 minutes, in milliseconds const TOKEN_EXPIRY_TIME = 2 * 60 * 1000 // 2 minutes, in milliseconds
@ -185,7 +185,7 @@ export default class {
return invoice return invoice
} }
async AddAppUserInvoice(appId: string, req: Types.AddAppUserInvoiceRequest, clinkRequester?: ClinkRequester): Promise<Types.NewInvoiceResponse> { async AddAppUserInvoice(appId: string, req: Types.AddAppUserInvoiceRequest, clinkRequester?: { pub: string, eventId: string }): Promise<Types.NewInvoiceResponse> {
const app = await this.storage.applicationStorage.GetApplication(appId) const app = await this.storage.applicationStorage.GetApplication(appId)
const log = getLogger({ appName: app.name }) const log = getLogger({ appName: app.name })
const receiver = await this.storage.applicationStorage.GetApplicationUser(app, req.receiver_identifier) const receiver = await this.storage.applicationStorage.GetApplicationUser(app, req.receiver_identifier)
@ -201,7 +201,8 @@ export default class {
offerId: req.offer_string, payerData: req.payer_data?.data, rejectUnauthorized: req.rejectUnauthorized, offerId: req.offer_string, payerData: req.payer_data?.data, rejectUnauthorized: req.rejectUnauthorized,
token: req.token, token: req.token,
blind: req.invoice_req.blind, blind: req.invoice_req.blind,
clinkRequester clinkRequesterPub: clinkRequester?.pub,
clinkRequesterEventId: clinkRequester?.eventId
} }
const appUserInvoice = await this.paymentManager.NewInvoice(receiver.user.user_id, req.invoice_req, opts) const appUserInvoice = await this.paymentManager.NewInvoice(receiver.user.user_id, req.invoice_req, opts)
return { return {

View file

@ -292,7 +292,7 @@ export default class {
} }
// Send CLINK receipt if this invoice was from a noffer request // Send CLINK receipt if this invoice was from a noffer request
try { try {
if (userInvoice.clink_requester) { if (userInvoice.clink_requester_pub && userInvoice.clink_requester_event_id) {
this.createClinkReceipt(log, userInvoice) this.createClinkReceipt(log, userInvoice)
} }
} catch (err: any) { } catch (err: any) {
@ -440,13 +440,12 @@ export default class {
} }
async createClinkReceipt(log: PubLogger, invoice: UserReceivingInvoice) { async createClinkReceipt(log: PubLogger, invoice: UserReceivingInvoice) {
const clinkRequester = invoice.clink_requester if (!invoice.clink_requester_pub || !invoice.clink_requester_event_id || !invoice.linkedApplication) {
if (!clinkRequester || !invoice.linkedApplication) {
return return
} }
log("📤 [CLINK RECEIPT] Sending payment receipt", { log("📤 [CLINK RECEIPT] Sending payment receipt", {
toPub: clinkRequester.pub, toPub: invoice.clink_requester_pub,
eventId: clinkRequester.eventId eventId: invoice.clink_requester_event_id
}) })
// Receipt payload - payer's wallet already has the preimage // Receipt payload - payer's wallet already has the preimage
const content = JSON.stringify({ res: 'ok' }) const content = JSON.stringify({ res: 'ok' })
@ -456,14 +455,14 @@ export default class {
kind: 21001, kind: 21001,
pubkey: "", pubkey: "",
tags: [ tags: [
["p", clinkRequester.pub], ["p", invoice.clink_requester_pub],
["e", clinkRequester.eventId], ["e", invoice.clink_requester_event_id],
["clink_version", "1"] ["clink_version", "1"]
], ],
} }
this.nostrSend( this.nostrSend(
{ type: 'app', appId: clinkRequester.appId }, { type: 'app', appId: invoice.linkedApplication.app_id },
{ type: 'event', event, encrypt: { toPub: clinkRequester.pub } } { type: 'event', event, encrypt: { toPub: invoice.clink_requester_pub } }
) )
} }

View file

@ -10,7 +10,6 @@ import { UserOffer } from '../storage/entity/UserOffer.js';
import { LiquidityManager } from "./liquidityManager.js" import { LiquidityManager } from "./liquidityManager.js"
import { NofferData, OfferPriceType, nofferEncode } from '@shocknet/clink-sdk'; import { NofferData, OfferPriceType, nofferEncode } from '@shocknet/clink-sdk';
import SettingsManager from "./settingsManager.js"; import SettingsManager from "./settingsManager.js";
import { ClinkRequester } from '../storage/entity/UserReceivingInvoice.js';
const mapToOfferConfig = (appUserId: string, offer: UserOffer, { pubkey, relay }: { pubkey: string, relay: string }): Types.OfferConfig => { const mapToOfferConfig = (appUserId: string, offer: UserOffer, { pubkey, relay }: { pubkey: string, relay: string }): Types.OfferConfig => {
const offerStr = offer.offer_id const offerStr = offer.offer_id
@ -166,10 +165,9 @@ export class OfferManager {
}) })
// Store requester info for sending receipt when invoice is paid // Store requester info for sending receipt when invoice is paid
const clinkRequester: ClinkRequester = { const clinkRequester = {
pub: event.pub, pub: event.pub,
eventId: event.id, eventId: event.id
appId: event.appId
} }
const offerInvoice = await this.getNofferInvoice(offerReq, event.appId, clinkRequester) const offerInvoice = await this.getNofferInvoice(offerReq, event.appId, clinkRequester)
@ -206,7 +204,7 @@ export class OfferManager {
return return
} }
async HandleDefaultUserOffer(offerReq: NofferData, appId: string, remote: number, { memo, expiry }: { memo?: string, expiry?: number }, clinkRequester?: ClinkRequester): Promise<{ success: true, invoice: string } | { success: false, code: number, max: number }> { async HandleDefaultUserOffer(offerReq: NofferData, appId: string, remote: number, { memo, expiry }: { memo?: string, expiry?: number }, clinkRequester?: { pub: string, eventId: string }): Promise<{ success: true, invoice: string } | { success: false, code: number, max: number }> {
const { amount_sats: amount, offer } = offerReq const { amount_sats: amount, offer } = offerReq
if (!amount || isNaN(amount) || amount < 10 || amount > remote) { if (!amount || isNaN(amount) || amount < 10 || amount > remote) {
return { success: false, code: 5, max: remote } return { success: false, code: 5, max: remote }
@ -219,7 +217,7 @@ export class OfferManager {
return { success: true, invoice: res.invoice } return { success: true, invoice: res.invoice }
} }
async HandleUserOffer(offerReq: NofferData, appId: string, remote: number, clinkRequester?: ClinkRequester): Promise<{ success: true, invoice: string } | { success: false, code: number, max: number }> { async HandleUserOffer(offerReq: NofferData, appId: string, remote: number, clinkRequester?: { pub: string, eventId: string }): Promise<{ success: true, invoice: string } | { success: false, code: number, max: number }> {
const { amount_sats: amount, offer } = offerReq const { amount_sats: amount, offer } = offerReq
const userOffer = await this.storage.offerStorage.GetOffer(offer) const userOffer = await this.storage.offerStorage.GetOffer(offer)
const expiry = offerReq.expires_in_seconds ? offerReq.expires_in_seconds : undefined const expiry = offerReq.expires_in_seconds ? offerReq.expires_in_seconds : undefined
@ -262,7 +260,7 @@ export class OfferManager {
return { success: true, invoice: res.invoice } return { success: true, invoice: res.invoice }
} }
async getNofferInvoice(offerReq: NofferData, appId: string, clinkRequester?: ClinkRequester): Promise<{ success: true, invoice: string } | { success: false, code: number, max: number }> { async getNofferInvoice(offerReq: NofferData, appId: string, clinkRequester?: { pub: string, eventId: string }): Promise<{ success: true, invoice: string } | { success: false, code: number, max: number }> {
try { try {
const { remote } = await this.lnd.ChannelBalance() const { remote } = await this.lnd.ChannelBalance()
let maxSendable = remote let maxSendable = remote

View file

@ -9,11 +9,6 @@ export type ZapInfo = {
description: string description: string
} }
export type ClinkRequester = {
pub: string // requester's pubkey
eventId: string // original request event ID
appId: string // app context for nostrSend
}
@Entity() @Entity()
@Index("recv_invoice_paid_serial", ["user.serial_id", "paid_at_unix", "serial_id"], { where: "paid_at_unix > 0" }) @Index("recv_invoice_paid_serial", ["user.serial_id", "paid_at_unix", "serial_id"], { where: "paid_at_unix > 0" })
export class UserReceivingInvoice { export class UserReceivingInvoice {
@ -86,11 +81,11 @@ export class UserReceivingInvoice {
}) })
liquidityProvider?: string liquidityProvider?: string
@Column({ @Column({ nullable: true })
nullable: true, clink_requester_pub?: string
type: 'simple-json'
}) @Column({ nullable: true })
clink_requester?: ClinkRequester clink_requester_event_id?: string
@CreateDateColumn() @CreateDateColumn()
created_at: Date created_at: Date

View file

@ -4,12 +4,13 @@ export class ClinkRequester1765354000000 implements MigrationInterface {
name = 'ClinkRequester1765354000000' name = 'ClinkRequester1765354000000'
public async up(queryRunner: QueryRunner): Promise<void> { public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "user_receiving_invoice" ADD COLUMN "clink_requester" text`); await queryRunner.query(`ALTER TABLE "user_receiving_invoice" ADD COLUMN "clink_requester_pub" varchar`);
await queryRunner.query(`ALTER TABLE "user_receiving_invoice" ADD COLUMN "clink_requester_event_id" varchar`);
} }
public async down(queryRunner: QueryRunner): Promise<void> { public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "user_receiving_invoice" DROP COLUMN "clink_requester"`); await queryRunner.query(`ALTER TABLE "user_receiving_invoice" DROP COLUMN "clink_requester_pub"`);
await queryRunner.query(`ALTER TABLE "user_receiving_invoice" DROP COLUMN "clink_requester_event_id"`);
} }
} }

View file

@ -3,7 +3,7 @@ import { And, Between, Equal, FindOperator, IsNull, LessThan, LessThanOrEqual, M
import { User } from './entity/User.js'; import { User } from './entity/User.js';
import { UserTransactionPayment } from './entity/UserTransactionPayment.js'; import { UserTransactionPayment } from './entity/UserTransactionPayment.js';
import { EphemeralKeyType, UserEphemeralKey } from './entity/UserEphemeralKey.js'; import { EphemeralKeyType, UserEphemeralKey } from './entity/UserEphemeralKey.js';
import { UserReceivingInvoice, ZapInfo, ClinkRequester } from './entity/UserReceivingInvoice.js'; import { UserReceivingInvoice, ZapInfo } from './entity/UserReceivingInvoice.js';
import { UserReceivingAddress } from './entity/UserReceivingAddress.js'; import { UserReceivingAddress } from './entity/UserReceivingAddress.js';
import { Product } from './entity/Product.js'; import { Product } from './entity/Product.js';
import UserStorage from './userStorage.js'; import UserStorage from './userStorage.js';
@ -14,7 +14,7 @@ import { Application } from './entity/Application.js';
import TransactionsQueue from "./db/transactionsQueue.js"; import TransactionsQueue from "./db/transactionsQueue.js";
import { LoggedEvent } from './eventsLog.js'; import { LoggedEvent } from './eventsLog.js';
import { StorageInterface } from './db/storageInterface.js'; import { StorageInterface } from './db/storageInterface.js';
export type InboundOptionals = { product?: Product, callbackUrl?: string, expiry: number, expectedPayer?: User, linkedApplication?: Application, zapInfo?: ZapInfo, offerId?: string, payerData?: Record<string, string>, rejectUnauthorized?: boolean, token?: string, blind?: boolean, clinkRequester?: ClinkRequester } export type InboundOptionals = { product?: Product, callbackUrl?: string, expiry: number, expectedPayer?: User, linkedApplication?: Application, zapInfo?: ZapInfo, offerId?: string, payerData?: Record<string, string>, rejectUnauthorized?: boolean, token?: string, blind?: boolean, clinkRequesterPub?: string, clinkRequesterEventId?: string }
export const defaultInvoiceExpiry = 60 * 60 export const defaultInvoiceExpiry = 60 * 60
export default class { export default class {
dbs: StorageInterface dbs: StorageInterface
@ -130,7 +130,8 @@ export default class {
payer_data: options.payerData, payer_data: options.payerData,
rejectUnauthorized: options.rejectUnauthorized, rejectUnauthorized: options.rejectUnauthorized,
bearer_token: options.token, bearer_token: options.token,
clink_requester: options.clinkRequester clink_requester_pub: options.clinkRequesterPub,
clink_requester_event_id: options.clinkRequesterEventId
}, txId) }, txId)
} }