nostr as transport
This commit is contained in:
parent
1e423e4363
commit
b660b9d0a2
12 changed files with 237 additions and 56 deletions
|
|
@ -1,10 +1,24 @@
|
|||
import 'dotenv/config' // TODO - test env
|
||||
import { generatePrivateKey, getPublicKey } from 'nostr-tools';
|
||||
import NewServer from '../proto/autogenerated/ts/express_server.js'
|
||||
import NewClient from '../proto/autogenerated/ts/http_client.js'
|
||||
import serverOptions from './auth.js';
|
||||
import GetServerMethods from './services/serverMethods/index.js'
|
||||
import Main, { LoadMainSettingsFromEnv } from './services/main/index.js'
|
||||
import * as Types from '../proto/autogenerated/ts/types.js';
|
||||
import nostrMiddleware from './nostrMiddleware.js'
|
||||
import { LoadNosrtSettingsFromEnv } from './services/nostr/index.js';
|
||||
import NostrHandler from './services/nostr/index.js'
|
||||
|
||||
|
||||
const settings = LoadNosrtSettingsFromEnv(true)
|
||||
|
||||
const clientPrivateKey = generatePrivateKey()
|
||||
const clientPublicKey = getPublicKey(Buffer.from(clientPrivateKey, "hex"))
|
||||
|
||||
const serverPrivateKey = generatePrivateKey()
|
||||
const serverPublicKey = getPublicKey(Buffer.from(serverPrivateKey, "hex"))
|
||||
|
||||
const testPort = 4000
|
||||
var userAuthHeader = ""
|
||||
const client = NewClient({
|
||||
|
|
@ -16,9 +30,24 @@ const client = NewClient({
|
|||
encryptCallback: async (b) => b,
|
||||
deviceId: "device0"
|
||||
})
|
||||
const clientNostr = new NostrHandler({
|
||||
allowedPubs: [],
|
||||
privateKey: clientPrivateKey,
|
||||
publicKey: clientPublicKey,
|
||||
relays: settings.relays
|
||||
}, (event, getContent) => {
|
||||
console.log(getContent())
|
||||
})
|
||||
const mainHandler = new Main(LoadMainSettingsFromEnv(true)) // TODO - test env file
|
||||
const server = NewServer(GetServerMethods(mainHandler), { ...serverOptions(mainHandler), throwErrors: true })
|
||||
export const ignore = true
|
||||
const serverMethods = GetServerMethods(mainHandler)
|
||||
nostrMiddleware(serverMethods, mainHandler, {
|
||||
allowedPubs: [clientPublicKey],
|
||||
privateKey: serverPrivateKey,
|
||||
publicKey: serverPublicKey,
|
||||
relays: settings.relays
|
||||
})
|
||||
const server = NewServer(serverMethods, { ...serverOptions(mainHandler), throwErrors: true })
|
||||
export const ignore = false
|
||||
|
||||
export const setup = async () => {
|
||||
await mainHandler.storage.Connect()
|
||||
|
|
@ -48,4 +77,7 @@ export default async (d: (message: string, failure?: boolean) => void) => {
|
|||
|
||||
console.log(await client.NewAddress({ address_type: Types.AddressType.WITNESS_PUBKEY_HASH }))
|
||||
d("new address ok")
|
||||
await new Promise(res => setTimeout(res, 2000))
|
||||
clientNostr.Send(serverPublicKey, JSON.stringify({ requestId: "a", method: '/api/user/chain/new', body: { address_type: 'WITNESS_PUBKEY_HASH' } }))
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,13 +3,16 @@ import NewServer from '../proto/autogenerated/ts/express_server'
|
|||
import GetServerMethods from './services/serverMethods'
|
||||
import serverOptions from './auth';
|
||||
import Main, { LoadMainSettingsFromEnv } from './services/main'
|
||||
import { LoadNosrtSettingsFromEnv } from './services/nostr'
|
||||
import nostrMiddleware from './nostrMiddleware.js'
|
||||
|
||||
const start = async () => {
|
||||
const mainHandler = new Main(LoadMainSettingsFromEnv())
|
||||
await mainHandler.storage.Connect()
|
||||
|
||||
const serverMethods = GetServerMethods(mainHandler)
|
||||
|
||||
const nostrSettings = LoadNosrtSettingsFromEnv()
|
||||
nostrMiddleware(serverMethods, mainHandler, nostrSettings)
|
||||
const Server = NewServer(serverMethods, serverOptions(mainHandler))
|
||||
Server.Listen(3000)
|
||||
}
|
||||
|
|
|
|||
64
src/nostrMiddleware.ts
Normal file
64
src/nostrMiddleware.ts
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
import Main from "./services/main/index.js"
|
||||
import Nostr from "./services/nostr/index.js"
|
||||
import { NostrSettings } from "./services/nostr/index.js"
|
||||
import * as Types from '../proto/autogenerated/ts/types.js'
|
||||
import GetServerMethods from './services/serverMethods/index.js'
|
||||
import serverOptions from './auth.js';
|
||||
const handledEvents: string[] = [] // TODO: - big memory leak here, add TTL
|
||||
const handledRequests: string[] = [] // TODO: - big memory leak here, add TTL
|
||||
type EventRequest = {
|
||||
requestId: string
|
||||
method: string
|
||||
params: Record<string, string>
|
||||
body: any
|
||||
query: Record<string, string>
|
||||
}
|
||||
|
||||
export default (serverMethods: Types.ServerMethods, mainHandler: Main, nostrSettings: NostrSettings) => {
|
||||
// TODO: - move to codegen
|
||||
const nostr = new Nostr(nostrSettings,
|
||||
async (event, getContent) => {
|
||||
//@ts-ignore
|
||||
const eventId = event.id
|
||||
if (handledEvents.includes(eventId)) {
|
||||
console.log("event already handled")
|
||||
return
|
||||
}
|
||||
handledEvents.push(eventId)
|
||||
const nostrPub = event.pubkey as string
|
||||
if (!nostrSettings.allowedPubs.includes(nostrPub)) {
|
||||
console.log("nostr pub not allowed")
|
||||
return
|
||||
}
|
||||
let nostrUser = await mainHandler.storage.FindNostrUser(nostrPub)
|
||||
if (!nostrUser) {
|
||||
|
||||
nostrUser = await mainHandler.storage.AddNostrUser(nostrPub)
|
||||
}
|
||||
let j: EventRequest
|
||||
try {
|
||||
j = JSON.parse(getContent())
|
||||
} catch {
|
||||
console.error("invalid json event received", event.content)
|
||||
return
|
||||
}
|
||||
if (handledRequests.includes(j.requestId)) {
|
||||
console.log("request already handled")
|
||||
return
|
||||
}
|
||||
handledRequests.push(j.requestId)
|
||||
switch (j.method) {
|
||||
case '/api/user/chain/new':
|
||||
const error = Types.NewAddressRequestValidate(j.body)
|
||||
if (error !== null) {
|
||||
console.error("invalid request from", nostrPub, j)// TODO: dont dox
|
||||
return // TODO: respond
|
||||
}
|
||||
if (!serverMethods.NewAddress) {
|
||||
throw new Error("unimplemented NewInvoice")
|
||||
}
|
||||
const res = await serverMethods.NewAddress({ user_id: nostrUser.user.user_id }, j.body)
|
||||
nostr.Send(nostrPub, JSON.stringify({ ...res, requestId: j.requestId }))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
@ -88,11 +88,11 @@ export default class {
|
|||
return (jwt.verify(token, this.settings.jwtSecret) as { userId: string }).userId
|
||||
}
|
||||
|
||||
async AddUser(req: Types.AddUserRequest): Promise<Types.AddUserResponse> {
|
||||
const newUser = await this.storage.AddUser(req.name, req.callback_url, req.secret)
|
||||
async AddBasicUser(req: Types.AddUserRequest): Promise<Types.AddUserResponse> {
|
||||
const { user } = await this.storage.AddBasicUser(req.name, req.secret)
|
||||
return {
|
||||
user_id: newUser.user_id,
|
||||
auth_token: this.SignUserToken(newUser.user_id)
|
||||
user_id: user.user_id,
|
||||
auth_token: this.SignUserToken(user.user_id)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -164,4 +164,6 @@ export default class {
|
|||
}
|
||||
|
||||
async OpenChannel(userId: string, req: Types.OpenChannelRequest): Promise<Types.OpenChannelResponse> { throw new Error("WIP") }
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -4,37 +4,42 @@ import { generatePrivateKey, getPublicKey, relayPool } from 'nostr-tools'
|
|||
//@ts-ignore
|
||||
import { decrypt, encrypt } from 'nostr-tools/nip04.js'
|
||||
import NostrHandler, { LoadNosrtSettingsFromEnv, NostrSettings } from './index.js'
|
||||
import { expect } from 'chai'
|
||||
export const ignore = true
|
||||
const settings = LoadNosrtSettingsFromEnv(true)
|
||||
const clientPool = relayPool()
|
||||
|
||||
const clientPrivateKey = generatePrivateKey()
|
||||
const clientPublicKey = getPublicKey(Buffer.from(clientPrivateKey, "hex"))
|
||||
|
||||
settings.privateKey = generatePrivateKey()
|
||||
settings.publicKey = getPublicKey(Buffer.from(settings.privateKey, "hex"))
|
||||
settings.allowedPubs = [clientPublicKey]
|
||||
|
||||
const nostr = new NostrHandler(settings, async id => { console.log(id); return true })
|
||||
clientPool.setPrivateKey(clientPrivateKey)
|
||||
export const setup = () => {
|
||||
settings.relays.forEach(relay => {
|
||||
try {
|
||||
clientPool.addRelay(relay, { read: true, write: true })
|
||||
} catch (e) {
|
||||
console.error("cannot add relay:", relay)
|
||||
}
|
||||
});
|
||||
}
|
||||
const serverPrivateKey = generatePrivateKey()
|
||||
const serverPublicKey = getPublicKey(Buffer.from(serverPrivateKey, "hex"))
|
||||
|
||||
export default async (d: (message: string, failure?: boolean) => void) => {
|
||||
const e = await clientPool.publish({
|
||||
content: encrypt(clientPrivateKey, settings.publicKey, "test"),
|
||||
created_at: Math.floor(Date.now() / 1000),
|
||||
kind: 4,
|
||||
pubkey: clientPublicKey,
|
||||
//@ts-ignore
|
||||
tags: [['p', settings.publicKey]]
|
||||
}, (status, url) => {
|
||||
console.log(status, url)
|
||||
|
||||
const clientNostr = new NostrHandler({
|
||||
allowedPubs: [],
|
||||
privateKey: clientPrivateKey,
|
||||
publicKey: clientPublicKey,
|
||||
relays: settings.relays
|
||||
}, (event, getContent) => {
|
||||
|
||||
})
|
||||
let receivedServerEvents = 0
|
||||
let latestReceivedServerEvent = ""
|
||||
const serverNostr = new NostrHandler({
|
||||
allowedPubs: [clientPublicKey],
|
||||
privateKey: serverPrivateKey,
|
||||
publicKey: serverPublicKey,
|
||||
relays: settings.relays
|
||||
}, (event, getContent) => {
|
||||
receivedServerEvents++
|
||||
latestReceivedServerEvent = getContent()
|
||||
})
|
||||
await new Promise(res => setTimeout(res, 2000))
|
||||
clientNostr.Send(serverPublicKey, "test")
|
||||
await new Promise(res => setTimeout(res, 1000))
|
||||
console.log(receivedServerEvents, latestReceivedServerEvent)
|
||||
expect(receivedServerEvents).to.equal(1)
|
||||
expect(latestReceivedServerEvent).to.equal("test")
|
||||
d("nostr ok")
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import { relayPool, Subscription } from 'nostr-tools'
|
||||
import { relayPool, Subscription, Event } from 'nostr-tools'
|
||||
//@ts-ignore
|
||||
import { decrypt, encrypt } from 'nostr-tools/nip04.js'
|
||||
import { EnvMustBeNonEmptyString } from '../helpers/envParser.js';
|
||||
|
|
@ -18,13 +18,11 @@ export const LoadNosrtSettingsFromEnv = (test = false): NostrSettings => {
|
|||
}
|
||||
}
|
||||
export default class {
|
||||
shouldHandleEvent: (eventId: string) => Promise<boolean>
|
||||
pool = relayPool()
|
||||
settings: NostrSettings
|
||||
sub: Subscription
|
||||
constructor(settings: NostrSettings, shouldHandleCb: (eventId: string) => Promise<boolean>) {
|
||||
constructor(settings: NostrSettings, eventCallback: (event: Event, getContent: () => string) => void) {
|
||||
this.settings = settings
|
||||
this.shouldHandleEvent = shouldHandleCb
|
||||
this.pool.setPrivateKey(settings.privateKey)
|
||||
settings.relays.forEach(relay => {
|
||||
try {
|
||||
|
|
@ -36,13 +34,31 @@ export default class {
|
|||
this.sub = this.pool.sub({
|
||||
//@ts-ignore
|
||||
filter: {
|
||||
since: Math.ceil(Date.now() / 1000),
|
||||
kinds: [4],
|
||||
'#p': [settings.publicKey],
|
||||
authors: settings.allowedPubs,
|
||||
},
|
||||
cb: async (event, relay) => {
|
||||
console.log(decrypt(this.settings.privateKey, event.pubkey, event.content))
|
||||
cb: async (e, relay) => {
|
||||
if (e.kind !== 4 || !e.pubkey) {
|
||||
return
|
||||
}
|
||||
eventCallback(e, () => {
|
||||
return decrypt(this.settings.privateKey, e.pubkey, e.content)
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
Send(nostrPub: string, message: string) {
|
||||
this.pool.publish({
|
||||
content: encrypt(this.settings.privateKey, nostrPub, message),
|
||||
created_at: Math.floor(Date.now() / 1000),
|
||||
kind: 4,
|
||||
pubkey: this.settings.publicKey,
|
||||
//@ts-ignore
|
||||
tags: [['p', nostrPub]]
|
||||
}, (status, url) => {
|
||||
console.log(status, url) // TODO
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -15,7 +15,7 @@ export default (mainHandler: Main): Types.ServerMethods => {
|
|||
secret_CustomCheck: secret => secret.length >= 8
|
||||
})
|
||||
if (err != null) throw new Error(err.message)
|
||||
return mainHandler.AddUser(req)
|
||||
return mainHandler.AddBasicUser(req)
|
||||
},
|
||||
AuthUser: async (ctx: Types.GuestContext, req: Types.AuthUserRequest): Promise<Types.AuthUserResponse> => {
|
||||
throw new Error("unimplemented")
|
||||
|
|
|
|||
|
|
@ -7,6 +7,8 @@ import { UserReceivingInvoice } from "./entity/UserReceivingInvoice.js"
|
|||
import { UserInvoicePayment } from "./entity/UserInvoicePayment.js"
|
||||
import { EnvMustBeNonEmptyString } from "../helpers/envParser.js"
|
||||
import { UserTransactionPayment } from "./entity/UserTransactionPayment.js"
|
||||
import { UserNostrAuth } from "./entity/UserNostrAuth.js"
|
||||
import { UserBasicAuth } from "./entity/UserBasicAuth.js"
|
||||
export type DbSettings = {
|
||||
databaseFile: string
|
||||
}
|
||||
|
|
@ -18,7 +20,7 @@ export default async (settings: DbSettings) => {
|
|||
type: "sqlite",
|
||||
database: settings.databaseFile,
|
||||
//logging: true,
|
||||
entities: [User, UserReceivingInvoice, UserReceivingAddress, AddressReceivingTransaction, UserInvoicePayment, UserTransactionPayment],
|
||||
entities: [User, UserReceivingInvoice, UserReceivingAddress, AddressReceivingTransaction, UserInvoicePayment, UserTransactionPayment, UserNostrAuth, UserBasicAuth],
|
||||
synchronize: true
|
||||
}).initialize()
|
||||
}
|
||||
|
|
@ -10,16 +10,6 @@ export class User {
|
|||
@Index({ unique: true })
|
||||
user_id: string
|
||||
|
||||
@Column()
|
||||
@Index({ unique: true })
|
||||
name: string
|
||||
|
||||
@Column()
|
||||
secret_sha256: string
|
||||
|
||||
@Column()
|
||||
callbackUrl: string
|
||||
|
||||
@Column({ type: 'integer', default: 0 })
|
||||
balance_sats: number
|
||||
|
||||
|
|
|
|||
19
src/services/storage/entity/UserBasicAuth.ts
Normal file
19
src/services/storage/entity/UserBasicAuth.ts
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
import { Entity, PrimaryGeneratedColumn, Column, Index, Check, ManyToOne } from "typeorm"
|
||||
import { User } from "./User.js"
|
||||
|
||||
@Entity()
|
||||
export class UserBasicAuth {
|
||||
|
||||
@PrimaryGeneratedColumn()
|
||||
serial_id: number
|
||||
|
||||
@ManyToOne(type => User)
|
||||
user: User
|
||||
|
||||
@Column()
|
||||
@Index({ unique: true })
|
||||
name: string
|
||||
|
||||
@Column()
|
||||
secret_sha256: string
|
||||
}
|
||||
16
src/services/storage/entity/UserNostrAuth.ts
Normal file
16
src/services/storage/entity/UserNostrAuth.ts
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
import { Entity, PrimaryGeneratedColumn, Column, Index, Check, ManyToOne } from "typeorm"
|
||||
import { User } from "./User.js"
|
||||
|
||||
@Entity()
|
||||
export class UserNostrAuth {
|
||||
|
||||
@PrimaryGeneratedColumn()
|
||||
serial_id: number
|
||||
|
||||
@ManyToOne(type => User)
|
||||
user: User
|
||||
|
||||
@Column()
|
||||
@Index({ unique: true })
|
||||
nostr_pub: string
|
||||
}
|
||||
|
|
@ -7,6 +7,8 @@ import { UserReceivingInvoice } from "./entity/UserReceivingInvoice.js";
|
|||
import { AddressReceivingTransaction } from "./entity/AddressReceivingTransaction.js";
|
||||
import { UserInvoicePayment } from "./entity/UserInvoicePayment.js";
|
||||
import { UserTransactionPayment } from "./entity/UserTransactionPayment.js";
|
||||
import { UserNostrAuth } from "./entity/UserNostrAuth.js";
|
||||
import { UserBasicAuth } from "./entity/UserBasicAuth.js";
|
||||
export type StorageSettings = {
|
||||
dbSettings: DbSettings
|
||||
}
|
||||
|
|
@ -26,15 +28,25 @@ export default class {
|
|||
StartTransaction(exec: (entityManager: EntityManager) => Promise<void>) {
|
||||
return this.DB.transaction(exec)
|
||||
}
|
||||
async AddUser(name: string, callbackUrl: string, secret: string, entityManager = this.DB): Promise<User> {
|
||||
async AddUser(entityManager = this.DB): Promise<User> {
|
||||
|
||||
const newUser = entityManager.getRepository(User).create({
|
||||
user_id: crypto.randomBytes(32).toString('hex'),
|
||||
name: name,
|
||||
callbackUrl: callbackUrl,
|
||||
secret_sha256: crypto.createHash('sha256').update(secret).digest('base64')
|
||||
user_id: crypto.randomBytes(32).toString('hex')
|
||||
})
|
||||
return entityManager.getRepository(User).save(newUser)
|
||||
}
|
||||
async AddBasicUser(name: string, secret: string): Promise<UserBasicAuth> {
|
||||
return this.DB.transaction(async tx => {
|
||||
const user = await this.AddUser(tx)
|
||||
const newUserAuth = tx.getRepository(UserBasicAuth).create({
|
||||
user: user,
|
||||
name: name,
|
||||
secret_sha256: crypto.createHash('sha256').update(secret).digest('base64')
|
||||
})
|
||||
return tx.getRepository(UserBasicAuth).save(newUserAuth)
|
||||
})
|
||||
|
||||
}
|
||||
FindUser(userId: string, entityManager = this.DB) {
|
||||
return entityManager.getRepository(User).findOne({
|
||||
where: {
|
||||
|
|
@ -50,6 +62,24 @@ export default class {
|
|||
return user
|
||||
}
|
||||
|
||||
async FindNostrUser(nostrPub: string, entityManager = this.DB): Promise<UserNostrAuth | null> {
|
||||
return entityManager.getRepository(UserNostrAuth).findOne({
|
||||
where: { nostr_pub: nostrPub }
|
||||
})
|
||||
|
||||
}
|
||||
async AddNostrUser(nostrPub: string): Promise<UserNostrAuth> {
|
||||
return this.DB.transaction(async tx => {
|
||||
const user = await this.AddUser(tx)
|
||||
const newAuth = tx.getRepository(UserNostrAuth).create({
|
||||
user: user,
|
||||
nostr_pub: nostrPub
|
||||
})
|
||||
return tx.getRepository(UserNostrAuth).save(newAuth)
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
async AddAddressReceivingTransaction(address: UserReceivingAddress, txHash: string, outputIndex: number, amount: number, serviceFee: number, entityManager = this.DB) {
|
||||
const newAddressTransaction = entityManager.getRepository(AddressReceivingTransaction).create({
|
||||
user_address: address,
|
||||
|
|
@ -154,4 +184,6 @@ export default class {
|
|||
throw new Error("unaffected balance decrement for " + userId) // TODO: fix logs doxing
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue