nostr as transport

This commit is contained in:
hatim boufnichel 2022-11-16 22:08:52 +01:00
parent 1e423e4363
commit b660b9d0a2
12 changed files with 237 additions and 56 deletions

View file

@ -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' } }))
}

View file

@ -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
View 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 }))
}
})
}

View file

@ -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") }
}

View file

@ -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")
}

View file

@ -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
})
}
}

View file

@ -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")

View file

@ -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()
}

View file

@ -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

View 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
}

View 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
}

View file

@ -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
}
}
}