code in pllace
This commit is contained in:
parent
8b25f05e6c
commit
ee5f3c2743
7 changed files with 114 additions and 53 deletions
|
|
@ -22,8 +22,14 @@ export type OfferPointer = {
|
||||||
pubkey: string,
|
pubkey: string,
|
||||||
relays?: string[],
|
relays?: string[],
|
||||||
offer: string
|
offer: string
|
||||||
|
priceType: 'fixed' | 'spontaneous' | 'variable',
|
||||||
|
price?: number
|
||||||
|
}
|
||||||
|
enum PriceType {
|
||||||
|
fixed = 0,
|
||||||
|
variable = 1,
|
||||||
|
spontaneous = 2,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
type TLV = { [t: number]: Uint8Array[] }
|
type TLV = { [t: number]: Uint8Array[] }
|
||||||
|
|
@ -67,11 +73,17 @@ export const encodeNoffer = (offer: OfferPointer): string => {
|
||||||
const settings = LoadNosrtSettingsFromEnv()
|
const settings = LoadNosrtSettingsFromEnv()
|
||||||
relays = settings.relays
|
relays = settings.relays
|
||||||
}
|
}
|
||||||
const data = encodeTLV({
|
const typeAsNum = Number(PriceType[offer.priceType])
|
||||||
|
const o: TLV = {
|
||||||
0: [hexToBytes(offer.pubkey)],
|
0: [hexToBytes(offer.pubkey)],
|
||||||
1: (relays).map(url => utf8Encoder.encode(url)),
|
1: (relays).map(url => utf8Encoder.encode(url)),
|
||||||
2: [utf8Encoder.encode(offer.offer)]
|
2: [utf8Encoder.encode(offer.offer)],
|
||||||
});
|
3: [new Uint8Array([typeAsNum])],
|
||||||
|
}
|
||||||
|
if (offer.price) {
|
||||||
|
o[4] = [new Uint8Array(new BigUint64Array([BigInt(offer.price)]).buffer)]
|
||||||
|
}
|
||||||
|
const data = encodeTLV(o);
|
||||||
const words = bech32.toWords(data)
|
const words = bech32.toWords(data)
|
||||||
return bech32.encode("noffer", words, 5000);
|
return bech32.encode("noffer", words, 5000);
|
||||||
}
|
}
|
||||||
|
|
@ -91,6 +103,24 @@ const parseTLV = (data: Uint8Array): TLV => {
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const decodeNoffer = (noffer: string): OfferPointer => {
|
||||||
|
const { prefix, words } = bech32.decode(noffer, 5000)
|
||||||
|
if (prefix !== "noffer") {
|
||||||
|
throw new Error("Expected nprofile prefix");
|
||||||
|
}
|
||||||
|
const data = new Uint8Array(bech32.fromWords(words))
|
||||||
|
|
||||||
|
const tlv = parseTLV(data);
|
||||||
|
if (!tlv[0]?.[0]) throw new Error('missing TLV 0 for noffer')
|
||||||
|
if (tlv[0][0].length !== 32) throw new Error('TLV 0 should be 32 bytes')
|
||||||
|
|
||||||
|
return {
|
||||||
|
pubkey: bytesToHex(tlv[0][0]),
|
||||||
|
relays: tlv[1] ? tlv[1].map(d => utf8Decoder.decode(d)) : [],
|
||||||
|
bridge: tlv[2] ? tlv[2].map(d => utf8Decoder.decode(d)) : []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export const decodeNprofile = (nprofile: string): CustomProfilePointer => {
|
export const decodeNprofile = (nprofile: string): CustomProfilePointer => {
|
||||||
const { prefix, words } = bech32.decode(nprofile, 5000)
|
const { prefix, words } = bech32.decode(nprofile, 5000)
|
||||||
if (prefix !== "nprofile") {
|
if (prefix !== "nprofile") {
|
||||||
|
|
@ -107,4 +137,4 @@ export const decodeNprofile = (nprofile: string): CustomProfilePointer => {
|
||||||
relays: tlv[1] ? tlv[1].map(d => utf8Decoder.decode(d)) : [],
|
relays: tlv[1] ? tlv[1].map(d => utf8Decoder.decode(d)) : [],
|
||||||
bridge: tlv[2] ? tlv[2].map(d => utf8Decoder.decode(d)) : []
|
bridge: tlv[2] ? tlv[2].map(d => utf8Decoder.decode(d)) : []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ import { ERROR, getLogger } from "./services/helpers/logger.js";
|
||||||
import { UnsignedEvent } from "./services/nostr/tools/event.js";
|
import { UnsignedEvent } from "./services/nostr/tools/event.js";
|
||||||
import { defaultInvoiceExpiry } from "./services/storage/paymentStorage.js";
|
import { defaultInvoiceExpiry } from "./services/storage/paymentStorage.js";
|
||||||
import { Application } from "./services/storage/entity/Application.js";
|
import { Application } from "./services/storage/entity/Application.js";
|
||||||
|
type NofferData = { offer: string, amount?: number }
|
||||||
export default (serverMethods: Types.ServerMethods, mainHandler: Main, nostrSettings: NostrSettings, onClientEvent: (e: { requestId: string }, fromPub: string) => void): { Stop: () => void, Send: NostrSend } => {
|
export default (serverMethods: Types.ServerMethods, mainHandler: Main, nostrSettings: NostrSettings, onClientEvent: (e: { requestId: string }, fromPub: string) => void): { Stop: () => void, Send: NostrSend } => {
|
||||||
const log = getLogger({})
|
const log = getLogger({})
|
||||||
const nostrTransport = NewNostrTransport(serverMethods, {
|
const nostrTransport = NewNostrTransport(serverMethods, {
|
||||||
|
|
@ -49,9 +49,8 @@ export default (serverMethods: Types.ServerMethods, mainHandler: Main, nostrSett
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (event.kind === 21001) {
|
if (event.kind === 21001) {
|
||||||
const offerReq = j as { offer: string }
|
const offerReq = j as NofferData
|
||||||
handleNofferEvent(mainHandler, offerReq, event)
|
mainHandler.handleNip69Noffer(offerReq, event)
|
||||||
.then(e => nostr.Send({ type: 'app', appId: event.appId }, { type: 'event', event: e, encrypt: { toPub: event.pub } }))
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (!j.rpcName) {
|
if (!j.rpcName) {
|
||||||
|
|
@ -69,40 +68,4 @@ export default (serverMethods: Types.ServerMethods, mainHandler: Main, nostrSett
|
||||||
return { Stop: () => nostr.Stop, Send: (...args) => nostr.Send(...args) }
|
return { Stop: () => nostr.Stop, Send: (...args) => nostr.Send(...args) }
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: move this to paymentManager
|
|
||||||
const handleNofferEvent = async (mainHandler: Main, offerReq: { offer: string }, event: NostrEvent): Promise<UnsignedEvent> => {
|
|
||||||
const app = await mainHandler.storage.applicationStorage.GetApplication(event.appId)
|
|
||||||
try {
|
|
||||||
const offer = offerReq.offer
|
|
||||||
let invoice: string
|
|
||||||
const split = offer.split(':')
|
|
||||||
if (split.length === 1) {
|
|
||||||
const user = await mainHandler.storage.applicationStorage.GetApplicationUser(app, split[0])
|
|
||||||
//TODO: add prop def for amount
|
|
||||||
const userInvoice = await mainHandler.paymentManager.NewInvoice(user.user.user_id, { amountSats: 1000, memo: "free offer" }, { expiry: defaultInvoiceExpiry, linkedApplication: app })
|
|
||||||
invoice = userInvoice.invoice
|
|
||||||
} else if (split[0] === 'p') {
|
|
||||||
const product = await mainHandler.productManager.NewProductInvoice(split[1])
|
|
||||||
invoice = product.invoice
|
|
||||||
} else {
|
|
||||||
return newNofferResponse(JSON.stringify({ code: 1, message: 'Invalid Offer' }), app, event)
|
|
||||||
}
|
|
||||||
return newNofferResponse(JSON.stringify({ bolt11: invoice }), app, event)
|
|
||||||
} catch (e: any) {
|
|
||||||
getLogger({ component: "noffer" })(ERROR, e.message || e)
|
|
||||||
return newNofferResponse(JSON.stringify({ code: 1, message: 'Invalid Offer' }), app, event)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const newNofferResponse = (content: string, app: Application, event: NostrEvent): UnsignedEvent => {
|
|
||||||
return {
|
|
||||||
content,
|
|
||||||
created_at: Math.floor(Date.now() / 1000),
|
|
||||||
kind: 21001,
|
|
||||||
pubkey: app.nostr_public_key!,
|
|
||||||
tags: [
|
|
||||||
['p', event.pub],
|
|
||||||
['e', event.id],
|
|
||||||
],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -161,7 +161,7 @@ export default class {
|
||||||
service_fee_bps: this.settings.outgoingAppUserInvoiceFeeBps
|
service_fee_bps: this.settings.outgoingAppUserInvoiceFeeBps
|
||||||
|
|
||||||
},
|
},
|
||||||
noffer: encodeNoffer({ pubkey: app.nostr_public_key!, offer: u.identifier }),
|
noffer: encodeNoffer({ pubkey: app.nostr_public_key!, offer: u.identifier, priceType: 'spontaneous' }),
|
||||||
max_withdrawable: this.paymentManager.GetMaxPayableInvoice(u.user.balance_sats, true)
|
max_withdrawable: this.paymentManager.GetMaxPayableInvoice(u.user.balance_sats, true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -199,7 +199,7 @@ export default class {
|
||||||
network_max_fee_bps: this.settings.lndSettings.feeRateBps,
|
network_max_fee_bps: this.settings.lndSettings.feeRateBps,
|
||||||
network_max_fee_fixed: this.settings.lndSettings.feeFixedLimit,
|
network_max_fee_fixed: this.settings.lndSettings.feeFixedLimit,
|
||||||
service_fee_bps: this.settings.outgoingAppUserInvoiceFeeBps
|
service_fee_bps: this.settings.outgoingAppUserInvoiceFeeBps
|
||||||
}, noffer: encodeNoffer({ pubkey: app.nostr_public_key!, offer: user.identifier })
|
}, noffer: encodeNoffer({ pubkey: app.nostr_public_key!, offer: user.identifier, priceType: 'spontaneous' })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ import AppUserManager from "./appUserManager.js"
|
||||||
import { Application } from '../storage/entity/Application.js'
|
import { Application } from '../storage/entity/Application.js'
|
||||||
import { UserReceivingInvoice } from '../storage/entity/UserReceivingInvoice.js'
|
import { UserReceivingInvoice } from '../storage/entity/UserReceivingInvoice.js'
|
||||||
import { UnsignedEvent } from '../nostr/tools/event.js'
|
import { UnsignedEvent } from '../nostr/tools/event.js'
|
||||||
import { NostrSend } from '../nostr/handler.js'
|
import { NostrEvent, NostrSend } from '../nostr/handler.js'
|
||||||
import MetricsManager from '../metrics/index.js'
|
import MetricsManager from '../metrics/index.js'
|
||||||
import { LoggedEvent } from '../storage/eventsLog.js'
|
import { LoggedEvent } from '../storage/eventsLog.js'
|
||||||
import { LiquidityProvider } from "./liquidityProvider.js"
|
import { LiquidityProvider } from "./liquidityProvider.js"
|
||||||
|
|
@ -21,6 +21,7 @@ import { Utils } from "../helpers/utilsWrapper.js"
|
||||||
import { RugPullTracker } from "./rugPullTracker.js"
|
import { RugPullTracker } from "./rugPullTracker.js"
|
||||||
import { AdminManager } from "./adminManager.js"
|
import { AdminManager } from "./adminManager.js"
|
||||||
import { Unlocker } from "./unlocker.js"
|
import { Unlocker } from "./unlocker.js"
|
||||||
|
import { defaultInvoiceExpiry } from "../storage/paymentStorage.js"
|
||||||
|
|
||||||
type UserOperationsSub = {
|
type UserOperationsSub = {
|
||||||
id: string
|
id: string
|
||||||
|
|
@ -30,6 +31,7 @@ type UserOperationsSub = {
|
||||||
newOutgoingTx: (operation: Types.UserOperation) => void
|
newOutgoingTx: (operation: Types.UserOperation) => void
|
||||||
}
|
}
|
||||||
const appTag = "Lightning.Pub"
|
const appTag = "Lightning.Pub"
|
||||||
|
|
||||||
export default class {
|
export default class {
|
||||||
storage: Storage
|
storage: Storage
|
||||||
lnd: LND
|
lnd: LND
|
||||||
|
|
@ -46,9 +48,9 @@ export default class {
|
||||||
liquidityProvider: LiquidityProvider
|
liquidityProvider: LiquidityProvider
|
||||||
utils: Utils
|
utils: Utils
|
||||||
rugPullTracker: RugPullTracker
|
rugPullTracker: RugPullTracker
|
||||||
unlocker:Unlocker
|
unlocker: Unlocker
|
||||||
nostrSend: NostrSend = () => { getLogger({})("nostr send not initialized yet") }
|
nostrSend: NostrSend = () => { getLogger({})("nostr send not initialized yet") }
|
||||||
constructor(settings: MainSettings, storage: Storage, adminManager: AdminManager, utils: Utils,unlocker:Unlocker) {
|
constructor(settings: MainSettings, storage: Storage, adminManager: AdminManager, utils: Utils, unlocker: Unlocker) {
|
||||||
this.settings = settings
|
this.settings = settings
|
||||||
this.storage = storage
|
this.storage = storage
|
||||||
this.utils = utils
|
this.utils = utils
|
||||||
|
|
@ -272,4 +274,67 @@ export default class {
|
||||||
log({ unsigned: event })
|
log({ unsigned: event })
|
||||||
this.nostrSend({ type: 'app', appId: invoice.linkedApplication.app_id }, { type: 'event', event })
|
this.nostrSend({ type: 'app', appId: invoice.linkedApplication.app_id }, { type: 'event', event })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getNofferInvoice(offerReq: { offer: string, amount?: number }, appId: string): Promise<{ success: true, invoice: string } | { success: false, code: number }> {
|
||||||
|
try {
|
||||||
|
const { remote } = await this.lnd.ChannelBalance()
|
||||||
|
const { offer, amount } = offerReq
|
||||||
|
const split = offer.split(':')
|
||||||
|
if (split.length === 1) {
|
||||||
|
if (!amount || isNaN(amount) || amount < 10 || amount > remote) {
|
||||||
|
return { success: false, code: 5 }
|
||||||
|
}
|
||||||
|
const res = await this.applicationManager.AddAppUserInvoice(appId, {
|
||||||
|
http_callback_url: "", payer_identifier: split[0], receiver_identifier: split[0],
|
||||||
|
invoice_req: { amountSats: amount, memo: "free offer" }
|
||||||
|
})
|
||||||
|
return { success: true, invoice: res.invoice }
|
||||||
|
} else if (split[0] === 'p') {
|
||||||
|
const product = await this.productManager.NewProductInvoice(split[1])
|
||||||
|
return { success: true, invoice: product.invoice }
|
||||||
|
} else {
|
||||||
|
return { success: false, code: 1 }
|
||||||
|
}
|
||||||
|
} catch (e: any) {
|
||||||
|
getLogger({ component: "noffer" })(ERROR, e.message || e)
|
||||||
|
return { success: false, code: 1 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async handleNip69Noffer(offerReq: { offer: string }, event: NostrEvent) {
|
||||||
|
const offerInvoice = await this.getNofferInvoice(offerReq, event.appId)
|
||||||
|
if (!offerInvoice.success) {
|
||||||
|
const code = offerInvoice.code
|
||||||
|
const e = newNofferResponse(JSON.stringify({ code, message: codeToMessage(code) }), event)
|
||||||
|
this.nostrSend({ type: 'app', appId: event.appId }, { type: 'event', event: e, encrypt: { toPub: event.pub } })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const e = newNofferResponse(JSON.stringify({ bolt11: offerInvoice.invoice }), event)
|
||||||
|
this.nostrSend({ type: 'app', appId: event.appId }, { type: 'event', event: e, encrypt: { toPub: event.pub } })
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const codeToMessage = (code: number) => {
|
||||||
|
switch (code) {
|
||||||
|
case 1: return 'Invalid Offer'
|
||||||
|
case 2: return 'Temporary Failure'
|
||||||
|
case 3: return 'Expired Offer'
|
||||||
|
case 4: return 'Unsupported Feature'
|
||||||
|
case 5: return 'Invalid Amount'
|
||||||
|
default: throw new Error("unknown error code" + code)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const newNofferResponse = (content: string, event: NostrEvent): UnsignedEvent => {
|
||||||
|
return {
|
||||||
|
content,
|
||||||
|
created_at: Math.floor(Date.now() / 1000),
|
||||||
|
kind: 21001,
|
||||||
|
pubkey: "",
|
||||||
|
tags: [
|
||||||
|
['p', event.pub],
|
||||||
|
['e', event.id],
|
||||||
|
],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -135,7 +135,7 @@ export default class {
|
||||||
//})
|
//})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
console.log({p})
|
console.log({ p })
|
||||||
const paymentRes = await this.lnd.GetPayment(p.paymentIndex)
|
const paymentRes = await this.lnd.GetPayment(p.paymentIndex)
|
||||||
const payment = paymentRes.payments[0]
|
const payment = paymentRes.payments[0]
|
||||||
if (!payment || Number(payment.paymentIndex) !== p.paymentIndex) {
|
if (!payment || Number(payment.paymentIndex) !== p.paymentIndex) {
|
||||||
|
|
@ -337,7 +337,7 @@ export default class {
|
||||||
const pendingPayment = await this.storage.txQueue.PushToQueue({
|
const pendingPayment = await this.storage.txQueue.PushToQueue({
|
||||||
dbTx: true, description: "payment started", exec: async tx => {
|
dbTx: true, description: "payment started", exec: async tx => {
|
||||||
await this.storage.userStorage.DecrementUserBalance(userId, totalAmountToDecrement + routingFeeLimit, invoice, tx)
|
await this.storage.userStorage.DecrementUserBalance(userId, totalAmountToDecrement + routingFeeLimit, invoice, tx)
|
||||||
return await this.storage.paymentStorage.AddPendingExternalPayment(userId, invoice, { payAmount, serviceFee, networkFee: routingFeeLimit }, linkedApplication, provider,tx)
|
return await this.storage.paymentStorage.AddPendingExternalPayment(userId, invoice, { payAmount, serviceFee, networkFee: routingFeeLimit }, linkedApplication, provider, tx)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
this.log("ready to pay")
|
this.log("ready to pay")
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,7 @@ export default class {
|
||||||
id: newProduct.product_id,
|
id: newProduct.product_id,
|
||||||
name: newProduct.name,
|
name: newProduct.name,
|
||||||
price_sats: newProduct.price_sats,
|
price_sats: newProduct.price_sats,
|
||||||
noffer: encodeNoffer({ pubkey: user.user_id, offer: offer })
|
noffer: encodeNoffer({ pubkey: user.user_id, offer: offer, priceType: 'fixed', price: newProduct.price_sats })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -189,6 +189,9 @@ export default class Handler {
|
||||||
const content = await encryptData(data.event.content, getSharedSecret(keys.privateKey, data.encrypt.toPub))
|
const content = await encryptData(data.event.content, getSharedSecret(keys.privateKey, data.encrypt.toPub))
|
||||||
toSign.content = encodePayload(content)
|
toSign.content = encodePayload(content)
|
||||||
}
|
}
|
||||||
|
if (!toSign.pubkey) {
|
||||||
|
toSign.pubkey = keys.publicKey
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const signed = finishEvent(toSign, keys.privateKey)
|
const signed = finishEvent(toSign, keys.privateKey)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue