This commit is contained in:
boufni95 2023-11-23 22:01:18 +01:00
parent 969cd31775
commit dcb68d0069
19 changed files with 2333 additions and 2221 deletions

View file

@ -95,9 +95,9 @@ The nostr server will send back a message response, and inside the body there wi
- __User__: - __User__:
- expected context content - expected context content
- __app_user_id__: _string_
- __user_id__: _string_ - __user_id__: _string_
- __app_id__: _string_ - __app_id__: _string_
- __app_user_id__: _string_
- __Admin__: - __Admin__:
- expected context content - expected context content
@ -187,6 +187,8 @@ The nostr server will send back a message response, and inside the body there wi
- the request url __query__ can take the following string items: - the request url __query__ can take the following string items:
- k1 - k1
- amount - amount
- nostr
- lnurl
- This methods has an __empty__ __request__ body - This methods has an __empty__ __request__ body
- output: [HandleLnurlPayResponse](#HandleLnurlPayResponse) - output: [HandleLnurlPayResponse](#HandleLnurlPayResponse)
@ -372,153 +374,32 @@ The nostr server will send back a message response, and inside the body there wi
## Messages ## Messages
### The content of requests and response from the methods ### The content of requests and response from the methods
### EncryptionExchangeRequest
- __publicKey__: _string_
- __deviceId__: _string_
### SetMockInvoiceAsPaidRequest
- __invoice__: _string_
- __amount__: _number_
### AddAppRequest
- __name__: _string_
- __allow_user_creation__: _boolean_
### SendAppUserToAppPaymentRequest
- __from_user_identifier__: _string_
- __amount__: _number_
### GetAppUserLNURLInfoRequest
- __user_identifier__: _string_
- __base_url_override__: _string_
### DecodeInvoiceResponse
- __amount__: _number_
### PayInvoiceResponse
- __preimage__: _string_
- __amount_paid__: _number_
- __operation_id__: _string_
- __service_fee__: _number_
- __network_fee__: _number_
### Empty
### LnurlPayInfoResponse
- __tag__: _string_
- __callback__: _string_
- __maxSendable__: _number_
- __minSendable__: _number_
- __metadata__: _string_
### GetUserOperationsRequest
- __latestIncomingInvoice__: _number_
- __latestOutgoingInvoice__: _number_
- __latestIncomingTx__: _number_
- __latestOutgoingTx__: _number_
- __latestIncomingUserToUserPayment__: _number_
- __latestOutgoingUserToUserPayment__: _number_
### Product
- __id__: _string_
- __name__: _string_
- __price_sats__: _number_
### LnurlWithdrawInfoResponse
- __tag__: _string_
- __callback__: _string_
- __k1__: _string_
- __defaultDescription__: _string_
- __minWithdrawable__: _number_
- __maxWithdrawable__: _number_
- __balanceCheck__: _string_
- __payLink__: _string_
### AddAppUserInvoiceRequest
- __receiver_identifier__: _string_
- __payer_identifier__: _string_
- __http_callback_url__: _string_
- __invoice_req__: _[NewInvoiceRequest](#NewInvoiceRequest)_
### PayAppUserInvoiceRequest
- __user_identifier__: _string_
- __invoice__: _string_
- __amount__: _number_
### PayAddressRequest
- __address__: _string_
- __amoutSats__: _number_
- __satsPerVByte__: _number_
### GetUserOperationsResponse
- __latestOutgoingInvoiceOperations__: _[UserOperations](#UserOperations)_
- __latestIncomingInvoiceOperations__: _[UserOperations](#UserOperations)_
- __latestOutgoingTxOperations__: _[UserOperations](#UserOperations)_
- __latestIncomingTxOperations__: _[UserOperations](#UserOperations)_
- __latestOutgoingUserToUserPayemnts__: _[UserOperations](#UserOperations)_
- __latestIncomingUserToUserPayemnts__: _[UserOperations](#UserOperations)_
### LndGetInfoResponse
- __alias__: _string_
### NewInvoiceResponse
- __invoice__: _string_
### LnurlLinkResponse
- __lnurl__: _string_
- __k1__: _string_
### HandleLnurlPayResponse
- __pr__: _string_
- __routes__: ARRAY of: _[Empty](#Empty)_
### UserOperations
- __fromIndex__: _number_
- __toIndex__: _number_
- __operations__: ARRAY of: _[UserOperation](#UserOperation)_
### GetProductBuyLinkResponse
- __link__: _string_
### LiveUserOperation
- __id__: _string_
- __operation__: _[UserOperation](#UserOperation)_
### AddAppUserRequest
- __identifier__: _string_
- __fail_if_exists__: _boolean_
- __balance__: _number_
### NewInvoiceRequest
- __amountSats__: _number_
- __memo__: _string_
### DecodeInvoiceRequest
- __invoice__: _string_
### Application ### Application
- __name__: _string_ - __name__: _string_
- __id__: _string_ - __id__: _string_
- __balance__: _number_ - __balance__: _number_
- __npub__: _string_ - __npub__: _string_
### AppUser ### GetAppUserLNURLInfoRequest
- __identifier__: _string_
- __info__: _[UserInfo](#UserInfo)_
- __max_withdrawable__: _number_
### SetMockAppUserBalanceRequest
- __user_identifier__: _string_ - __user_identifier__: _string_
- __amount__: _number_ - __base_url_override__: _string_
### NewAddressRequest ### NewAddressRequest
- __addressType__: _[AddressType](#AddressType)_ - __addressType__: _[AddressType](#AddressType)_
### PayAddressResponse ### Product
- __txId__: _string_ - __id__: _string_
- __operation_id__: _string_ - __name__: _string_
- __service_fee__: _number_ - __price_sats__: _number_
- __network_fee__: _number_
### GetProductBuyLinkResponse
- __link__: _string_
### OpenChannelRequest
- __destination__: _string_
- __fundingAmount__: _number_
- __pushAmount__: _number_
- __closeAddress__: _string_
### UserOperation ### UserOperation
- __paidAtUnix__: _number_ - __paidAtUnix__: _number_
@ -530,57 +411,179 @@ The nostr server will send back a message response, and inside the body there wi
- __service_fee__: _number_ - __service_fee__: _number_
- __network_fee__: _number_ - __network_fee__: _number_
### EncryptionExchangeRequest
- __publicKey__: _string_
- __deviceId__: _string_
### AuthApp
- __app__: _[Application](#Application)_
- __auth_token__: _string_
### AppUser
- __identifier__: _string_
- __info__: _[UserInfo](#UserInfo)_
- __max_withdrawable__: _number_
### AddAppUserInvoiceRequest
- __receiver_identifier__: _string_
- __payer_identifier__: _string_
- __http_callback_url__: _string_
- __invoice_req__: _[NewInvoiceRequest](#NewInvoiceRequest)_
### SendAppUserToAppPaymentRequest
- __from_user_identifier__: _string_
- __amount__: _number_
### DecodeInvoiceRequest
- __invoice__: _string_
### LndGetInfoRequest ### LndGetInfoRequest
- __nodeId__: _number_ - __nodeId__: _number_
### AddAppRequest
- __name__: _string_
- __allow_user_creation__: _boolean_
### GetAppUserRequest ### GetAppUserRequest
- __user_identifier__: _string_ - __user_identifier__: _string_
### AuthAppRequest ### NewInvoiceResponse
- __name__: _string_ - __invoice__: _string_
- __allow_user_creation__: _boolean_ *this field is optional
### SetMockAppBalanceRequest ### SetMockInvoiceAsPaidRequest
- __amount__: _number_
### PayInvoiceRequest
- __invoice__: _string_ - __invoice__: _string_
- __amount__: _number_ - __amount__: _number_
### OpenChannelRequest ### NewInvoiceRequest
- __destination__: _string_ - __amountSats__: _number_
- __fundingAmount__: _number_ - __memo__: _string_
- __pushAmount__: _number_
- __closeAddress__: _string_
### UserInfo ### HandleLnurlPayResponse
- __userId__: _string_ - __pr__: _string_
- __balance__: _number_ - __routes__: ARRAY of: _[Empty](#Empty)_
- __max_withdrawable__: _number_
### AddProductRequest
- __name__: _string_
- __price_sats__: _number_
### AddAppInvoiceRequest ### AddAppInvoiceRequest
- __payer_identifier__: _string_ - __payer_identifier__: _string_
- __http_callback_url__: _string_ - __http_callback_url__: _string_
- __invoice_req__: _[NewInvoiceRequest](#NewInvoiceRequest)_ - __invoice_req__: _[NewInvoiceRequest](#NewInvoiceRequest)_
### PayAddressRequest
- __address__: _string_
- __amoutSats__: _number_
- __satsPerVByte__: _number_
### PayInvoiceRequest
- __invoice__: _string_
- __amount__: _number_
### UserOperations
- __fromIndex__: _number_
- __toIndex__: _number_
- __operations__: ARRAY of: _[UserOperation](#UserOperation)_
### AddAppUserRequest
- __identifier__: _string_
- __fail_if_exists__: _boolean_
- __balance__: _number_
### OpenChannelResponse
- __channelId__: _string_
### LnurlLinkResponse
- __lnurl__: _string_
- __k1__: _string_
### LnurlWithdrawInfoResponse
- __tag__: _string_
- __callback__: _string_
- __k1__: _string_
- __defaultDescription__: _string_
- __minWithdrawable__: _number_
- __maxWithdrawable__: _number_
- __balanceCheck__: _string_
- __payLink__: _string_
### LnurlPayInfoResponse
- __tag__: _string_
- __callback__: _string_
- __maxSendable__: _number_
- __minSendable__: _number_
- __metadata__: _string_
- __allowsNostr__: _boolean_
- __nostrPubkey__: _string_
### UserInfo
- __userId__: _string_
- __balance__: _number_
- __max_withdrawable__: _number_
### PayAddressResponse
- __txId__: _string_
- __operation_id__: _string_
- __service_fee__: _number_
- __network_fee__: _number_
### DecodeInvoiceResponse
- __amount__: _number_
### LndGetInfoResponse
- __alias__: _string_
### AuthAppRequest
- __name__: _string_
- __allow_user_creation__: _boolean_ *this field is optional
### SendAppUserToAppUserPaymentRequest ### SendAppUserToAppUserPaymentRequest
- __from_user_identifier__: _string_ - __from_user_identifier__: _string_
- __to_user_identifier__: _string_ - __to_user_identifier__: _string_
- __amount__: _number_ - __amount__: _number_
### SetMockAppUserBalanceRequest
- __user_identifier__: _string_
- __amount__: _number_
### SetMockAppBalanceRequest
- __amount__: _number_
### NewAddressResponse ### NewAddressResponse
- __address__: _string_ - __address__: _string_
### OpenChannelResponse ### LiveUserOperation
- __channelId__: _string_ - __operation__: _[UserOperation](#UserOperation)_
### AuthApp ### Empty
- __app__: _[Application](#Application)_
- __auth_token__: _string_ ### PayAppUserInvoiceRequest
- __user_identifier__: _string_
- __invoice__: _string_
- __amount__: _number_
### PayInvoiceResponse
- __preimage__: _string_
- __amount_paid__: _number_
- __operation_id__: _string_
- __service_fee__: _number_
- __network_fee__: _number_
### GetUserOperationsRequest
- __latestIncomingInvoice__: _number_
- __latestOutgoingInvoice__: _number_
- __latestIncomingTx__: _number_
- __latestOutgoingTx__: _number_
- __latestIncomingUserToUserPayment__: _number_
- __latestOutgoingUserToUserPayment__: _number_
### GetUserOperationsResponse
- __latestOutgoingInvoiceOperations__: _[UserOperations](#UserOperations)_
- __latestIncomingInvoiceOperations__: _[UserOperations](#UserOperations)_
- __latestOutgoingTxOperations__: _[UserOperations](#UserOperations)_
- __latestIncomingTxOperations__: _[UserOperations](#UserOperations)_
- __latestOutgoingUserToUserPayemnts__: _[UserOperations](#UserOperations)_
- __latestIncomingUserToUserPayemnts__: _[UserOperations](#UserOperations)_
### AddProductRequest
- __name__: _string_
- __price_sats__: _number_
## Enums ## Enums
### The enumerators used in the messages ### The enumerators used in the messages

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -136,7 +136,7 @@ service LightningPub {
option (auth_type) = "Guest"; option (auth_type) = "Guest";
option (http_method) = "get"; option (http_method) = "get";
option (http_route) = "/api/guest/lnurl_pay/handle"; option (http_route) = "/api/guest/lnurl_pay/handle";
option (query) = {items: ["k1", "amount"]}; option (query) = {items: ["k1", "amount", "nostr", "lnurl"]};
} }
//</Guest> //</Guest>

View file

@ -191,6 +191,8 @@ message LnurlPayInfoResponse {
int64 maxSendable = 3; // millisatoshi - unsafe overflow possible, but very unlikely int64 maxSendable = 3; // millisatoshi - unsafe overflow possible, but very unlikely
int64 minSendable = 4; // millisatoshi - unsafe overflow possible, but very unlikely int64 minSendable = 4; // millisatoshi - unsafe overflow possible, but very unlikely
string metadata = 5; string metadata = 5;
bool allowsNostr = 6;
string nostrPubkey = 7;
} }
message HandleLnurlPayResponse { message HandleLnurlPayResponse {
string pr = 1; string pr = 1;
@ -260,6 +262,5 @@ message GetProductBuyLinkResponse {
} }
message LiveUserOperation { message LiveUserOperation {
string id = 1; UserOperation operation = 1;
UserOperation operation = 2;
} }

View file

@ -16,14 +16,15 @@ const start = async () => {
const appsData = await mainHandler.storage.applicationStorage.GetApplications() const appsData = await mainHandler.storage.applicationStorage.GetApplications()
const apps = await Promise.all(appsData.map(app => { const apps = await Promise.all(appsData.map(app => {
if (!app.nostr_private_key) { // TMP -- if (!app.nostr_private_key || !app.nostr_public_key) { // TMP --
return mainHandler.storage.applicationStorage.GenerateApplicationKeys(app); return mainHandler.storage.applicationStorage.GenerateApplicationKeys(app);
} // -- } // --
else { else {
return { privateKey: app.nostr_private_key, publicKey: app.nostr_public_key, appId: app.app_id, name: app.name } return { privateKey: app.nostr_private_key, publicKey: app.nostr_public_key, appId: app.app_id, name: app.name }
} }
})) }))
nostrMiddleware(serverMethods, mainHandler, { ...nostrSettings, apps }) const { Send } = nostrMiddleware(serverMethods, mainHandler, { ...nostrSettings, apps })
mainHandler.attachNostrSend(Send)
const Server = NewServer(serverMethods, serverOptions(mainHandler)) const Server = NewServer(serverMethods, serverOptions(mainHandler))
if (process.argv[2] === 'unlock') { if (process.argv[2] === 'unlock') {
const u = process.argv[3] const u = process.argv[3]

View file

@ -1,11 +1,11 @@
import Main from "./services/main/index.js" import Main from "./services/main/index.js"
import Nostr from "./services/nostr/index.js" import Nostr from "./services/nostr/index.js"
import { NostrSettings } from "./services/nostr/handler.js" import { NostrSend, NostrSettings } from "./services/nostr/handler.js"
import * as Types from '../proto/autogenerated/ts/types.js' import * as Types from '../proto/autogenerated/ts/types.js'
import NewNostrTransport, { NostrRequest } from '../proto/autogenerated/ts/nostr_transport.js'; import NewNostrTransport, { NostrRequest } from '../proto/autogenerated/ts/nostr_transport.js';
const handledRequests: string[] = [] // TODO: - big memory leak here, add TTL const handledRequests: string[] = [] // TODO: - big memory leak here, add TTL
export default (serverMethods: Types.ServerMethods, mainHandler: Main, nostrSettings: NostrSettings) => { export default (serverMethods: Types.ServerMethods, mainHandler: Main, nostrSettings: NostrSettings): { Stop: () => void, Send: NostrSend } => {
const nostrTransport = NewNostrTransport(serverMethods, { const nostrTransport = NewNostrTransport(serverMethods, {
NostrUserAuthGuard: async (appId, pub) => { NostrUserAuthGuard: async (appId, pub) => {
const app = await mainHandler.storage.applicationStorage.GetApplication(appId || "") const app = await mainHandler.storage.applicationStorage.GetApplication(appId || "")
@ -22,10 +22,10 @@ export default (serverMethods: Types.ServerMethods, mainHandler: Main, nostrSett
return return
} }
nostrTransport({ ...j, appId: event.appId }, res => { nostrTransport({ ...j, appId: event.appId }, res => {
nostr.Send(event.appId, event.pub, JSON.stringify({ ...res, requestId: j.requestId })) nostr.Send(event.appId, { type: 'content', pub: event.pub, content: JSON.stringify({ ...res, requestId: j.requestId }) })
}) })
}) })
return { Stop: nostr.Stop } return { Stop: () => nostr.Stop, Send: (...args) => nostr.Send(...args) }
} }
/* /*

View file

@ -30,6 +30,7 @@ export default class {
} }
return decoded return decoded
} }
async GetUserInfo(ctx: Types.UserContext): Promise<Types.UserInfo> { async GetUserInfo(ctx: Types.UserContext): Promise<Types.UserInfo> {
const user = await this.storage.userStorage.GetUser(ctx.user_id) const user = await this.storage.userStorage.GetUser(ctx.user_id)
return { return {

View file

@ -53,7 +53,7 @@ export default class {
id: app.app_id, id: app.app_id,
name: app.name, name: app.name,
balance: app.owner.balance_sats, balance: app.owner.balance_sats,
npub: app.nostr_public_key npub: app.nostr_public_key || ""
}, },
auth_token: this.SignAppToken(app.app_id) auth_token: this.SignAppToken(app.app_id)
} }
@ -69,7 +69,7 @@ export default class {
id: app.app_id, id: app.app_id,
name: app.name, name: app.name,
balance: app.owner.balance_sats, balance: app.owner.balance_sats,
npub: app.nostr_public_key npub: app.nostr_public_key || ""
}, },
auth_token: this.SignAppToken(app.app_id) auth_token: this.SignAppToken(app.app_id)
} }
@ -81,7 +81,7 @@ export default class {
name: app.name, name: app.name,
id: app.app_id, id: app.app_id,
balance: app.owner.balance_sats, balance: app.owner.balance_sats,
npub: app.nostr_public_key npub: app.nostr_public_key || ""
} }
} }

View file

@ -11,6 +11,10 @@ import NewLightningHandler, { LoadLndSettingsFromEnv, LightningHandler } from ".
import { AddressPaidCb, InvoicePaidCb } from "../lnd/settings.js" import { AddressPaidCb, InvoicePaidCb } from "../lnd/settings.js"
import { getLogger, PubLogger } from "../helpers/logger.js" import { getLogger, PubLogger } from "../helpers/logger.js"
import AppUserManager from "./appUserManager.js" import AppUserManager from "./appUserManager.js"
import { Application } from '../storage/entity/Application.js'
import { UserReceivingInvoice, ZapInfo } from '../storage/entity/UserReceivingInvoice.js'
import { UnsignedEvent } from '../nostr/tools/event.js'
import { NostrSend } from '../nostr/handler.js'
export const LoadMainSettingsFromEnv = (test = false): MainSettings => { export const LoadMainSettingsFromEnv = (test = false): MainSettings => {
return { return {
lndSettings: LoadLndSettingsFromEnv(test), lndSettings: LoadLndSettingsFromEnv(test),
@ -47,6 +51,7 @@ export default class {
appUserManager: AppUserManager appUserManager: AppUserManager
paymentManager: PaymentManager paymentManager: PaymentManager
paymentSubs: Record<string, ((op: Types.UserOperation) => void) | null> = {} paymentSubs: Record<string, ((op: Types.UserOperation) => void) | null> = {}
nostrSend: NostrSend = () => { getLogger({})("nostr send not initialized yet") }
constructor(settings: MainSettings) { constructor(settings: MainSettings) {
this.settings = settings this.settings = settings
@ -59,6 +64,10 @@ export default class {
this.appUserManager = new AppUserManager(this.storage, this.settings, this.applicationManager) this.appUserManager = new AppUserManager(this.storage, this.settings, this.applicationManager)
} }
attachNostrSend(f: NostrSend) {
this.nostrSend = f
}
addressPaidCb: AddressPaidCb = (txOutput, address, amount, internal) => { addressPaidCb: AddressPaidCb = (txOutput, address, amount, internal) => {
this.storage.StartTransaction(async tx => { this.storage.StartTransaction(async tx => {
const userAddress = await this.storage.paymentStorage.GetAddressOwner(address, tx) const userAddress = await this.storage.paymentStorage.GetAddressOwner(address, tx)
@ -78,7 +87,7 @@ export default class {
const addedTx = await this.storage.paymentStorage.AddAddressReceivingTransaction(userAddress, txOutput.hash, txOutput.index, amount, fee, internal, tx) const addedTx = await this.storage.paymentStorage.AddAddressReceivingTransaction(userAddress, txOutput.hash, txOutput.index, amount, fee, internal, tx)
await this.storage.userStorage.IncrementUserBalance(userAddress.user.user_id, addedTx.paid_amount - fee, tx) await this.storage.userStorage.IncrementUserBalance(userAddress.user.user_id, addedTx.paid_amount - fee, tx)
const operationId = `${Types.UserOperationType.INCOMING_TX}-${userAddress.serial_id}` const operationId = `${Types.UserOperationType.INCOMING_TX}-${userAddress.serial_id}`
this.triggerSubs(userAddress.user.user_id, { amount, paidAtUnix: Date.now() / 1000, inbound: true, type: Types.UserOperationType.INCOMING_TX, identifier: userAddress.address, operationId, network_fee: 0, service_fee: fee }) this.sendOperationToNostr(userAddress.linkedApplication, userAddress.user.user_id, { amount, paidAtUnix: Date.now() / 1000, inbound: true, type: Types.UserOperationType.INCOMING_TX, identifier: userAddress.address, operationId, network_fee: 0, service_fee: fee })
} catch { } catch {
} }
@ -111,7 +120,8 @@ export default class {
await this.triggerPaidCallback(log, userInvoice.callbackUrl) await this.triggerPaidCallback(log, userInvoice.callbackUrl)
const operationId = `${Types.UserOperationType.INCOMING_INVOICE}-${userInvoice.serial_id}` const operationId = `${Types.UserOperationType.INCOMING_INVOICE}-${userInvoice.serial_id}`
this.triggerSubs(userInvoice.user.user_id, { amount, paidAtUnix: Date.now() / 1000, inbound: true, type: Types.UserOperationType.INCOMING_INVOICE, identifier: userInvoice.invoice, operationId, network_fee: 0, service_fee: fee }) this.sendOperationToNostr(userInvoice.linkedApplication, userInvoice.user.user_id, { amount, paidAtUnix: Date.now() / 1000, inbound: true, type: Types.UserOperationType.INCOMING_INVOICE, identifier: userInvoice.invoice, operationId, network_fee: 0, service_fee: fee })
this.createZapReceipt(userInvoice)
log("paid invoice processed successfully") log("paid invoice processed successfully")
} catch (err: any) { } catch (err: any) {
log("ERROR", "cannot process paid invoice", err.message || "") log("ERROR", "cannot process paid invoice", err.message || "")
@ -130,29 +140,32 @@ export default class {
} }
} }
triggerSubs(userId: string, op: Types.UserOperation) { async sendOperationToNostr(app: Application, userId: string, op: Types.UserOperation) {
const sub = this.paymentSubs[userId] const user = await this.storage.applicationStorage.GetAppUserFromUser(app, userId)
const log = getLogger({ userId }) if (!user || !user.nostr_public_key) {
if (!sub) { getLogger({})("cannot notify user, not a nostr user")
log("no sub found for user")
return return
} }
log("notifyng user of payment") const message: Types.LiveUserOperation & { requestId: string } = { operation: op, requestId: "GetLiveUserOperations" }
sub(op) this.nostrSend(app.app_id, { type: 'content', content: JSON.stringify(message), pub: user.nostr_public_key })
} }
async SubToPayment(ctx: Types.UserContext, cb: (res: Types.LiveUserOperation, err: Error | null) => void) { async createZapReceipt(invoice: UserReceivingInvoice) {
const sub = this.paymentSubs[ctx.user_id] const zapInfo = invoice.zap_info
const app = await this.storage.applicationStorage.GetApplication(ctx.app_id) if (!zapInfo || !invoice.linkedApplication || !invoice.linkedApplication.nostr_public_key) {
const log = getLogger({ appName: app.name, userId: ctx.user_id }) return
log("subbing user to payment")
await this.storage.applicationStorage.GetApplicationUser(app, ctx.app_user_id)
if (sub) {
log("overriding user payment stream")
} }
this.paymentSubs[ctx.user_id] = (op) => { const tags = [["p", zapInfo.pub], ["bolt11", invoice.invoice], ["description", zapInfo.description]]
const rand = crypto.randomBytes(16).toString('hex') if (zapInfo.eventId) {
cb({ id: rand, operation: op }, null) tags.push(["e", zapInfo.eventId])
} }
const event: UnsignedEvent = {
content: "",
created_at: invoice.paid_at_unix,
kind: 9735,
pubkey: invoice.linkedApplication.nostr_public_key,
tags,
}
this.nostrSend(invoice.linkedApplication.app_id, { type: 'event', event })
} }
} }

View file

@ -9,8 +9,9 @@ import { Application } from '../storage/entity/Application.js'
import { getLogger } from '../helpers/logger.js' import { getLogger } from '../helpers/logger.js'
import { UserReceivingAddress } from '../storage/entity/UserReceivingAddress.js' import { UserReceivingAddress } from '../storage/entity/UserReceivingAddress.js'
import { AddressPaidCb, InvoicePaidCb, PaidInvoice } from '../lnd/settings.js' import { AddressPaidCb, InvoicePaidCb, PaidInvoice } from '../lnd/settings.js'
import { UserReceivingInvoice } from '../storage/entity/UserReceivingInvoice.js' import { UserReceivingInvoice, ZapInfo } from '../storage/entity/UserReceivingInvoice.js'
import { SendCoinsResponse } from '../../../proto/lnd/lightning.js' import { SendCoinsResponse } from '../../../proto/lnd/lightning.js'
import { Event, verifiedSymbol, verifySignature } from '../nostr/tools/event.js'
interface UserOperationInfo { interface UserOperationInfo {
serial_id: number serial_id: number
paid_amount: number paid_amount: number
@ -283,24 +284,83 @@ export default class {
callback: `${url}?k1=${payK1.key}`, callback: `${url}?k1=${payK1.key}`,
maxSendable: remote * 1000, maxSendable: remote * 1000,
minSendable: 10000, minSendable: 10000,
metadata: defaultLnurlPayMetadata metadata: defaultLnurlPayMetadata,
allowsNostr: !!linkedApplication.nostr_public_key,
nostrPubkey: linkedApplication.nostr_public_key || ""
} }
} }
async GetLnurlPayInfo(payInfoK1: string): Promise<Types.LnurlPayInfoResponse> { async GetLnurlPayInfo(payInfoK1: string): Promise<Types.LnurlPayInfoResponse> {
const key = await this.storage.paymentStorage.UseUserEphemeralKey(payInfoK1, 'pay', true) const key = await this.storage.paymentStorage.UseUserEphemeralKey(payInfoK1, 'pay', true)
if (!key.linkedApplication) {
throw new Error("invalid lnurl request")
}
const { remote } = await this.lnd.ChannelBalance() const { remote } = await this.lnd.ChannelBalance()
return { return {
tag: 'payRequest', tag: 'payRequest',
callback: `${this.settings.serviceUrl}/api/guest/lnurl_pay/handle?k1=${payInfoK1}`, callback: `${this.settings.serviceUrl}/api/guest/lnurl_pay/handle?k1=${payInfoK1}`,
maxSendable: remote * 1000, maxSendable: remote * 1000,
minSendable: 10000, minSendable: 10000,
metadata: defaultLnurlPayMetadata metadata: defaultLnurlPayMetadata,
allowsNostr: !!key.linkedApplication.nostr_public_key,
nostrPubkey: key.linkedApplication.nostr_public_key || ""
} }
} }
async HandleLnurlPay(payK1: string, amountMillis: number): Promise<Types.HandleLnurlPayResponse> { parseTags(tag: string, tags: string[][], opts: { multiples?: boolean, required?: boolean } = {}): string[] {
const key = await this.storage.paymentStorage.UseUserEphemeralKey(payK1, 'pay', true) const { multiples, required } = opts
const found = tags.filter(t => t && t.length >= 2 && t[0] === tag)
if (found.length === 0) {
if (required) {
throw new Error(`missing tag for "${tag}"`)
}
return []
}
if (found.length === 1) {
const elements = found[0]
elements.shift()
if (elements.length === 0) {
throw new Error(`invalid content for "${tag}" tag`)
}
if (!multiples && elements.length !== 1) {
throw new Error(`too many contents for "${tag}" tag`)
}
return elements
}
throw new Error(`too many entries for "${tag}" tag`)
}
validateZapEvent(event: string, amt: number): ZapInfo {
const nostrEvent = JSON.parse(event) as Event
delete nostrEvent[verifiedSymbol]
const verified = verifySignature(nostrEvent)
if (!verified) {
throw new Error("nostr event not valid")
}
const p = this.parseTags("p", nostrEvent.tags, { required: true })
const e = this.parseTags("e", nostrEvent.tags)
const relays = this.parseTags("relays", nostrEvent.tags, { required: true, multiples: true })
const amount = this.parseTags("amount", nostrEvent.tags)
if (+amount !== amt) {
throw new Error("amount mismatch")
}
return { pub: p[0], eventId: e.length > 0 ? e[0] : "", relays, description: event }
}
async HandleLnurlPay(ctx: Types.HandleLnurlPay_Context): Promise<Types.HandleLnurlPayResponse> {
if (!ctx.k1 || !ctx.amount) {
throw new Error("invalid lnurl pay to handle")
}
const amountMillis = +ctx.amount
if (isNaN(amountMillis)) {
throw new Error("invalid amount in lnurl pay to handle")
}
let zapInfo: ZapInfo | undefined
if (ctx.nostr) {
zapInfo = this.validateZapEvent(ctx.nostr, amountMillis)
}
const key = await this.storage.paymentStorage.UseUserEphemeralKey(ctx.k1, 'pay', true)
const sats = amountMillis / 1000 const sats = amountMillis / 1000
if (!Number.isInteger(sats)) { if (!Number.isInteger(sats)) {
throw new Error("millisats amount must be integer sats amount") throw new Error("millisats amount must be integer sats amount")
@ -310,8 +370,8 @@ export default class {
} }
const invoice = await this.NewInvoice(key.user.user_id, { const invoice = await this.NewInvoice(key.user.user_id, {
amountSats: sats, amountSats: sats,
memo: defaultLnurlPayMetadata memo: zapInfo ? zapInfo.description : defaultLnurlPayMetadata
}, { expiry: defaultInvoiceExpiry, linkedApplication: key.linkedApplication }) }, { expiry: defaultInvoiceExpiry, linkedApplication: key.linkedApplication, zapInfo })
return { return {
pr: invoice.invoice, pr: invoice.invoice,
routes: [] routes: []

View file

@ -3,6 +3,9 @@ import { SimplePool, Sub, Event, UnsignedEvent, getEventHash, finishEvent, relay
import { encryptData, decryptData, getSharedSecret, decodePayload, encodePayload } from './nip44.js' import { encryptData, decryptData, getSharedSecret, decodePayload, encodePayload } from './nip44.js'
const handledEvents: string[] = [] // TODO: - big memory leak here, add TTL const handledEvents: string[] = [] // TODO: - big memory leak here, add TTL
type AppInfo = { appId: string, publicKey: string, privateKey: string, name: string } type AppInfo = { appId: string, publicKey: string, privateKey: string, name: string }
export type SendData = { type: "content", content: string, pub: string } | { type: "event", event: UnsignedEvent }
export type NostrSend = (appId: string, data: SendData, relays?: string[] | undefined) => void
export type NostrSettings = { export type NostrSettings = {
apps: AppInfo[] apps: AppInfo[]
relays: string[] relays: string[]
@ -21,8 +24,8 @@ type SettingsRequest = {
type SendRequest = { type SendRequest = {
type: 'send' type: 'send'
appId: string appId: string
pub: string data: SendData
message: string relays?: string[]
} }
type ReadyResponse = { type ReadyResponse = {
type: 'ready' type: 'ready'
@ -46,7 +49,7 @@ process.on("message", (message: ChildProcessRequest) => {
initSubprocessHandler(message.settings) initSubprocessHandler(message.settings)
break break
case 'send': case 'send':
sendToNostr(message.appId, message.pub, message.message) sendToNostr(message.appId, message.data, message.relays)
break break
default: default:
console.error("unknown nostr request", message) console.error("unknown nostr request", message)
@ -65,12 +68,12 @@ const initSubprocessHandler = (settings: NostrSettings) => {
}) })
}) })
} }
const sendToNostr = (appId: string, pub: string, message: string) => { const sendToNostr: NostrSend = (appId, data, relays) => {
if (!subProcessHandler) { if (!subProcessHandler) {
console.error("nostr was not initialized") console.error("nostr was not initialized")
return return
} }
subProcessHandler.Send(appId, pub, message) subProcessHandler.Send(appId, data, relays)
} }
send({ type: 'ready' }) send({ type: 'ready' })
@ -123,19 +126,25 @@ export default class Handler {
this.subs.push(sub) this.subs.push(sub)
} }
async Send(appId: string, pubKey: string, message: string) { async Send(appId: string, data: SendData, relays?: string[]) {
const appInfo = this.GetAppKeys({ appId }) const appInfo = this.GetAppKeys({ appId })
const decoded = await encryptData(message, getSharedSecret(appInfo.privateKey, pubKey)) let toSign: UnsignedEvent
const content = encodePayload(decoded) if (data.type === 'content') {
const event: UnsignedEvent = { const decoded = await encryptData(data.content, getSharedSecret(appInfo.privateKey, data.pub))
content, const content = encodePayload(decoded)
created_at: Math.floor(Date.now() / 1000), toSign = {
kind: 4, content,
pubkey: appInfo.publicKey, created_at: Math.floor(Date.now() / 1000),
tags: [['p', pubKey]], kind: 4,
pubkey: appInfo.publicKey,
tags: [['p', data.pub]],
}
} else {
toSign = data.event
} }
const signed = finishEvent(event, appInfo.privateKey)
this.pool.publish(this.settings.relays, signed).forEach(p => { const signed = finishEvent(toSign, appInfo.privateKey)
this.pool.publish(relays || this.settings.relays, signed).forEach(p => {
p.then(() => console.log("sent ok")) p.then(() => console.log("sent ok"))
p.catch(() => console.log("failed to send")) p.catch(() => console.log("failed to send"))
}) })

View file

@ -1,6 +1,6 @@
import { ChildProcess, fork } from 'child_process' import { ChildProcess, fork } from 'child_process'
import { EnvMustBeNonEmptyString } from "../helpers/envParser.js" import { EnvMustBeNonEmptyString } from "../helpers/envParser.js"
import { NostrSettings, NostrEvent, ChildProcessRequest, ChildProcessResponse } from "./handler.js" import { NostrSettings, NostrEvent, ChildProcessRequest, ChildProcessResponse, SendData } from "./handler.js"
type EventCallback = (event: NostrEvent) => void type EventCallback = (event: NostrEvent) => void
export const LoadNosrtSettingsFromEnv = (test = false) => { export const LoadNosrtSettingsFromEnv = (test = false) => {
return { return {
@ -31,8 +31,8 @@ export default class NostrSubprocess {
this.childProcess.send(message) this.childProcess.send(message)
} }
Send(appId: string, pub: string, message: string) { Send(appId: string, data: SendData, relays?: string[]) {
this.sendToChildProcess({ type: 'send', pub, message, appId }) this.sendToChildProcess({ type: 'send', data, appId, relays })
} }
Stop() { Stop() {
this.childProcess.kill() this.childProcess.kill()

View file

@ -1,4 +1,5 @@
import * as Types from '../../../proto/autogenerated/ts/types.js' import * as Types from '../../../proto/autogenerated/ts/types.js'
import { getLogger } from '../helpers/logger.js'
import Main from '../main/index.js' import Main from '../main/index.js'
export default (mainHandler: Main): Types.ServerMethods => { export default (mainHandler: Main): Types.ServerMethods => {
return { return {
@ -70,13 +71,7 @@ export default (mainHandler: Main): Types.ServerMethods => {
return mainHandler.paymentManager.GetLnurlPayInfo(ctx.k1) return mainHandler.paymentManager.GetLnurlPayInfo(ctx.k1)
}, },
HandleLnurlPay: async (ctx) => { HandleLnurlPay: async (ctx) => {
if (!ctx.k1 || !ctx.amount) { return mainHandler.paymentManager.HandleLnurlPay(ctx)
throw new Error("invalid lnurl pay to handle")
}
if (isNaN(+ctx.amount)) {
throw new Error("invalid amount in lnurl pay to handle")
}
return mainHandler.paymentManager.HandleLnurlPay(ctx.k1, +ctx.amount)
}, },
AddProduct: async (ctx, req) => { AddProduct: async (ctx, req) => {
return mainHandler.productManager.AddProduct(ctx.user_id, req) return mainHandler.productManager.AddProduct(ctx.user_id, req)
@ -178,7 +173,6 @@ export default (mainHandler: Main): Types.ServerMethods => {
await mainHandler.applicationManager.SetMockAppBalance(ctx.app_id, req) await mainHandler.applicationManager.SetMockAppBalance(ctx.app_id, req)
}, },
GetLiveUserOperations: async (ctx, cb) => { GetLiveUserOperations: async (ctx, cb) => {
mainHandler.SubToPayment(ctx, cb)
} }
} }
} }

View file

@ -119,8 +119,8 @@ export default class {
return found return found
} }
async IsApplicationUser(userId: string, entityManager = this.DB): Promise<ApplicationUser | null> { async GetAppUserFromUser(application: Application, userId: string, entityManager = this.DB): Promise<ApplicationUser | null> {
return await entityManager.getRepository(ApplicationUser).findOne({ where: { user: { user_id: userId } } }) return await entityManager.getRepository(ApplicationUser).findOne({ where: { user: { user_id: userId }, application: { app_id: application.app_id } } })
} }
async IsApplicationOwner(userId: string, entityManager = this.DB) { async IsApplicationOwner(userId: string, entityManager = this.DB) {

View file

@ -22,10 +22,10 @@ export class Application {
allow_user_creation: boolean allow_user_creation: boolean
@Column({ nullable: true, unique: true }) @Column({ nullable: true, unique: true })
nostr_private_key: string nostr_private_key?: string
@Column({ nullable: true, unique: true }) @Column({ nullable: true, unique: true })
nostr_public_key: string nostr_public_key?: string
@CreateDateColumn() @CreateDateColumn()
created_at: Date created_at: Date

View file

@ -21,7 +21,7 @@ export class ApplicationUser {
identifier: string identifier: string
@Column({ nullable: true, unique: true }) @Column({ nullable: true, unique: true })
nostr_public_key: string nostr_public_key?: string
@CreateDateColumn() @CreateDateColumn()
created_at: Date created_at: Date

View file

@ -2,7 +2,12 @@ import { Entity, PrimaryGeneratedColumn, Column, Index, Check, ManyToOne, JoinCo
import { Product } from "./Product.js" import { Product } from "./Product.js"
import { User } from "./User.js" import { User } from "./User.js"
import { Application } from "./Application.js" import { Application } from "./Application.js"
export type ZapInfo = {
pub: string
eventId: string
relays: string[]
description: string
}
@Entity() @Entity()
export class UserReceivingInvoice { export class UserReceivingInvoice {
@ -47,6 +52,12 @@ export class UserReceivingInvoice {
@ManyToOne(type => Application, { eager: true }) @ManyToOne(type => Application, { eager: true })
linkedApplication: Application | null linkedApplication: Application | null
@Column({
nullable: true,
type: 'simple-json'
})
zap_info?: ZapInfo
@CreateDateColumn() @CreateDateColumn()
created_at: Date created_at: Date

View file

@ -3,7 +3,7 @@ import { DataSource, EntityManager, MoreThan, MoreThanOrEqual } from "typeorm"
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 } 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';
@ -11,7 +11,7 @@ import { AddressReceivingTransaction } from './entity/AddressReceivingTransactio
import { UserInvoicePayment } from './entity/UserInvoicePayment.js'; import { UserInvoicePayment } from './entity/UserInvoicePayment.js';
import { UserToUserPayment } from './entity/UserToUserPayment.js'; import { UserToUserPayment } from './entity/UserToUserPayment.js';
import { Application } from './entity/Application.js'; import { Application } from './entity/Application.js';
export type InboundOptionals = { product?: Product, callbackUrl?: string, expiry: number, expectedPayer?: User, linkedApplication?: Application } export type InboundOptionals = { product?: Product, callbackUrl?: string, expiry: number, expectedPayer?: User, linkedApplication?: Application, zapInfo?: ZapInfo }
export const defaultInvoiceExpiry = 60 * 60 export const defaultInvoiceExpiry = 60 * 60
export default class { export default class {
DB: DataSource | EntityManager DB: DataSource | EntityManager
@ -89,7 +89,8 @@ export default class {
product: options.product, product: options.product,
expires_at_unix: Math.floor(Date.now() / 1000) + options.expiry, expires_at_unix: Math.floor(Date.now() / 1000) + options.expiry,
payer: options.expectedPayer, payer: options.expectedPayer,
linkedApplication: options.linkedApplication linkedApplication: options.linkedApplication,
zap_info: options.zapInfo
}) })
return entityManager.getRepository(UserReceivingInvoice).save(newUserInvoice) return entityManager.getRepository(UserReceivingInvoice).save(newUserInvoice)
} }