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__:
- expected context content
- __app_user_id__: _string_
- __user_id__: _string_
- __app_id__: _string_
- __app_user_id__: _string_
- __Admin__:
- 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:
- k1
- amount
- nostr
- lnurl
- This methods has an __empty__ __request__ body
- output: [HandleLnurlPayResponse](#HandleLnurlPayResponse)
@ -372,153 +374,32 @@ The nostr server will send back a message response, and inside the body there wi
## Messages
### 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
- __name__: _string_
- __id__: _string_
- __balance__: _number_
- __npub__: _string_
### AppUser
- __identifier__: _string_
- __info__: _[UserInfo](#UserInfo)_
- __max_withdrawable__: _number_
### SetMockAppUserBalanceRequest
### GetAppUserLNURLInfoRequest
- __user_identifier__: _string_
- __amount__: _number_
- __base_url_override__: _string_
### NewAddressRequest
- __addressType__: _[AddressType](#AddressType)_
### PayAddressResponse
- __txId__: _string_
- __operation_id__: _string_
- __service_fee__: _number_
- __network_fee__: _number_
### Product
- __id__: _string_
- __name__: _string_
- __price_sats__: _number_
### GetProductBuyLinkResponse
- __link__: _string_
### OpenChannelRequest
- __destination__: _string_
- __fundingAmount__: _number_
- __pushAmount__: _number_
- __closeAddress__: _string_
### UserOperation
- __paidAtUnix__: _number_
@ -530,57 +411,179 @@ The nostr server will send back a message response, and inside the body there wi
- __service_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
- __nodeId__: _number_
### AddAppRequest
- __name__: _string_
- __allow_user_creation__: _boolean_
### GetAppUserRequest
- __user_identifier__: _string_
### AuthAppRequest
- __name__: _string_
- __allow_user_creation__: _boolean_ *this field is optional
### NewInvoiceResponse
- __invoice__: _string_
### SetMockAppBalanceRequest
- __amount__: _number_
### PayInvoiceRequest
### SetMockInvoiceAsPaidRequest
- __invoice__: _string_
- __amount__: _number_
### OpenChannelRequest
- __destination__: _string_
- __fundingAmount__: _number_
- __pushAmount__: _number_
- __closeAddress__: _string_
### NewInvoiceRequest
- __amountSats__: _number_
- __memo__: _string_
### UserInfo
- __userId__: _string_
- __balance__: _number_
- __max_withdrawable__: _number_
### AddProductRequest
- __name__: _string_
- __price_sats__: _number_
### HandleLnurlPayResponse
- __pr__: _string_
- __routes__: ARRAY of: _[Empty](#Empty)_
### AddAppInvoiceRequest
- __payer_identifier__: _string_
- __http_callback_url__: _string_
- __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
- __from_user_identifier__: _string_
- __to_user_identifier__: _string_
- __amount__: _number_
### SetMockAppUserBalanceRequest
- __user_identifier__: _string_
- __amount__: _number_
### SetMockAppBalanceRequest
- __amount__: _number_
### NewAddressResponse
- __address__: _string_
### OpenChannelResponse
- __channelId__: _string_
### LiveUserOperation
- __operation__: _[UserOperation](#UserOperation)_
### AuthApp
- __app__: _[Application](#Application)_
- __auth_token__: _string_
### Empty
### 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
### 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 (http_method) = "get";
option (http_route) = "/api/guest/lnurl_pay/handle";
option (query) = {items: ["k1", "amount"]};
option (query) = {items: ["k1", "amount", "nostr", "lnurl"]};
}
//</Guest>

View file

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

View file

@ -16,14 +16,15 @@ const start = async () => {
const appsData = await mainHandler.storage.applicationStorage.GetApplications()
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);
} // --
else {
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))
if (process.argv[2] === 'unlock') {
const u = process.argv[3]

View file

@ -1,11 +1,11 @@
import Main from "./services/main/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 NewNostrTransport, { NostrRequest } from '../proto/autogenerated/ts/nostr_transport.js';
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, {
NostrUserAuthGuard: async (appId, pub) => {
const app = await mainHandler.storage.applicationStorage.GetApplication(appId || "")
@ -22,10 +22,10 @@ export default (serverMethods: Types.ServerMethods, mainHandler: Main, nostrSett
return
}
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
}
async GetUserInfo(ctx: Types.UserContext): Promise<Types.UserInfo> {
const user = await this.storage.userStorage.GetUser(ctx.user_id)
return {

View file

@ -53,7 +53,7 @@ export default class {
id: app.app_id,
name: app.name,
balance: app.owner.balance_sats,
npub: app.nostr_public_key
npub: app.nostr_public_key || ""
},
auth_token: this.SignAppToken(app.app_id)
}
@ -69,7 +69,7 @@ export default class {
id: app.app_id,
name: app.name,
balance: app.owner.balance_sats,
npub: app.nostr_public_key
npub: app.nostr_public_key || ""
},
auth_token: this.SignAppToken(app.app_id)
}
@ -81,7 +81,7 @@ export default class {
name: app.name,
id: app.app_id,
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 { getLogger, PubLogger } from "../helpers/logger.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 => {
return {
lndSettings: LoadLndSettingsFromEnv(test),
@ -47,6 +51,7 @@ export default class {
appUserManager: AppUserManager
paymentManager: PaymentManager
paymentSubs: Record<string, ((op: Types.UserOperation) => void) | null> = {}
nostrSend: NostrSend = () => { getLogger({})("nostr send not initialized yet") }
constructor(settings: MainSettings) {
this.settings = settings
@ -59,6 +64,10 @@ export default class {
this.appUserManager = new AppUserManager(this.storage, this.settings, this.applicationManager)
}
attachNostrSend(f: NostrSend) {
this.nostrSend = f
}
addressPaidCb: AddressPaidCb = (txOutput, address, amount, internal) => {
this.storage.StartTransaction(async 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)
await this.storage.userStorage.IncrementUserBalance(userAddress.user.user_id, addedTx.paid_amount - fee, tx)
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 {
}
@ -111,7 +120,8 @@ export default class {
await this.triggerPaidCallback(log, userInvoice.callbackUrl)
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")
} catch (err: any) {
log("ERROR", "cannot process paid invoice", err.message || "")
@ -130,29 +140,32 @@ export default class {
}
}
triggerSubs(userId: string, op: Types.UserOperation) {
const sub = this.paymentSubs[userId]
const log = getLogger({ userId })
if (!sub) {
log("no sub found for user")
async sendOperationToNostr(app: Application, userId: string, op: Types.UserOperation) {
const user = await this.storage.applicationStorage.GetAppUserFromUser(app, userId)
if (!user || !user.nostr_public_key) {
getLogger({})("cannot notify user, not a nostr user")
return
}
log("notifyng user of payment")
sub(op)
const message: Types.LiveUserOperation & { requestId: string } = { operation: op, requestId: "GetLiveUserOperations" }
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) {
const sub = this.paymentSubs[ctx.user_id]
const app = await this.storage.applicationStorage.GetApplication(ctx.app_id)
const log = getLogger({ appName: app.name, userId: ctx.user_id })
log("subbing user to payment")
await this.storage.applicationStorage.GetApplicationUser(app, ctx.app_user_id)
if (sub) {
log("overriding user payment stream")
async createZapReceipt(invoice: UserReceivingInvoice) {
const zapInfo = invoice.zap_info
if (!zapInfo || !invoice.linkedApplication || !invoice.linkedApplication.nostr_public_key) {
return
}
this.paymentSubs[ctx.user_id] = (op) => {
const rand = crypto.randomBytes(16).toString('hex')
cb({ id: rand, operation: op }, null)
const tags = [["p", zapInfo.pub], ["bolt11", invoice.invoice], ["description", zapInfo.description]]
if (zapInfo.eventId) {
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 { UserReceivingAddress } from '../storage/entity/UserReceivingAddress.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 { Event, verifiedSymbol, verifySignature } from '../nostr/tools/event.js'
interface UserOperationInfo {
serial_id: number
paid_amount: number
@ -283,24 +284,83 @@ export default class {
callback: `${url}?k1=${payK1.key}`,
maxSendable: remote * 1000,
minSendable: 10000,
metadata: defaultLnurlPayMetadata
metadata: defaultLnurlPayMetadata,
allowsNostr: !!linkedApplication.nostr_public_key,
nostrPubkey: linkedApplication.nostr_public_key || ""
}
}
async GetLnurlPayInfo(payInfoK1: string): Promise<Types.LnurlPayInfoResponse> {
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()
return {
tag: 'payRequest',
callback: `${this.settings.serviceUrl}/api/guest/lnurl_pay/handle?k1=${payInfoK1}`,
maxSendable: remote * 1000,
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> {
const key = await this.storage.paymentStorage.UseUserEphemeralKey(payK1, 'pay', true)
parseTags(tag: string, tags: string[][], opts: { multiples?: boolean, required?: boolean } = {}): string[] {
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
if (!Number.isInteger(sats)) {
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, {
amountSats: sats,
memo: defaultLnurlPayMetadata
}, { expiry: defaultInvoiceExpiry, linkedApplication: key.linkedApplication })
memo: zapInfo ? zapInfo.description : defaultLnurlPayMetadata
}, { expiry: defaultInvoiceExpiry, linkedApplication: key.linkedApplication, zapInfo })
return {
pr: invoice.invoice,
routes: []

View file

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

View file

@ -1,6 +1,6 @@
import { ChildProcess, fork } from 'child_process'
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
export const LoadNosrtSettingsFromEnv = (test = false) => {
return {
@ -31,8 +31,8 @@ export default class NostrSubprocess {
this.childProcess.send(message)
}
Send(appId: string, pub: string, message: string) {
this.sendToChildProcess({ type: 'send', pub, message, appId })
Send(appId: string, data: SendData, relays?: string[]) {
this.sendToChildProcess({ type: 'send', data, appId, relays })
}
Stop() {
this.childProcess.kill()

View file

@ -1,4 +1,5 @@
import * as Types from '../../../proto/autogenerated/ts/types.js'
import { getLogger } from '../helpers/logger.js'
import Main from '../main/index.js'
export default (mainHandler: Main): Types.ServerMethods => {
return {
@ -70,13 +71,7 @@ export default (mainHandler: Main): Types.ServerMethods => {
return mainHandler.paymentManager.GetLnurlPayInfo(ctx.k1)
},
HandleLnurlPay: async (ctx) => {
if (!ctx.k1 || !ctx.amount) {
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)
return mainHandler.paymentManager.HandleLnurlPay(ctx)
},
AddProduct: async (ctx, 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)
},
GetLiveUserOperations: async (ctx, cb) => {
mainHandler.SubToPayment(ctx, cb)
}
}
}

View file

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

View file

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

View file

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

View file

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

View file

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