nip69 server
This commit is contained in:
parent
8e18e67995
commit
747bce3cf1
9 changed files with 130 additions and 37 deletions
|
|
@ -652,6 +652,7 @@ The nostr server will send back a message response, and inside the body there wi
|
||||||
- __identifier__: _string_
|
- __identifier__: _string_
|
||||||
- __info__: _[UserInfo](#UserInfo)_
|
- __info__: _[UserInfo](#UserInfo)_
|
||||||
- __max_withdrawable__: _number_
|
- __max_withdrawable__: _number_
|
||||||
|
- __noffer__: _string_
|
||||||
|
|
||||||
### Application
|
### Application
|
||||||
- __balance__: _number_
|
- __balance__: _number_
|
||||||
|
|
@ -897,6 +898,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
|
||||||
|
|
|
||||||
|
|
@ -510,6 +510,7 @@ export type AppUser = {
|
||||||
identifier: string
|
identifier: string
|
||||||
info: UserInfo
|
info: UserInfo
|
||||||
max_withdrawable: number
|
max_withdrawable: number
|
||||||
|
noffer: string
|
||||||
}
|
}
|
||||||
export const AppUserOptionalFields: [] = []
|
export const AppUserOptionalFields: [] = []
|
||||||
export type AppUserOptions = OptionsBaseMessage & {
|
export type AppUserOptions = OptionsBaseMessage & {
|
||||||
|
|
@ -517,6 +518,7 @@ export type AppUserOptions = OptionsBaseMessage & {
|
||||||
identifier_CustomCheck?: (v: string) => boolean
|
identifier_CustomCheck?: (v: string) => boolean
|
||||||
info_Options?: UserInfoOptions
|
info_Options?: UserInfoOptions
|
||||||
max_withdrawable_CustomCheck?: (v: number) => boolean
|
max_withdrawable_CustomCheck?: (v: number) => boolean
|
||||||
|
noffer_CustomCheck?: (v: string) => boolean
|
||||||
}
|
}
|
||||||
export const AppUserValidate = (o?: AppUser, opts: AppUserOptions = {}, path: string = 'AppUser::root.'): Error | null => {
|
export const AppUserValidate = (o?: AppUser, opts: AppUserOptions = {}, path: string = 'AppUser::root.'): Error | null => {
|
||||||
if (opts.checkOptionalsAreSet && opts.allOptionalsAreSet) return new Error(path + ': only one of checkOptionalsAreSet or allOptionalNonDefault can be set for each message')
|
if (opts.checkOptionalsAreSet && opts.allOptionalsAreSet) return new Error(path + ': only one of checkOptionalsAreSet or allOptionalNonDefault can be set for each message')
|
||||||
|
|
@ -532,6 +534,9 @@ export const AppUserValidate = (o?: AppUser, opts: AppUserOptions = {}, path: st
|
||||||
if (typeof o.max_withdrawable !== 'number') return new Error(`${path}.max_withdrawable: is not a number`)
|
if (typeof o.max_withdrawable !== 'number') return new Error(`${path}.max_withdrawable: is not a number`)
|
||||||
if (opts.max_withdrawable_CustomCheck && !opts.max_withdrawable_CustomCheck(o.max_withdrawable)) return new Error(`${path}.max_withdrawable: custom check failed`)
|
if (opts.max_withdrawable_CustomCheck && !opts.max_withdrawable_CustomCheck(o.max_withdrawable)) return new Error(`${path}.max_withdrawable: 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`)
|
||||||
|
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1977,6 +1982,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 +1990,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 +2003,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`)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -192,6 +192,7 @@ message AppUser {
|
||||||
string identifier = 1;
|
string identifier = 1;
|
||||||
UserInfo info = 2;
|
UserInfo info = 2;
|
||||||
int64 max_withdrawable = 3;
|
int64 max_withdrawable = 3;
|
||||||
|
string noffer = 4;
|
||||||
}
|
}
|
||||||
|
|
||||||
message AddAppInvoiceRequest {
|
message AddAppInvoiceRequest {
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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,7 +15,13 @@ 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,
|
||||||
|
relays?: string[],
|
||||||
|
offer: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -23,15 +30,15 @@ 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,16 +48,31 @@ 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const encodeNoffer = (offer: OfferPointer): string => {
|
||||||
|
let relays = offer.relays
|
||||||
|
if (!relays) {
|
||||||
|
const settings = LoadNosrtSettingsFromEnv()
|
||||||
|
relays = settings.relays
|
||||||
|
}
|
||||||
|
const data = encodeTLV({
|
||||||
|
0: [hexToBytes(offer.pubkey)],
|
||||||
|
1: (relays).map(url => utf8Encoder.encode(url)),
|
||||||
|
2: [hexToBytes(offer.offer)]
|
||||||
|
});
|
||||||
|
const words = bech32.toWords(data)
|
||||||
return bech32.encode("nprofile", words, 5000);
|
return bech32.encode("nprofile", words, 5000);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -70,19 +92,19 @@ const parseTLV = (data: Uint8Array): TLV => {
|
||||||
}
|
}
|
||||||
|
|
||||||
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") {
|
||||||
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 nprofile')
|
||||||
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')
|
||||||
|
|
||||||
return {
|
return {
|
||||||
pubkey: bytesToHex(tlv[0][0]),
|
pubkey: bytesToHex(tlv[0][0]),
|
||||||
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)) : []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,12 @@
|
||||||
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 { 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,12 @@ 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 { offer: string }
|
||||||
|
handleNofferEvent(mainHandler, offerReq, event)
|
||||||
|
.then(e => nostr.Send({ type: 'app', appId: event.appId }, { type: 'event', event: e, encrypt: { toPub: event.pub } }))
|
||||||
|
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 +68,41 @@ 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],
|
||||||
|
],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 } 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
|
||||||
|
|
||||||
|
|
@ -160,6 +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 }),
|
||||||
max_withdrawable: this.paymentManager.GetMaxPayableInvoice(u.user.balance_sats, true)
|
max_withdrawable: this.paymentManager.GetMaxPayableInvoice(u.user.balance_sats, true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -197,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 })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 } 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 })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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,10 @@ 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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const signed = finishEvent(toSign, keys.privateKey)
|
const signed = finishEvent(toSign, keys.privateKey)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue