Merge pull request #727 from shocknet/noffer

nip69 server
This commit is contained in:
Justin (shocknet) 2024-09-07 18:15:55 -04:00 committed by GitHub
commit 9b278cff31
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 206 additions and 46 deletions

View file

@ -897,6 +897,7 @@ The nostr server will send back a message response, and inside the body there wi
### Product ### Product
- __id__: _string_ - __id__: _string_
- __name__: _string_ - __name__: _string_
- __noffer__: _string_
- __price_sats__: _number_ - __price_sats__: _number_
### RelaysMigration ### RelaysMigration
@ -964,6 +965,7 @@ The nostr server will send back a message response, and inside the body there wi
- __max_withdrawable__: _number_ - __max_withdrawable__: _number_
- __network_max_fee_bps__: _number_ - __network_max_fee_bps__: _number_
- __network_max_fee_fixed__: _number_ - __network_max_fee_fixed__: _number_
- __noffer__: _string_
- __service_fee_bps__: _number_ - __service_fee_bps__: _number_
- __userId__: _string_ - __userId__: _string_
- __user_identifier__: _string_ - __user_identifier__: _string_

View file

@ -1977,6 +1977,7 @@ export const PaymentStateValidate = (o?: PaymentState, opts: PaymentStateOptions
export type Product = { export type Product = {
id: string id: string
name: string name: string
noffer: string
price_sats: number price_sats: number
} }
export const ProductOptionalFields: [] = [] export const ProductOptionalFields: [] = []
@ -1984,6 +1985,7 @@ export type ProductOptions = OptionsBaseMessage & {
checkOptionalsAreSet?: [] checkOptionalsAreSet?: []
id_CustomCheck?: (v: string) => boolean id_CustomCheck?: (v: string) => boolean
name_CustomCheck?: (v: string) => boolean name_CustomCheck?: (v: string) => boolean
noffer_CustomCheck?: (v: string) => boolean
price_sats_CustomCheck?: (v: number) => boolean price_sats_CustomCheck?: (v: number) => boolean
} }
export const ProductValidate = (o?: Product, opts: ProductOptions = {}, path: string = 'Product::root.'): Error | null => { export const ProductValidate = (o?: Product, opts: ProductOptions = {}, path: string = 'Product::root.'): Error | null => {
@ -1996,6 +1998,9 @@ export const ProductValidate = (o?: Product, opts: ProductOptions = {}, path: st
if (typeof o.name !== 'string') return new Error(`${path}.name: is not a string`) if (typeof o.name !== 'string') return new Error(`${path}.name: is not a string`)
if (opts.name_CustomCheck && !opts.name_CustomCheck(o.name)) return new Error(`${path}.name: custom check failed`) if (opts.name_CustomCheck && !opts.name_CustomCheck(o.name)) return new Error(`${path}.name: custom check failed`)
if (typeof o.noffer !== 'string') return new Error(`${path}.noffer: is not a string`)
if (opts.noffer_CustomCheck && !opts.noffer_CustomCheck(o.noffer)) return new Error(`${path}.noffer: custom check failed`)
if (typeof o.price_sats !== 'number') return new Error(`${path}.price_sats: is not a number`) if (typeof o.price_sats !== 'number') return new Error(`${path}.price_sats: is not a number`)
if (opts.price_sats_CustomCheck && !opts.price_sats_CustomCheck(o.price_sats)) return new Error(`${path}.price_sats: custom check failed`) if (opts.price_sats_CustomCheck && !opts.price_sats_CustomCheck(o.price_sats)) return new Error(`${path}.price_sats: custom check failed`)
@ -2351,6 +2356,7 @@ export type UserInfo = {
max_withdrawable: number max_withdrawable: number
network_max_fee_bps: number network_max_fee_bps: number
network_max_fee_fixed: number network_max_fee_fixed: number
noffer: string
service_fee_bps: number service_fee_bps: number
userId: string userId: string
user_identifier: string user_identifier: string
@ -2362,6 +2368,7 @@ export type UserInfoOptions = OptionsBaseMessage & {
max_withdrawable_CustomCheck?: (v: number) => boolean max_withdrawable_CustomCheck?: (v: number) => boolean
network_max_fee_bps_CustomCheck?: (v: number) => boolean network_max_fee_bps_CustomCheck?: (v: number) => boolean
network_max_fee_fixed_CustomCheck?: (v: number) => boolean network_max_fee_fixed_CustomCheck?: (v: number) => boolean
noffer_CustomCheck?: (v: string) => boolean
service_fee_bps_CustomCheck?: (v: number) => boolean service_fee_bps_CustomCheck?: (v: number) => boolean
userId_CustomCheck?: (v: string) => boolean userId_CustomCheck?: (v: string) => boolean
user_identifier_CustomCheck?: (v: string) => boolean user_identifier_CustomCheck?: (v: string) => boolean
@ -2382,6 +2389,9 @@ export const UserInfoValidate = (o?: UserInfo, opts: UserInfoOptions = {}, path:
if (typeof o.network_max_fee_fixed !== 'number') return new Error(`${path}.network_max_fee_fixed: is not a number`) if (typeof o.network_max_fee_fixed !== 'number') return new Error(`${path}.network_max_fee_fixed: is not a number`)
if (opts.network_max_fee_fixed_CustomCheck && !opts.network_max_fee_fixed_CustomCheck(o.network_max_fee_fixed)) return new Error(`${path}.network_max_fee_fixed: custom check failed`) if (opts.network_max_fee_fixed_CustomCheck && !opts.network_max_fee_fixed_CustomCheck(o.network_max_fee_fixed)) return new Error(`${path}.network_max_fee_fixed: custom check failed`)
if (typeof o.noffer !== 'string') return new Error(`${path}.noffer: is not a string`)
if (opts.noffer_CustomCheck && !opts.noffer_CustomCheck(o.noffer)) return new Error(`${path}.noffer: custom check failed`)
if (typeof o.service_fee_bps !== 'number') return new Error(`${path}.service_fee_bps: is not a number`) if (typeof o.service_fee_bps !== 'number') return new Error(`${path}.service_fee_bps: is not a number`)
if (opts.service_fee_bps_CustomCheck && !opts.service_fee_bps_CustomCheck(o.service_fee_bps)) return new Error(`${path}.service_fee_bps: custom check failed`) if (opts.service_fee_bps_CustomCheck && !opts.service_fee_bps_CustomCheck(o.service_fee_bps)) return new Error(`${path}.service_fee_bps: custom check failed`)

View file

@ -353,6 +353,7 @@ message UserInfo{
int64 service_fee_bps = 5; int64 service_fee_bps = 5;
int64 network_max_fee_bps = 6; int64 network_max_fee_bps = 6;
int64 network_max_fee_fixed = 7; int64 network_max_fee_fixed = 7;
string noffer = 8;
} }
message GetUserOperationsRequest{ message GetUserOperationsRequest{
@ -409,6 +410,7 @@ message Product {
string id = 1; string id = 1;
string name = 2; string name = 2;
int64 price_sats = 3; int64 price_sats = 3;
string noffer = 4;
} }
message GetProductBuyLinkResponse { message GetProductBuyLinkResponse {

View file

@ -1,11 +1,12 @@
/* /*
This file contains functions that deal with encoding and decoding nprofiles, This file contains functions that deal with encoding and decoding nprofiles,
but with he addition of bridge urls in the nprofile. but with he addition of bridge urls in the nprofile.
These functions are basically the same functions from nostr-tools package These functions are basically the same functions from nostr-tools package
but with some tweaks to allow for the bridge inclusion. but with some tweaks to allow for the bridge inclusion.
*/ */
import { bytesToHex, concatBytes, hexToBytes } from '@noble/hashes/utils'; import { bytesToHex, concatBytes, hexToBytes } from '@noble/hashes/utils';
import { bech32 } from 'bech32'; import { bech32 } from 'bech32';
import { LoadNosrtSettingsFromEnv } from './services/nostr/index.js';
export const utf8Decoder = new TextDecoder('utf-8') export const utf8Decoder = new TextDecoder('utf-8')
export const utf8Encoder = new TextEncoder() export const utf8Encoder = new TextEncoder()
@ -14,24 +15,36 @@ export const utf8Encoder = new TextEncoder()
export type CustomProfilePointer = { export type CustomProfilePointer = {
pubkey: string pubkey: string
relays?: string[] relays?: string[]
bridge?: string[] // one bridge bridge?: string[] // one bridge
} }
export type OfferPointer = {
pubkey: string,
relay: string,
offer: string
priceType: PriceType,
price?: number
}
export enum PriceType {
fixed = 0,
variable = 1,
spontaneous = 2,
}
type TLV = { [t: number]: Uint8Array[] } type TLV = { [t: number]: Uint8Array[] }
const encodeTLV = (tlv: TLV): Uint8Array => { const encodeTLV = (tlv: TLV): Uint8Array => {
const entries: Uint8Array[] = [] const entries: Uint8Array[] = []
Object.entries(tlv) Object.entries(tlv)
/* /*
the original function does a reverse() here, the original function does a reverse() here,
but here it causes the nprofile string to be different, but here it causes the nprofile string to be different,
even though it would still decode to the correct original inputs even though it would still decode to the correct original inputs
*/ */
//.reverse() //.reverse()
.forEach(([t, vs]) => { .forEach(([t, vs]) => {
vs.forEach(v => { vs.forEach(v => {
const entry = new Uint8Array(v.length + 2) const entry = new Uint8Array(v.length + 2)
@ -41,19 +54,39 @@ const encodeTLV = (tlv: TLV): Uint8Array => {
entries.push(entry) entries.push(entry)
}) })
}) })
return concatBytes(...entries); return concatBytes(...entries);
} }
export const encodeNprofile = (profile: CustomProfilePointer): string => { export const encodeNprofile = (profile: CustomProfilePointer): string => {
const data = encodeTLV({ const data = encodeTLV({
0: [hexToBytes(profile.pubkey)], 0: [hexToBytes(profile.pubkey)],
1: (profile.relays || []).map(url => utf8Encoder.encode(url)), 1: (profile.relays || []).map(url => utf8Encoder.encode(url)),
2: (profile.bridge || []).map(url => utf8Encoder.encode(url)) 2: (profile.bridge || []).map(url => utf8Encoder.encode(url))
}); });
const words = bech32.toWords(data) const words = bech32.toWords(data)
return bech32.encode("nprofile", words, 5000); return bech32.encode("nprofile", words, 5000);
} }
export const encodeNoffer = (offer: OfferPointer): string => {
let relay = offer.relay
if (!relay) {
const settings = LoadNosrtSettingsFromEnv()
relay = settings.relays[0]
}
const o: TLV = {
0: [hexToBytes(offer.pubkey)],
1: [utf8Encoder.encode(relay)],
2: [utf8Encoder.encode(offer.offer)],
3: [new Uint8Array([Number(offer.priceType)])],
}
if (offer.price) {
o[4] = [new Uint8Array(new BigUint64Array([BigInt(offer.price)]).buffer)]
}
const data = encodeTLV(o);
const words = bech32.toWords(data)
return bech32.encode("noffer", words, 5000);
}
const parseTLV = (data: Uint8Array): TLV => { const parseTLV = (data: Uint8Array): TLV => {
const result: TLV = {} const result: TLV = {}
let rest = data let rest = data
@ -69,20 +102,42 @@ const parseTLV = (data: Uint8Array): TLV => {
return result return result
} }
export const decodeNprofile = (nprofile: string): CustomProfilePointer => { export const decodeNoffer = (noffer: string): OfferPointer => {
const { prefix, words } = bech32.decode(nprofile, 5000) const { prefix, words } = bech32.decode(noffer, 5000)
if (prefix !== "nprofile") { if (prefix !== "noffer") {
throw new Error ("Expected nprofile prefix"); throw new Error("Expected nprofile prefix");
} }
const data = new Uint8Array(bech32.fromWords(words)) const data = new Uint8Array(bech32.fromWords(words))
const tlv = parseTLV(data); const tlv = parseTLV(data);
if (!tlv[0]?.[0]) throw new Error('missing TLV 0 for nprofile') 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') if (tlv[0][0].length !== 32) throw new Error('TLV 0 should be 32 bytes')
if (!tlv[1]?.[0]) throw new Error('missing TLV 1 for noffer')
if (!tlv[2]?.[0]) throw new Error('missing TLV 2 for noffer')
if (!tlv[3]?.[0]) throw new Error('missing TLV 3 for noffer')
return {
pubkey: bytesToHex(tlv[0][0]),
relay: utf8Decoder.decode(tlv[1][0]),
offer: utf8Decoder.decode(tlv[2][0]),
priceType: tlv[3][0][0],
price: tlv[4] ? Number(new BigUint64Array(tlv[4][0])[0]) : undefined
}
}
return { export const decodeNprofile = (nprofile: string): CustomProfilePointer => {
pubkey: bytesToHex(tlv[0][0]), const { prefix, words } = bech32.decode(nprofile, 5000)
relays: tlv[1] ? tlv[1].map(d => utf8Decoder.decode(d)) : [], if (prefix !== "nprofile") {
bridge: tlv[2] ? tlv[2].map(d => utf8Decoder.decode(d)): [] 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 nprofile')
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)) : []
}
}

View file

@ -17,7 +17,7 @@ const start = async () => {
log("manual process ended") log("manual process ended")
return return
} }
const { apps, mainHandler, liquidityProviderInfo, wizard, adminManager } = keepOn const { apps, mainHandler, liquidityProviderInfo, wizard, adminManager } = keepOn
const serverMethods = GetServerMethods(mainHandler) const serverMethods = GetServerMethods(mainHandler)
const nostrSettings = LoadNosrtSettingsFromEnv() const nostrSettings = LoadNosrtSettingsFromEnv()

View file

@ -1,9 +1,12 @@
import Main from "./services/main/index.js" import Main, { NofferData } from "./services/main/index.js"
import Nostr from "./services/nostr/index.js" import Nostr from "./services/nostr/index.js"
import { NostrSend, NostrSettings } from "./services/nostr/handler.js" import { NostrEvent, 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';
import { ERROR, getLogger } from "./services/helpers/logger.js"; import { ERROR, getLogger } from "./services/helpers/logger.js";
import { UnsignedEvent } from "./services/nostr/tools/event.js";
import { defaultInvoiceExpiry } from "./services/storage/paymentStorage.js";
import { Application } from "./services/storage/entity/Application.js";
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({})
@ -45,6 +48,11 @@ export default (serverMethods: Types.ServerMethods, mainHandler: Main, nostrSett
log(ERROR, "invalid json event received", event.content) log(ERROR, "invalid json event received", event.content)
return return
} }
if (event.kind === 21001) {
const offerReq = j as NofferData
mainHandler.handleNip69Noffer(offerReq, event)
return
}
if (!j.rpcName) { if (!j.rpcName) {
onClientEvent(j as { requestId: string }, event.pub) onClientEvent(j as { requestId: string }, event.pub)
return return
@ -59,3 +67,5 @@ 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) }
} }

View file

@ -4,6 +4,7 @@ import * as Types from '../../../proto/autogenerated/ts/types.js'
import { MainSettings } from './settings.js' import { MainSettings } from './settings.js'
import ApplicationManager from './applicationManager.js' import ApplicationManager from './applicationManager.js'
import { encodeNoffer, PriceType } from '../../custom-nip19.js'
export default class { export default class {
storage: Storage storage: Storage
settings: MainSettings settings: MainSettings
@ -59,7 +60,8 @@ export default class {
user_identifier: appUser.identifier, user_identifier: appUser.identifier,
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: appUser.identifier, priceType: PriceType.spontaneous, relay: "" })
} }
} }

View file

@ -8,6 +8,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 { encodeNoffer, PriceType } from '../../custom-nip19.js'
const TOKEN_EXPIRY_TIME = 2 * 60 * 1000 // 2 minutes, in milliseconds const TOKEN_EXPIRY_TIME = 2 * 60 * 1000 // 2 minutes, in milliseconds
@ -157,7 +158,8 @@ export default class {
user_identifier: u.identifier, user_identifier: u.identifier,
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: u.identifier, priceType: PriceType.spontaneous, relay: "" })
}, },
max_withdrawable: this.paymentManager.GetMaxPayableInvoice(u.user.balance_sats, true) max_withdrawable: this.paymentManager.GetMaxPayableInvoice(u.user.balance_sats, true)
@ -196,8 +198,9 @@ export default class {
user_identifier: user.identifier, user_identifier: user.identifier,
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, priceType: PriceType.spontaneous, relay: "" })
},
} }
} }

View file

@ -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 type NofferData = { offer: string, amount?: number }
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: NofferData, appId: string): Promise<{ success: true, invoice: string } | { success: false, code: number, max: 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, max: remote }
}
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, max: remote }
}
} catch (e: any) {
getLogger({ component: "noffer" })(ERROR, e.message || e)
return { success: false, code: 1, max: 0 }
}
}
async handleNip69Noffer(offerReq: NofferData, event: NostrEvent) {
const offerInvoice = await this.getNofferInvoice(offerReq, event.appId)
if (!offerInvoice.success) {
const code = offerInvoice.code
const e = newNofferResponse(JSON.stringify({ code, error: codeToMessage(code), range: { min: 10, max: offerInvoice.max } }), 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],
],
}
}

View file

@ -5,6 +5,7 @@ import * as Types from '../../../proto/autogenerated/ts/types.js'
import { MainSettings } from './settings.js' import { MainSettings } from './settings.js'
import PaymentManager from './paymentManager.js' import PaymentManager from './paymentManager.js'
import { defaultInvoiceExpiry } from '../storage/paymentStorage.js' import { defaultInvoiceExpiry } from '../storage/paymentStorage.js'
import { encodeNoffer, PriceType } from '../../custom-nip19.js'
export default class { export default class {
storage: Storage storage: Storage
@ -20,10 +21,12 @@ export default class {
async AddProduct(userId: string, req: Types.AddProductRequest): Promise<Types.Product> { async AddProduct(userId: string, req: Types.AddProductRequest): Promise<Types.Product> {
const user = await this.storage.userStorage.GetUser(userId) const user = await this.storage.userStorage.GetUser(userId)
const newProduct = await this.storage.productStorage.AddProduct(req.name, req.price_sats, user) const newProduct = await this.storage.productStorage.AddProduct(req.name, req.price_sats, user)
const offer = `p:${newProduct.product_id}`
return { return {
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, priceType: PriceType.fixed, price: newProduct.price_sats, relay: "" })
} }
} }

View file

@ -6,7 +6,7 @@ import { encodeNprofile } from '../../custom-nip19.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 }
type ClientInfo = { clientId: string, publicKey: string, privateKey: string, name: string } type ClientInfo = { clientId: string, publicKey: string, privateKey: string, name: string }
export type SendData = { type: "content", content: string, pub: string } | { type: "event", event: UnsignedEvent } export type SendData = { type: "content", content: string, pub: string } | { type: "event", event: UnsignedEvent, encrypt?: { toPub: string } }
export type SendInitiator = { type: 'app', appId: string } | { type: 'client', clientId: string } export type SendInitiator = { type: 'app', appId: string } | { type: 'client', clientId: string }
export type NostrSend = (initiator: SendInitiator, data: SendData, relays?: string[] | undefined) => void export type NostrSend = (initiator: SendInitiator, data: SendData, relays?: string[] | undefined) => void
@ -22,6 +22,7 @@ export type NostrEvent = {
appId: string appId: string
startAtNano: string startAtNano: string
startAtMs: number startAtMs: number
kind: number
} }
type SettingsRequest = { type SettingsRequest = {
@ -89,7 +90,7 @@ const sendToNostr: NostrSend = (initiator, data, relays) => {
subProcessHandler.Send(initiator, data, relays) subProcessHandler.Send(initiator, data, relays)
} }
send({ type: 'ready' }) send({ type: 'ready' })
const supportedKinds = [21000, 21001]
export default class Handler { export default class Handler {
pool = new SimplePool() pool = new SimplePool()
settings: NostrSettings settings: NostrSettings
@ -132,7 +133,7 @@ export default class Handler {
const sub = relay.sub([ const sub = relay.sub([
{ {
since: Math.ceil(Date.now() / 1000), since: Math.ceil(Date.now() / 1000),
kinds: [21000], kinds: supportedKinds,
'#p': Object.keys(this.apps), '#p': Object.keys(this.apps),
} }
]) ])
@ -140,7 +141,7 @@ export default class Handler {
log("up to date with nostr events") log("up to date with nostr events")
}) })
sub.on('event', async (e) => { sub.on('event', async (e) => {
if (e.kind !== 21000 || !e.pubkey) { if (!supportedKinds.includes(e.kind) || !e.pubkey) {
return return
} }
const pubTags = e.tags.find(tags => tags && tags.length > 1 && tags[0] === 'p') const pubTags = e.tags.find(tags => tags && tags.length > 1 && tags[0] === 'p')
@ -155,7 +156,7 @@ export default class Handler {
}) })
} }
async processEvent(e: Event<21000>, app: AppInfo) { async processEvent(e: Event, app: AppInfo) {
const eventId = e.id const eventId = e.id
if (handledEvents.includes(eventId)) { if (handledEvents.includes(eventId)) {
this.log("event already handled") this.log("event already handled")
@ -166,7 +167,7 @@ export default class Handler {
const startAtNano = process.hrtime.bigint().toString() const startAtNano = process.hrtime.bigint().toString()
const decoded = decodePayload(e.content) const decoded = decodePayload(e.content)
const content = await decryptData(decoded, getSharedSecret(app.privateKey, e.pubkey)) const content = await decryptData(decoded, getSharedSecret(app.privateKey, e.pubkey))
this.eventCallback({ id: eventId, content, pub: e.pubkey, appId: app.appId, startAtNano, startAtMs }) this.eventCallback({ id: eventId, content, pub: e.pubkey, appId: app.appId, startAtNano, startAtMs, kind: e.kind })
} }
async Send(initiator: SendInitiator, data: SendData, relays?: string[]) { async Send(initiator: SendInitiator, data: SendData, relays?: string[]) {
@ -184,6 +185,13 @@ export default class Handler {
} }
} else { } else {
toSign = data.event toSign = data.event
if (data.encrypt) {
const content = await encryptData(data.event.content, getSharedSecret(keys.privateKey, data.encrypt.toPub))
toSign.content = encodePayload(content)
}
if (!toSign.pubkey) {
toSign.pubkey = keys.publicKey
}
} }
const signed = finishEvent(toSign, keys.privateKey) const signed = finishEvent(toSign, keys.privateKey)