Merge pull request #612 from shocknet/product-qr

product qr
This commit is contained in:
boufni95 2023-03-24 00:23:55 +01:00 committed by GitHub
commit 46819446d5
18 changed files with 1752 additions and 1263 deletions

View file

@ -18,6 +18,18 @@ The nostr server will send back a message response, and inside the body there wi
- This methods has an __empty__ __request__ body
- output: [UserInfo](#UserInfo)
- AddProduct
- auth type: __User__
- input: [AddProductRequest](#AddProductRequest)
- output: [Product](#Product)
- NewProductInvoice
- auth type: __User__
- the request url __query__ can take the following string items:
- id
- This methods has an __empty__ __request__ body
- output: [NewInvoiceResponse](#NewInvoiceResponse)
- GetUserOperations
- auth type: __User__
- input: [GetUserOperationsRequest](#GetUserOperationsRequest)
@ -126,6 +138,22 @@ The nostr server will send back a message response, and inside the body there wi
- This methods has an __empty__ __request__ body
- output: [UserInfo](#UserInfo)
- AddProduct
- auth type: __User__
- http method: __post__
- http route: __/api/user/product/add__
- input: [AddProductRequest](#AddProductRequest)
- output: [Product](#Product)
- NewProductInvoice
- auth type: __User__
- http method: __get__
- http route: __/api/user/product/get/invoice__
- the request url __query__ can take the following string items:
- id
- This methods has an __empty__ __request__ body
- output: [NewInvoiceResponse](#NewInvoiceResponse)
- GetUserOperations
- auth type: __User__
- http method: __post__
@ -232,15 +260,78 @@ The nostr server will send back a message response, and inside the body there wi
## Messages
### The content of requests and response from the methods
### OpenChannelResponse
- __channelId__: _string_
### UserInfo
- __userId__: _string_
- __balance__: _number_
### GetUserOperationsRequest
- __latestIncomingInvoice__: _number_
- __latestOutgoingInvoice__: _number_
- __latestIncomingTx__: _number_
- __latestOutgoingTx__: _number_
### AddProductRequest
- __name__: _string_
- __price_sats__: _number_
### Product
- __id__: _string_
- __name__: _string_
- __price_sats__: _number_
### EncryptionExchangeRequest
- __publicKey__: _string_
- __deviceId__: _string_
### NewInvoiceRequest
- __amountSats__: _number_
- __memo__: _string_
### PayInvoiceResponse
- __preimage__: _string_
### HandleLnurlPayResponse
- __pr__: _string_
- __routes__: ARRAY of: _[Empty](#Empty)_
### AuthUserRequest
- __name__: _string_
- __secret__: _string_
### UserOperations
- __fromIndex__: _number_
- __toIndex__: _number_
- __operations__: ARRAY of: _[UserOperation](#UserOperation)_
### PayAddressRequest
- __address__: _string_
- __amoutSats__: _number_
- __targetConf__: _number_
### OpenChannelRequest
- __destination__: _string_
- __fundingAmount__: _number_
- __pushAmount__: _number_
- __closeAddress__: _string_
### LnurlLinkResponse
- __lnurl__: _string_
- __k1__: _string_
### Empty
### NewInvoiceResponse
- __invoice__: _string_
### DecodeInvoiceRequest
- __invoice__: _string_
### AddUserRequest
- __callbackUrl__: _string_
- __name__: _string_
- __secret__: _string_
### AuthUserResponse
- __userId__: _string_
- __authToken__: _string_
### GetUserOperationsResponse
- __latestOutgoingInvoiceOperations__: _[UserOperations](#UserOperations)_
@ -248,8 +339,15 @@ The nostr server will send back a message response, and inside the body there wi
- __latestOutgoingTxOperations__: _[UserOperations](#UserOperations)_
- __latestIncomingTxOperations__: _[UserOperations](#UserOperations)_
### PayAddressResponse
- __txId__: _string_
### LndGetInfoRequest
- __nodeId__: _number_
### LndGetInfoResponse
- __alias__: _string_
### AddUserResponse
- __userId__: _string_
- __authToken__: _string_
### LnurlWithdrawInfoResponse
- __tag__: _string_
@ -261,21 +359,13 @@ The nostr server will send back a message response, and inside the body there wi
- __balanceCheck__: _string_
- __payLink__: _string_
### UserInfo
- __userId__: _string_
- __balance__: _number_
### GetProductBuyLinkResponse
- __link__: _string_
### NewAddressResponse
- __address__: _string_
### PayAddressRequest
- __address__: _string_
- __amoutSats__: _number_
- __targetConf__: _number_
### HandleLnurlPayResponse
- __pr__: _string_
- __routes__: ARRAY of: _[Empty](#Empty)_
### AddUserRequest
- __callbackUrl__: _string_
- __name__: _string_
- __secret__: _string_
### UserOperation
- __paidAtUnix__: _number_
@ -283,25 +373,21 @@ The nostr server will send back a message response, and inside the body there wi
- __inbound__: _boolean_
- __amount__: _number_
### DecodeInvoiceResponse
- __amount__: _number_
### AddUserResponse
- __userId__: _string_
- __authToken__: _string_
### NewInvoiceResponse
- __invoice__: _string_
### PayInvoiceResponse
- __preimage__: _string_
### NewAddressRequest
- __addressType__: _[AddressType](#AddressType)_
### LnurlLinkResponse
- __lnurl__: _string_
- __k1__: _string_
### NewAddressResponse
- __address__: _string_
### PayInvoiceRequest
- __invoice__: _string_
- __amount__: _number_
### PayAddressResponse
- __txId__: _string_
### DecodeInvoiceResponse
- __amount__: _number_
### LnurlPayInfoResponse
- __tag__: _string_
@ -309,52 +395,6 @@ The nostr server will send back a message response, and inside the body there wi
- __maxSendable__: _number_
- __minSendable__: _number_
- __metadata__: _string_
### GetUserOperationsRequest
- __latestIncomingInvoice__: _number_
- __latestOutgoingInvoice__: _number_
- __latestIncomingTx__: _number_
- __latestOutgoingTx__: _number_
### AuthUserResponse
- __userId__: _string_
- __authToken__: _string_
### EncryptionExchangeRequest
- __publicKey__: _string_
- __deviceId__: _string_
### LndGetInfoRequest
- __nodeId__: _number_
### LndGetInfoResponse
- __alias__: _string_
### PayInvoiceRequest
- __invoice__: _string_
- __amount__: _number_
### OpenChannelRequest
- __destination__: _string_
- __fundingAmount__: _number_
- __pushAmount__: _number_
- __closeAddress__: _string_
### OpenChannelResponse
- __channelId__: _string_
### AuthUserRequest
- __name__: _string_
- __secret__: _string_
### UserOperations
- __fromIndex__: _number_
- __toIndex__: _number_
- __operations__: ARRAY of: _[UserOperation](#UserOperation)_
### NewInvoiceRequest
- __amountSats__: _number_
- __memo__: _string_
## Enums
### The enumerators used in the messages

File diff suppressed because it is too large Load diff

View file

@ -105,6 +105,31 @@ export default (methods: Types.ServerMethods, opts: ServerOptions) => {
res.json({status: 'OK', ...response})
} catch (ex) { const e = ex as any; logErrorAndReturnResponse(e, e.message || e, res, logger); if (opts.throwErrors) throw e }
})
if (!opts.allowNotImplementedMethods && !methods.AddProduct) throw new Error('method: AddProduct is not implemented')
app.post('/api/user/product/add', async (req, res) => {
try {
if (!methods.AddProduct) throw new Error('method: AddProduct is not implemented')
const authContext = await opts.UserAuthGuard(req.headers['authorization'])
const request = req.body
const error = Types.AddProductRequestValidate(request)
if (error !== null) return logErrorAndReturnResponse(error, 'invalid request body', res, logger)
const query = req.query
const params = req.params
const response = await methods.AddProduct({ ...authContext, ...query, ...params }, request)
res.json({status: 'OK', ...response})
} catch (ex) { const e = ex as any; logErrorAndReturnResponse(e, e.message || e, res, logger); if (opts.throwErrors) throw e }
})
if (!opts.allowNotImplementedMethods && !methods.NewProductInvoice) throw new Error('method: NewProductInvoice is not implemented')
app.get('/api/user/product/get/invoice', async (req, res) => {
try {
if (!methods.NewProductInvoice) throw new Error('method: NewProductInvoice is not implemented')
const authContext = await opts.UserAuthGuard(req.headers['authorization'])
const query = req.query
const params = req.params
const response = await methods.NewProductInvoice({ ...authContext, ...query, ...params })
res.json({status: 'OK', ...response})
} catch (ex) { const e = ex as any; logErrorAndReturnResponse(e, e.message || e, res, logger); if (opts.throwErrors) throw e }
})
if (!opts.allowNotImplementedMethods && !methods.GetUserOperations) throw new Error('method: GetUserOperations is not implemented')
app.post('/api/user/operations', async (req, res) => {
try {

View file

@ -92,6 +92,36 @@ export default (params: ClientParams) => ({
}
return { status: 'ERROR', reason: 'invalid response' }
},
AddProduct: async (request: Types.AddProductRequest): Promise<ResultError | ({ status: 'OK' }& Types.Product)> => {
const auth = await params.retrieveUserAuth()
if (auth === null) throw new Error('retrieveUserAuth() returned null')
let finalRoute = '/api/user/product/add'
const { data } = await axios.post(params.baseUrl + finalRoute, request, { headers: { 'authorization': auth } })
if (data.status === 'ERROR' && typeof data.reason === 'string') return data
if (data.status === 'OK') {
const result = data
if(!params.checkResult) return { status: 'OK', ...result }
const error = Types.ProductValidate(result)
if (error === null) { return { status: 'OK', ...result } } else return { status: 'ERROR', reason: error.message }
}
return { status: 'ERROR', reason: 'invalid response' }
},
NewProductInvoice: async (query: Types.NewProductInvoice_Query): Promise<ResultError | ({ status: 'OK' }& Types.NewInvoiceResponse)> => {
const auth = await params.retrieveUserAuth()
if (auth === null) throw new Error('retrieveUserAuth() returned null')
let finalRoute = '/api/user/product/get/invoice'
const q = (new URLSearchParams(query)).toString()
finalRoute = finalRoute + (q === '' ? '' : '?' + q)
const { data } = await axios.get(params.baseUrl + finalRoute, { headers: { 'authorization': auth } })
if (data.status === 'ERROR' && typeof data.reason === 'string') return data
if (data.status === 'OK') {
const result = data
if(!params.checkResult) return { status: 'OK', ...result }
const error = Types.NewInvoiceResponseValidate(result)
if (error === null) { return { status: 'OK', ...result } } else return { status: 'ERROR', reason: error.message }
}
return { status: 'ERROR', reason: 'invalid response' }
},
GetUserOperations: async (request: Types.GetUserOperationsRequest): Promise<ResultError | ({ status: 'OK' }& Types.GetUserOperationsResponse)> => {
const auth = await params.retrieveUserAuth()
if (auth === null) throw new Error('retrieveUserAuth() returned null')

View file

@ -23,6 +23,36 @@ export default (params: NostrClientParams, send: (to:string, message: NostrRequ
}
return { status: 'ERROR', reason: 'invalid response' }
},
AddProduct: async (request: Types.AddProductRequest): Promise<ResultError | ({ status: 'OK' }& Types.Product)> => {
const auth = await params.retrieveNostrUserAuth()
if (auth === null) throw new Error('retrieveNostrUserAuth() returned null')
const nostrRequest: NostrRequest = {}
nostrRequest.body = request
const data = await send(params.pubDestination, {rpcName:'AddProduct',authIdentifier:auth, ...nostrRequest })
if (data.status === 'ERROR' && typeof data.reason === 'string') return data
if (data.status === 'OK') {
const result = data
if(!params.checkResult) return { status: 'OK', ...result }
const error = Types.ProductValidate(result)
if (error === null) { return { status: 'OK', ...result } } else return { status: 'ERROR', reason: error.message }
}
return { status: 'ERROR', reason: 'invalid response' }
},
NewProductInvoice: async (query: Types.NewProductInvoice_Query): Promise<ResultError | ({ status: 'OK' }& Types.NewInvoiceResponse)> => {
const auth = await params.retrieveNostrUserAuth()
if (auth === null) throw new Error('retrieveNostrUserAuth() returned null')
const nostrRequest: NostrRequest = {}
nostrRequest.query = query
const data = await send(params.pubDestination, {rpcName:'NewProductInvoice',authIdentifier:auth, ...nostrRequest })
if (data.status === 'ERROR' && typeof data.reason === 'string') return data
if (data.status === 'OK') {
const result = data
if(!params.checkResult) return { status: 'OK', ...result }
const error = Types.NewInvoiceResponseValidate(result)
if (error === null) { return { status: 'OK', ...result } } else return { status: 'ERROR', reason: error.message }
}
return { status: 'ERROR', reason: 'invalid response' }
},
GetUserOperations: async (request: Types.GetUserOperationsRequest): Promise<ResultError | ({ status: 'OK' }& Types.GetUserOperationsResponse)> => {
const auth = await params.retrieveNostrUserAuth()
if (auth === null) throw new Error('retrieveNostrUserAuth() returned null')

View file

@ -31,6 +31,29 @@ export default (methods: Types.ServerMethods, opts: NostrOptions) => {
res({status: 'OK', ...response})
}catch(ex){ const e = ex as any; logErrorAndReturnResponse(e, e.message || e, res, logger); if (opts.throwErrors) throw e }
break
case 'AddProduct':
try {
if (!methods.AddProduct) throw new Error('method: AddProduct is not implemented')
const authContext = await opts.NostrUserAuthGuard(req.authIdentifier)
const request = req.body
const error = Types.AddProductRequestValidate(request)
if (error !== null) return logErrorAndReturnResponse(error, 'invalid request body', res, logger)
const query = req.query
const params = req.params
const response = await methods.AddProduct({ ...authContext, ...query, ...params }, request)
res({status: 'OK', ...response})
}catch(ex){ const e = ex as any; logErrorAndReturnResponse(e, e.message || e, res, logger); if (opts.throwErrors) throw e }
break
case 'NewProductInvoice':
try {
if (!methods.NewProductInvoice) throw new Error('method: NewProductInvoice is not implemented')
const authContext = await opts.NostrUserAuthGuard(req.authIdentifier)
const query = req.query
const params = req.params
const response = await methods.NewProductInvoice({ ...authContext, ...query, ...params })
res({status: 'OK', ...response})
}catch(ex){ const e = ex as any; logErrorAndReturnResponse(e, e.message || e, res, logger); if (opts.throwErrors) throw e }
break
case 'GetUserOperations':
try {
if (!methods.GetUserOperations) throw new Error('method: GetUserOperations is not implemented')

File diff suppressed because it is too large Load diff

View file

@ -78,7 +78,6 @@ service LightningPub {
option (http_method) = "post";
option (http_route) = "/api/lnd/getinfo";
};
rpc AddUser(structs.AddUserRequest)returns (structs.AddUserResponse){
option (auth_type) = "Guest";
option (http_method) = "post";
@ -96,6 +95,19 @@ service LightningPub {
option (nostr) = true;
}
// USER
rpc AddProduct(structs.AddProductRequest) returns (structs.Product){
option (auth_type) = "User";
option (http_method) = "post";
option (http_route) = "/api/user/product/add";
option (nostr) = true;
};
rpc NewProductInvoice(structs.Empty) returns (structs.NewInvoiceResponse){
option (auth_type) = "User";
option (http_method) = "get";
option (http_route) = "/api/user/product/get/invoice";
option (query) = {items: ["id"]};
option (nostr) = true;
};
rpc GetUserOperations(structs.GetUserOperationsRequest) returns (structs.GetUserOperationsResponse) {
option (auth_type) = "User";
option (http_method) = "post";

View file

@ -49,7 +49,7 @@ message NewInvoiceResponse{
string invoice = 1;
}
message DecodeInvoiceRequest{
string invoice=1;
string invoice = 1;
}
message DecodeInvoiceResponse{
int64 amount=1;
@ -152,4 +152,19 @@ message GetUserOperationsResponse{
UserOperations latestIncomingInvoiceOperations=2;
UserOperations latestOutgoingTxOperations=3;
UserOperations latestIncomingTxOperations=4;
}
message AddProductRequest {
string name = 1;
int64 price_sats = 2;
}
message Product {
string id = 1;
string name = 2;
int64 price_sats = 3;
}
message GetProductBuyLinkResponse {
string link = 1;
}

View file

@ -5,17 +5,8 @@ import * as Types from '../../../proto/autogenerated/ts/types.js'
import LND, { AddressPaidCb, InvoicePaidCb, LndSettings, LoadLndSettingsFromEnv } from '../lnd/index.js'
import { EnvMustBeInteger, EnvMustBeNonEmptyString } from '../helpers/envParser.js'
import { UserReceivingInvoice } from '../storage/entity/UserReceivingInvoice.js'
import { Type } from 'typescript'
export type MainSettings = {
storageSettings: StorageSettings,
lndSettings: LndSettings,
jwtSecret: string
incomingTxFee: number
outgoingTxFee: number
incomingInvoiceFee: number
outgoingInvoiceFee: number
serviceUrl: string
}
import ProductManager from './product.js'
import { MainSettings } from './settings.js'
export const LoadMainSettingsFromEnv = (test = false): MainSettings => {
return {
lndSettings: LoadLndSettingsFromEnv(test),
@ -47,10 +38,12 @@ export default class {
lnd: LND
settings: MainSettings
userOperationsSub: UserOperationsSub | null = null
productManager: ProductManager
constructor(settings: MainSettings) {
this.settings = settings
this.storage = new Storage(settings.storageSettings)
this.lnd = new LND(settings.lndSettings, this.addressPaidCb, this.invoicePaidCb)
this.productManager = new ProductManager(this.storage, this.lnd, this.settings)
}
getServiceFee(action: Types.UserOperationType, amount: number): number {
switch (action) {
@ -119,8 +112,9 @@ export default class {
}
async NewInvoice(userId: string, req: Types.NewInvoiceRequest): Promise<Types.NewInvoiceResponse> {
const user = await this.storage.GetUser(userId)
const res = await this.lnd.NewInvoice(req.amountSats, req.memo)
const userInvoice = await this.storage.AddUserInvoice(userId, res.paymentRequest)
const userInvoice = await this.storage.AddUserInvoice(user, res.paymentRequest)
return {
invoice: userInvoice.invoice
}

View file

@ -0,0 +1,36 @@
import Storage from '../storage/index.js'
import * as Types from '../../../proto/autogenerated/ts/types.js'
import LND from '../lnd/index.js'
import { MainSettings } from './settings.js'
export default class {
storage: Storage
settings: MainSettings
lnd: LND
constructor(storage: Storage, lnd: LND, settings: MainSettings) {
this.storage = storage
this.settings = settings
this.lnd = lnd
}
async AddProduct(userId: string, req: Types.AddProductRequest): Promise<Types.Product> {
const user = await this.storage.GetUser(userId)
const newProduct = await this.storage.productStorage.AddProduct(req.name, req.price_sats, user)
return {
id: newProduct.product_id,
name: newProduct.name,
price_sats: newProduct.price_sats,
}
}
async NewProductInvoice(id: string): Promise<Types.NewInvoiceResponse> {
const product = await this.storage.productStorage.GetProduct(id)
const newInvoice = await this.lnd.NewInvoice(product.price_sats, product.name)
await this.storage.AddUserInvoice(product.owner, newInvoice.paymentRequest, product)
return {
invoice: newInvoice.paymentRequest
}
}
}

View file

@ -0,0 +1,12 @@
import { StorageSettings } from '../storage/index.js'
import { LndSettings } from '../lnd/index.js'
export type MainSettings = {
storageSettings: StorageSettings,
lndSettings: LndSettings,
jwtSecret: string
incomingTxFee: number
outgoingTxFee: number
incomingInvoiceFee: number
outgoingInvoiceFee: number
serviceUrl: string
}

View file

@ -90,8 +90,17 @@ export default (mainHandler: Main): Types.ServerMethods => {
}
return mainHandler.HandleLnurlPay(ctx.k1, +ctx.amount)
},
AddProduct: async (ctx, req) => {
return mainHandler.productManager.AddProduct(ctx.user_id, req)
},
NewProductInvoice: async (ctx) => {
if (!ctx.id) {
throw new Error("product id must be non empty")
}
return mainHandler.productManager.NewProductInvoice(ctx.id)
},
GetLNURLChannelLink: async (ctx) => {
throw new Error("unimplemented")
}
},
}
}

View file

@ -10,6 +10,7 @@ import { UserTransactionPayment } from "./entity/UserTransactionPayment.js"
import { UserNostrAuth } from "./entity/UserNostrAuth.js"
import { UserBasicAuth } from "./entity/UserBasicAuth.js"
import { UserEphemeralKey } from "./entity/UserEphemeralKey.js"
import { Product } from "./entity/Product.js"
export type DbSettings = {
databaseFile: string
}
@ -21,7 +22,7 @@ export default async (settings: DbSettings) => {
type: "sqlite",
database: settings.databaseFile,
//logging: true,
entities: [User, UserReceivingInvoice, UserReceivingAddress, AddressReceivingTransaction, UserInvoicePayment, UserTransactionPayment, UserNostrAuth, UserBasicAuth, UserEphemeralKey],
entities: [User, UserReceivingInvoice, UserReceivingAddress, AddressReceivingTransaction, UserInvoicePayment, UserTransactionPayment, UserNostrAuth, UserBasicAuth, UserEphemeralKey, Product],
synchronize: true
}).initialize()
}

View file

@ -0,0 +1,18 @@
import { Entity, PrimaryGeneratedColumn, Column, ManyToOne } from "typeorm"
import { User } from "./User.js"
@Entity()
export class Product {
@PrimaryGeneratedColumn('uuid')
product_id: string
@ManyToOne(type => User, { eager: true })
owner: User
@Column()
name: string
@Column()
price_sats: number
}

View file

@ -1,4 +1,5 @@
import { Entity, PrimaryGeneratedColumn, Column, Index, Check, ManyToOne, JoinColumn } from "typeorm"
import { Product } from "./Product.js"
import { User } from "./User.js"
@Entity()
@ -26,4 +27,7 @@ export class UserReceivingInvoice {
@Column({ default: 0 })
service_fee: number
@ManyToOne(type => Product, { eager: true })
product: Product | null
}

View file

@ -10,6 +10,8 @@ import { UserTransactionPayment } from "./entity/UserTransactionPayment.js";
import { UserNostrAuth } from "./entity/UserNostrAuth.js";
import { UserBasicAuth } from "./entity/UserBasicAuth.js";
import { EphemeralKeyType, UserEphemeralKey } from "./entity/UserEphemeralKey.js";
import ProductStorage from './productStorage.js'
import { Product } from "./entity/Product.js";
export type StorageSettings = {
dbSettings: DbSettings
}
@ -20,11 +22,13 @@ export const LoadStorageSettingsFromEnv = (test = false): StorageSettings => {
export default class {
DB: DataSource | EntityManager
settings: StorageSettings
productStorage: ProductStorage
constructor(settings: StorageSettings) {
this.settings = settings
}
async Connect() {
this.DB = await NewDB(this.settings.dbSettings)
this.productStorage = new ProductStorage(this.DB)
}
StartTransaction(exec: (entityManager: EntityManager) => Promise<void>) {
return this.DB.transaction(exec)
@ -134,11 +138,12 @@ export default class {
})
}
async AddUserInvoice(userId: string, invoice: string, callbackUrl = "", entityManager = this.DB): Promise<UserReceivingInvoice> {
async AddUserInvoice(user: User, invoice: string, product?: Product, callbackUrl = "", entityManager = this.DB): Promise<UserReceivingInvoice> {
const newUserInvoice = entityManager.getRepository(UserReceivingInvoice).create({
invoice: invoice,
callbackUrl,
user: await this.GetUser(userId, entityManager)
user: user,
product: product
})
return entityManager.getRepository(UserReceivingInvoice).save(newUserInvoice)
}

View file

@ -0,0 +1,23 @@
import { DataSource, EntityManager } from "typeorm"
import { Product } from "./entity/Product.js"
import { User } from "./entity/User.js"
export default class {
DB: DataSource | EntityManager
constructor(DB: DataSource | EntityManager) {
this.DB = DB
}
async AddProduct(name: string, priceSats: number, user: User, entityManager = this.DB): Promise<Product> {
const newProduct = entityManager.getRepository(Product).create({
name: name, price_sats: priceSats, owner: user
})
return entityManager.getRepository(Product).save(newProduct)
}
async GetProduct(id: string, entityManager = this.DB): Promise<Product> {
const product = await entityManager.getRepository(Product).findOne({ where: { product_id: id } })
if (!product) {
throw new Error("product not found")
}
return product
}
}