more fixies

This commit is contained in:
hatim 2023-05-11 16:35:59 +02:00
parent c5ea8c899d
commit 6ae03e520c
19 changed files with 2330 additions and 1880 deletions

File diff suppressed because it is too large Load diff

View file

@ -66,10 +66,10 @@ export default (methods: Types.ServerMethods, opts: ServerOptions) => {
} catch (ex) { const e = ex as any; logErrorAndReturnResponse(e, e.message || e, res, logger); if (opts.throwErrors) throw e } } catch (ex) { const e = ex as any; logErrorAndReturnResponse(e, e.message || e, res, logger); if (opts.throwErrors) throw e }
}) })
if (!opts.allowNotImplementedMethods && !methods.SetMockInvoiceAsPaid) throw new Error('method: SetMockInvoiceAsPaid is not implemented') if (!opts.allowNotImplementedMethods && !methods.SetMockInvoiceAsPaid) throw new Error('method: SetMockInvoiceAsPaid is not implemented')
app.post('/api/admin/lnd/mock/invoice/paid', async (req, res) => { app.post('/api/lnd/mock/invoice/paid', async (req, res) => {
try { try {
if (!methods.SetMockInvoiceAsPaid) throw new Error('method: SetMockInvoiceAsPaid is not implemented') if (!methods.SetMockInvoiceAsPaid) throw new Error('method: SetMockInvoiceAsPaid is not implemented')
const authContext = await opts.AdminAuthGuard(req.headers['authorization']) const authContext = await opts.GuestAuthGuard(req.headers['authorization'])
const request = req.body const request = req.body
const error = Types.SetMockInvoiceAsPaidRequestValidate(request) const error = Types.SetMockInvoiceAsPaidRequestValidate(request)
if (error !== null) return logErrorAndReturnResponse(error, 'invalid request body', res, logger) if (error !== null) return logErrorAndReturnResponse(error, 'invalid request body', res, logger)
@ -93,6 +93,17 @@ export default (methods: Types.ServerMethods, opts: ServerOptions) => {
res.json({status: 'OK', ...response}) res.json({status: 'OK', ...response})
} catch (ex) { const e = ex as any; logErrorAndReturnResponse(e, e.message || e, res, logger); if (opts.throwErrors) throw e } } catch (ex) { const e = ex as any; logErrorAndReturnResponse(e, e.message || e, res, logger); if (opts.throwErrors) throw e }
}) })
if (!opts.allowNotImplementedMethods && !methods.GetApp) throw new Error('method: GetApp is not implemented')
app.post('/api/app/get', async (req, res) => {
try {
if (!methods.GetApp) throw new Error('method: GetApp is not implemented')
const authContext = await opts.AppAuthGuard(req.headers['authorization'])
const query = req.query
const params = req.params
const response = await methods.GetApp({ ...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.AddAppUser) throw new Error('method: AddAppUser is not implemented') if (!opts.allowNotImplementedMethods && !methods.AddAppUser) throw new Error('method: AddAppUser is not implemented')
app.post('/api/app/user/add', async (req, res) => { app.post('/api/app/user/add', async (req, res) => {
try { try {
@ -205,6 +216,34 @@ export default (methods: Types.ServerMethods, opts: ServerOptions) => {
res.json({status: 'OK', ...response}) res.json({status: 'OK', ...response})
} catch (ex) { const e = ex as any; logErrorAndReturnResponse(e, e.message || e, res, logger); if (opts.throwErrors) throw e } } catch (ex) { const e = ex as any; logErrorAndReturnResponse(e, e.message || e, res, logger); if (opts.throwErrors) throw e }
}) })
if (!opts.allowNotImplementedMethods && !methods.SetMockAppUserBalance) throw new Error('method: SetMockAppUserBalance is not implemented')
app.post('/api/app/mock/user/blance/set', async (req, res) => {
try {
if (!methods.SetMockAppUserBalance) throw new Error('method: SetMockAppUserBalance is not implemented')
const authContext = await opts.AppAuthGuard(req.headers['authorization'])
const request = req.body
const error = Types.SetMockAppUserBalanceRequestValidate(request)
if (error !== null) return logErrorAndReturnResponse(error, 'invalid request body', res, logger)
const query = req.query
const params = req.params
await methods.SetMockAppUserBalance({ ...authContext, ...query, ...params }, request)
res.json({status: 'OK'})
} catch (ex) { const e = ex as any; logErrorAndReturnResponse(e, e.message || e, res, logger); if (opts.throwErrors) throw e }
})
if (!opts.allowNotImplementedMethods && !methods.SetMockAppBalance) throw new Error('method: SetMockAppBalance is not implemented')
app.post('/api/app/mock/blance/set', async (req, res) => {
try {
if (!methods.SetMockAppBalance) throw new Error('method: SetMockAppBalance is not implemented')
const authContext = await opts.AppAuthGuard(req.headers['authorization'])
const request = req.body
const error = Types.SetMockAppBalanceRequestValidate(request)
if (error !== null) return logErrorAndReturnResponse(error, 'invalid request body', res, logger)
const query = req.query
const params = req.params
await methods.SetMockAppBalance({ ...authContext, ...query, ...params }, request)
res.json({status: 'OK'})
} catch (ex) { const e = ex as any; logErrorAndReturnResponse(e, e.message || e, res, logger); if (opts.throwErrors) throw e }
})
if (!opts.allowNotImplementedMethods && !methods.AddUser) throw new Error('method: AddUser is not implemented') if (!opts.allowNotImplementedMethods && !methods.AddUser) throw new Error('method: AddUser is not implemented')
app.post('/api/user/add', async (req, res) => { app.post('/api/user/add', async (req, res) => {
try { try {

View file

@ -52,9 +52,9 @@ export default (params: ClientParams) => ({
return { status: 'ERROR', reason: 'invalid response' } return { status: 'ERROR', reason: 'invalid response' }
}, },
SetMockInvoiceAsPaid: async (request: Types.SetMockInvoiceAsPaidRequest): Promise<ResultError | ({ status: 'OK' })> => { SetMockInvoiceAsPaid: async (request: Types.SetMockInvoiceAsPaidRequest): Promise<ResultError | ({ status: 'OK' })> => {
const auth = await params.retrieveAdminAuth() const auth = await params.retrieveGuestAuth()
if (auth === null) throw new Error('retrieveAdminAuth() returned null') if (auth === null) throw new Error('retrieveGuestAuth() returned null')
let finalRoute = '/api/admin/lnd/mock/invoice/paid' let finalRoute = '/api/lnd/mock/invoice/paid'
const { data } = await axios.post(params.baseUrl + finalRoute, request, { headers: { 'authorization': auth } }) 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 === 'ERROR' && typeof data.reason === 'string') return data
if (data.status === 'OK') { if (data.status === 'OK') {
@ -76,6 +76,20 @@ export default (params: ClientParams) => ({
} }
return { status: 'ERROR', reason: 'invalid response' } return { status: 'ERROR', reason: 'invalid response' }
}, },
GetApp: async (): Promise<ResultError | ({ status: 'OK' }& Types.Application)> => {
const auth = await params.retrieveAppAuth()
if (auth === null) throw new Error('retrieveAppAuth() returned null')
let finalRoute = '/api/app/get'
const { data } = await axios.post(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.ApplicationValidate(result)
if (error === null) { return { status: 'OK', ...result } } else return { status: 'ERROR', reason: error.message }
}
return { status: 'ERROR', reason: 'invalid response' }
},
AddAppUser: async (request: Types.AddAppUserRequest): Promise<ResultError | ({ status: 'OK' }& Types.AppUser)> => { AddAppUser: async (request: Types.AddAppUserRequest): Promise<ResultError | ({ status: 'OK' }& Types.AppUser)> => {
const auth = await params.retrieveAppAuth() const auth = await params.retrieveAppAuth()
if (auth === null) throw new Error('retrieveAppAuth() returned null') if (auth === null) throw new Error('retrieveAppAuth() returned null')
@ -182,6 +196,28 @@ export default (params: ClientParams) => ({
} }
return { status: 'ERROR', reason: 'invalid response' } return { status: 'ERROR', reason: 'invalid response' }
}, },
SetMockAppUserBalance: async (request: Types.SetMockAppUserBalanceRequest): Promise<ResultError | ({ status: 'OK' })> => {
const auth = await params.retrieveAppAuth()
if (auth === null) throw new Error('retrieveAppAuth() returned null')
let finalRoute = '/api/app/mock/user/blance/set'
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') {
return data
}
return { status: 'ERROR', reason: 'invalid response' }
},
SetMockAppBalance: async (request: Types.SetMockAppBalanceRequest): Promise<ResultError | ({ status: 'OK' })> => {
const auth = await params.retrieveAppAuth()
if (auth === null) throw new Error('retrieveAppAuth() returned null')
let finalRoute = '/api/app/mock/blance/set'
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') {
return data
}
return { status: 'ERROR', reason: 'invalid response' }
},
AddUser: async (request: Types.AddUserRequest): Promise<ResultError | ({ status: 'OK' }& Types.AddUserResponse)> => { AddUser: async (request: Types.AddUserRequest): Promise<ResultError | ({ status: 'OK' }& Types.AddUserResponse)> => {
const auth = await params.retrieveGuestAuth() const auth = await params.retrieveGuestAuth()
if (auth === null) throw new Error('retrieveGuestAuth() returned null') if (auth === null) throw new Error('retrieveGuestAuth() returned null')

File diff suppressed because it is too large Load diff

View file

@ -89,9 +89,9 @@ service LightningPub {
}; };
rpc SetMockInvoiceAsPaid(structs.SetMockInvoiceAsPaidRequest) returns (structs.Empty) { rpc SetMockInvoiceAsPaid(structs.SetMockInvoiceAsPaidRequest) returns (structs.Empty) {
option (auth_type) = "Admin"; option (auth_type) = "Guest";
option (http_method) = "post"; option (http_method) = "post";
option (http_route) = "/api/admin/lnd/mock/invoice/paid"; option (http_route) = "/api/lnd/mock/invoice/paid";
} }
// <App> // <App>
@ -102,6 +102,12 @@ service LightningPub {
option (http_route) = "/api/admin/app/add"; option (http_route) = "/api/admin/app/add";
}; };
rpc GetApp(structs.Empty) returns (structs.Application) {
option (auth_type) = "App";
option (http_method) = "post";
option (http_route) = "/api/app/get";
}
rpc AddAppUser(structs.AddAppUserRequest)returns (structs.AppUser) { rpc AddAppUser(structs.AddAppUserRequest)returns (structs.AppUser) {
option (auth_type) = "App"; option (auth_type) = "App";
option (http_method) = "post"; option (http_method) = "post";
@ -149,6 +155,16 @@ service LightningPub {
option (http_method) = "post"; option (http_method) = "post";
option (http_route) = "/api/app/user/lnurl/pay/info"; option (http_route) = "/api/app/user/lnurl/pay/info";
} }
rpc SetMockAppUserBalance(structs.SetMockAppUserBalanceRequest) returns (structs.Empty) {
option (auth_type) = "App";
option (http_method) = "post";
option (http_route) = "/api/app/mock/user/blance/set";
}
rpc SetMockAppBalance(structs.SetMockAppBalanceRequest) returns (structs.Empty) {
option (auth_type) = "App";
option (http_method) = "post";
option (http_route) = "/api/app/mock/blance/set";
}
// </App> // </App>

View file

@ -29,11 +29,16 @@ message AddAppRequest {
string name = 1; string name = 1;
} }
message AddAppResponse { message Application {
string name = 1; string name = 1;
string id = 2; string id = 2;
string auth_token = 3; int64 balance = 3;
} }
message AddAppResponse {
Application app = 1;
string auth_token = 2;
}
message AddAppUserRequest { message AddAppUserRequest {
string identifier = 1; string identifier = 1;
@ -91,6 +96,15 @@ message GetAppUserLNURLInfoRequest {
string user_identifier = 1; string user_identifier = 1;
} }
message SetMockAppUserBalanceRequest {
string user_identifier = 1;
int64 amount = 2;
}
message SetMockAppBalanceRequest {
int64 amount = 1;
}
enum AddressType { enum AddressType {
WITNESS_PUBKEY_HASH = 0; WITNESS_PUBKEY_HASH = 0;
NESTED_PUBKEY_HASH = 1; NESTED_PUBKEY_HASH = 1;

View file

@ -32,8 +32,10 @@ export interface LightningHandler {
export default (settings: LndSettings, addressPaidCb: AddressPaidCb, invoicePaidCb: InvoicePaidCb): LightningHandler => { export default (settings: LndSettings, addressPaidCb: AddressPaidCb, invoicePaidCb: InvoicePaidCb): LightningHandler => {
if (settings.mockLnd) { if (settings.mockLnd) {
console.log("registering mock lnd handler")
return new MockLnd(settings, addressPaidCb, invoicePaidCb) return new MockLnd(settings, addressPaidCb, invoicePaidCb)
} else { } else {
console.log("registering prod lnd handler")
return new LND(settings, addressPaidCb, invoicePaidCb) return new LND(settings, addressPaidCb, invoicePaidCb)
} }
} }

View file

@ -157,7 +157,6 @@ export default class {
this.checkReady() this.checkReady()
const abortController = new AbortController() const abortController = new AbortController()
const req = PayInvoiceReq(invoice, amount, feeLimit) const req = PayInvoiceReq(invoice, amount, feeLimit)
console.log(req)
const stream = this.router.sendPaymentV2(req, { abort: abortController.signal }) const stream = this.router.sendPaymentV2(req, { abort: abortController.signal })
return new Promise((res, rej) => { return new Promise((res, rej) => {
stream.responses.onError(error => { stream.responses.onError(error => {

View file

@ -15,7 +15,7 @@ import { SendCoinsReq } from './sendCoinsReq.js';
import { LndSettings, AddressPaidCb, InvoicePaidCb, NodeInfo, Invoice, DecodedInvoice, PaidInvoice } from './settings.js'; import { LndSettings, AddressPaidCb, InvoicePaidCb, NodeInfo, Invoice, DecodedInvoice, PaidInvoice } from './settings.js';
export default class { export default class {
invoicesAwaiting: Record<string /* invoice */, { value: number, memo: string, expiryUnix: number }> invoicesAwaiting: Record<string /* invoice */, { value: number, memo: string, expiryUnix: number }> = {}
settings: LndSettings settings: LndSettings
abortController = new AbortController() abortController = new AbortController()
addressPaidCb: AddressPaidCb addressPaidCb: AddressPaidCb
@ -49,12 +49,16 @@ export default class {
} }
async NewInvoice(value: number, memo: string, expiry: number): Promise<Invoice> { async NewInvoice(value: number, memo: string, expiry: number): Promise<Invoice> {
const mockInvoice = "lnbcrtmock" + crypto.randomBytes(32).toString('hex') const mockInvoice = "lnbcrtmockin" + crypto.randomBytes(32).toString('hex')
this.invoicesAwaiting[mockInvoice] = { value, memo, expiryUnix: expiry + Date.now() / 1000 } this.invoicesAwaiting[mockInvoice] = { value, memo, expiryUnix: expiry + Date.now() / 1000 }
return { payRequest: mockInvoice } return { payRequest: mockInvoice }
} }
async DecodeInvoice(paymentRequest: string): Promise<DecodedInvoice> { async DecodeInvoice(paymentRequest: string): Promise<DecodedInvoice> {
if (paymentRequest.startsWith('lnbcrtmockout')) {
const amt = this.decodeOutboundInvoice(paymentRequest)
return { numSatoshis: amt }
}
const i = this.invoicesAwaiting[paymentRequest] const i = this.invoicesAwaiting[paymentRequest]
if (!i) { if (!i) {
throw new Error("invoice not found") throw new Error("invoice not found")
@ -70,15 +74,23 @@ export default class {
return Math.max(0, Math.floor(amount * (1 - this.settings.feeRateLimit) - this.settings.feeFixedLimit)) return Math.max(0, Math.floor(amount * (1 - this.settings.feeRateLimit) - this.settings.feeFixedLimit))
} }
async PayInvoice(invoice: string, amount: number, feeLimit: number): Promise<PaidInvoice> { decodeOutboundInvoice(invoice: string): number {
if (!invoice.startsWith('lnbcrtmock')) { if (!invoice.startsWith('lnbcrtmockout')) {
throw new Error("invalid mock invoice provided for payment") throw new Error("invalid mock invoice provided for payment")
} }
const amt = invoice.substring('lnbcrtmock'.length) const amt = invoice.substring('lnbcrtmockout'.length).split("__")[0]
if (isNaN(+amt)) { if (isNaN(+amt)) {
throw new Error("invalid mock invoice provided for payment") throw new Error("invalid mock invoice provided for payment")
} }
return { feeSat: 0, paymentPreimage: "all_good", valueSat: +amt || amount } return +amt
}
async PayInvoice(invoice: string, amount: number, feeLimit: number): Promise<PaidInvoice> {
console.log('payng', invoice)
await new Promise(res => setTimeout(res, 200))
const amt = this.decodeOutboundInvoice(invoice)
console.log('paid', invoice)
return { feeSat: 1, paymentPreimage: "all_good", valueSat: amt || amount }
} }
async EstimateChainFees(address: string, amount: number, targetConf: number): Promise<EstimateFeeResponse> { async EstimateChainFees(address: string, amount: number, targetConf: number): Promise<EstimateFeeResponse> {

View file

@ -27,19 +27,45 @@ export default class {
t = token.substring("Bearer ".length) t = token.substring("Bearer ".length)
} }
if (!t) throw new Error("no app token provided") if (!t) throw new Error("no app token provided")
return (jwt.verify(token, this.settings.jwtSecret) as { appId: string }).appId const decoded = jwt.verify(token, this.settings.jwtSecret) as { appId?: string }
if (!decoded.appId) {
throw new Error("the provided token is not an app token")
}
return decoded.appId
}
async SetMockAppUserBalance(appId: string, req: Types.SetMockAppUserBalanceRequest) {
const user = await this.storage.applicationStorage.GetOrCreateApplicationUser(appId, req.user_identifier, 0)
await this.paymentManager.SetMockUserBalance(user.user.user_id, req.amount)
}
async SetMockAppBalance(appId: string, req: Types.SetMockAppBalanceRequest) {
const app = await this.storage.applicationStorage.GetApplication(appId)
await this.paymentManager.SetMockUserBalance(app.owner.user_id, req.amount)
} }
async AddApp(req: Types.AddAppRequest): Promise<Types.AddAppResponse> { async AddApp(req: Types.AddAppRequest): Promise<Types.AddAppResponse> {
const app = await this.storage.applicationStorage.AddApplication(req.name) const app = await this.storage.applicationStorage.AddApplication(req.name)
return { return {
id: app.app_id, app: {
name: app.name, id: app.app_id,
name: app.name,
balance: 0
},
auth_token: this.SignAppToken(app.app_id) auth_token: this.SignAppToken(app.app_id)
} }
} }
async GetApp(appId: string): Promise<Types.Application> {
const app = await this.storage.applicationStorage.GetApplication(appId)
return {
name: app.name,
id: app.app_id,
balance: app.owner.balance_sats
}
}
async AddAppUser(appId: string, req: Types.AddAppUserRequest): Promise<Types.AppUser> { async AddAppUser(appId: string, req: Types.AddAppUserRequest): Promise<Types.AppUser> {
let u: ApplicationUser let u: ApplicationUser
if (req.fail_if_exists) { if (req.fail_if_exists) {
@ -60,7 +86,7 @@ export default class {
async AddAppInvoice(appId: string, req: Types.AddAppInvoiceRequest): Promise<Types.NewInvoiceResponse> { async AddAppInvoice(appId: string, req: Types.AddAppInvoiceRequest): Promise<Types.NewInvoiceResponse> {
const app = await this.storage.applicationStorage.GetApplication(appId) const app = await this.storage.applicationStorage.GetApplication(appId)
const payer = await this.storage.applicationStorage.GetOrCreateApplicationUser(appId, req.payer_identifier, 0) const payer = await this.storage.applicationStorage.GetOrCreateApplicationUser(appId, req.payer_identifier, 0)
const opts: InboundOptionals = { callbackUrl: req.http_callback_url, expiry: defaultInvoiceExpiry, expectedPayer: payer.user } const opts: InboundOptionals = { callbackUrl: req.http_callback_url, expiry: defaultInvoiceExpiry, expectedPayer: payer.user, linkedApplication: app }
return this.paymentManager.NewInvoice(app.owner.user_id, req.invoice_req, opts) return this.paymentManager.NewInvoice(app.owner.user_id, req.invoice_req, opts)
} }
@ -77,6 +103,7 @@ export default class {
async GetAppUser(appId: string, req: Types.GetAppUserRequest): Promise<Types.AppUser> { async GetAppUser(appId: string, req: Types.GetAppUserRequest): Promise<Types.AppUser> {
const user = await this.storage.applicationStorage.GetApplicationUser(appId, req.user_identifier) const user = await this.storage.applicationStorage.GetApplicationUser(appId, req.user_identifier)
const max = this.paymentManager.GetMaxPayableInvoice(user.user.balance_sats) const max = this.paymentManager.GetMaxPayableInvoice(user.user.balance_sats)
console.log(max, user.user.balance_sats)
return { return {
max_withdrawable: max, identifier: req.user_identifier, info: { max_withdrawable: max, identifier: req.user_identifier, info: {
userId: user.user.user_id, balance: user.user.balance_sats userId: user.user.user_id, balance: user.user.balance_sats
@ -85,14 +112,16 @@ export default class {
} }
async PayAppUserInvoice(appId: string, req: Types.PayAppUserInvoiceRequest): Promise<Types.PayAppUserInvoiceResponse> { async PayAppUserInvoice(appId: string, req: Types.PayAppUserInvoiceRequest): Promise<Types.PayAppUserInvoiceResponse> {
const app = await this.storage.applicationStorage.GetApplication(appId)
const appUser = await this.storage.applicationStorage.GetApplicationUser(appId, req.user_identifier) const appUser = await this.storage.applicationStorage.GetApplicationUser(appId, req.user_identifier)
return this.paymentManager.PayInvoice(appUser.user.user_id, req) return this.paymentManager.PayInvoice(appUser.user.user_id, req, app)
} }
async SendAppUserToAppUserPayment(appId: string, req: Types.SendAppUserToAppUserPaymentRequest): Promise<void> { async SendAppUserToAppUserPayment(appId: string, req: Types.SendAppUserToAppUserPaymentRequest): Promise<void> {
const fromUser = await this.storage.applicationStorage.GetApplicationUser(appId, req.from_user_identifier) const fromUser = await this.storage.applicationStorage.GetApplicationUser(appId, req.from_user_identifier)
const toUser = await this.storage.applicationStorage.GetApplicationUser(appId, req.to_user_identifier) const toUser = await this.storage.applicationStorage.GetApplicationUser(appId, req.to_user_identifier)
await this.paymentManager.SendUserToUserPayment(fromUser.user.user_id, toUser.user.user_id, req.amount) const app = await this.storage.applicationStorage.GetApplication(appId)
await this.paymentManager.SendUserToUserPayment(fromUser.user.user_id, toUser.user.user_id, req.amount, app)
} }
async SendAppUserToAppPayment(appId: string, req: Types.SendAppUserToAppPaymentRequest): Promise<void> { async SendAppUserToAppPayment(appId: string, req: Types.SendAppUserToAppPaymentRequest): Promise<void> {

View file

@ -73,11 +73,13 @@ export default class {
const userInvoice = await this.storage.paymentStorage.GetInvoiceOwner(paymentRequest, tx) const userInvoice = await this.storage.paymentStorage.GetInvoiceOwner(paymentRequest, tx)
if (!userInvoice || userInvoice.paid_at_unix > 0) { return } if (!userInvoice || userInvoice.paid_at_unix > 0) { return }
const fee = this.paymentManager.getServiceFee(Types.UserOperationType.INCOMING_INVOICE, amount) const fee = this.paymentManager.getServiceFee(Types.UserOperationType.INCOMING_INVOICE, amount)
const maybeApp = await this.storage.applicationStorage.IsApplicationUser(userInvoice.user.user_id)
try { try {
// This call will fail if the invoice is already registered // This call will fail if the invoice is already registered
await this.storage.paymentStorage.FlagInvoiceAsPaid(userInvoice, amount, fee, tx) await this.storage.paymentStorage.FlagInvoiceAsPaid(userInvoice, amount, fee, tx)
await this.storage.userStorage.IncrementUserBalance(userInvoice.user.user_id, amount - fee, tx) await this.storage.userStorage.IncrementUserBalance(userInvoice.user.user_id, amount - fee, tx)
if (userInvoice.linkedApplication) {
await this.storage.userStorage.IncrementUserBalance(userInvoice.linkedApplication.owner.user_id, fee, tx)
}
await this.triggerPaidCallback(userInvoice.callbackUrl) await this.triggerPaidCallback(userInvoice.callbackUrl)
} catch { } catch {
//TODO //TODO

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 { InboundOptionals, defaultInvoiceExpiry } from '../storage/paymentStorage.js' import { InboundOptionals, defaultInvoiceExpiry } from '../storage/paymentStorage.js'
import { LightningHandler } from '../lnd/index.js' import { LightningHandler } from '../lnd/index.js'
import { Application } from '../storage/entity/Application.js'
interface UserOperationInfo { interface UserOperationInfo {
serial_id: number serial_id: number
paid_amount: number paid_amount: number
@ -39,9 +40,20 @@ export default class {
} }
async SetMockInvoiceAsPaid(req: Types.SetMockInvoiceAsPaidRequest) { async SetMockInvoiceAsPaid(req: Types.SetMockInvoiceAsPaidRequest) {
if (!this.settings.lndSettings.mockLnd) {
throw new Error("mock disabled, cannot set invoice as paid")
}
await this.lnd.SetMockInvoiceAsPaid(req.invoice, req.amount) await this.lnd.SetMockInvoiceAsPaid(req.invoice, req.amount)
} }
async SetMockUserBalance(userId: string, balance: number) {
if (!this.settings.lndSettings.mockLnd) {
throw new Error("mock disabled, cannot set invoice as paid")
}
console.log("setting mock balance...")
await this.storage.userStorage.UpdateUser(userId, { balance_sats: balance })
}
async NewAddress(userId: string, req: Types.NewAddressRequest): Promise<Types.NewAddressResponse> { async NewAddress(userId: string, req: Types.NewAddressRequest): Promise<Types.NewAddressResponse> {
const res = await this.lnd.NewAddress(req.addressType) const res = await this.lnd.NewAddress(req.addressType)
const userAddress = await this.storage.paymentStorage.AddUserAddress(userId, res.address) const userAddress = await this.storage.paymentStorage.AddUserAddress(userId, res.address)
@ -60,8 +72,12 @@ export default class {
} }
async lockUserWithMinBalance(userId: string, minBalance: number) { async lockUserWithMinBalance(userId: string, minBalance: number) {
console.log("locking", userId)
return this.storage.StartTransaction(async tx => { return this.storage.StartTransaction(async tx => {
const user = await this.storage.userStorage.GetUser(userId, tx) const user = await this.storage.userStorage.GetUser(userId, tx)
if (user.locked) {
throw new Error("user is already withdrawing")
}
if (user.balance_sats < minBalance) { if (user.balance_sats < minBalance) {
throw new Error("insufficient balance") throw new Error("insufficient balance")
} }
@ -80,7 +96,7 @@ export default class {
amount: Number(decoded.numSatoshis) amount: Number(decoded.numSatoshis)
} }
} }
async PayInvoice(userId: string, req: Types.PayInvoiceRequest): Promise<Types.PayInvoiceResponse> { async PayInvoice(userId: string, req: Types.PayInvoiceRequest, linkedApplication?: Application): Promise<Types.PayInvoiceResponse> {
const decoded = await this.lnd.DecodeInvoice(req.invoice) const decoded = await this.lnd.DecodeInvoice(req.invoice)
if (decoded.numSatoshis !== 0 && req.amount !== 0) { if (decoded.numSatoshis !== 0 && req.amount !== 0) {
throw new Error("invoice has value, do not provide amount the the request") throw new Error("invoice has value, do not provide amount the the request")
@ -92,11 +108,15 @@ export default class {
const serviceFee = this.getServiceFee(Types.UserOperationType.OUTGOING_INVOICE, payAmount) const serviceFee = this.getServiceFee(Types.UserOperationType.OUTGOING_INVOICE, payAmount)
const totalAmountToDecrement = payAmount + serviceFee const totalAmountToDecrement = payAmount + serviceFee
const routingFeeLimit = this.lnd.GetFeeLimitAmount(payAmount) const routingFeeLimit = this.lnd.GetFeeLimitAmount(payAmount)
await this.lockUserWithMinBalance(userId, totalAmountToDecrement + routingFeeLimit) await this.lockUserWithMinBalance(userId, totalAmountToDecrement + routingFeeLimit)
const payment = await this.lnd.PayInvoice(req.invoice, req.amount, routingFeeLimit) const payment = await this.lnd.PayInvoice(req.invoice, req.amount, routingFeeLimit)
await this.storage.userStorage.DecrementUserBalance(userId, totalAmountToDecrement + Number(payment.feeSat)) await this.storage.userStorage.DecrementUserBalance(userId, totalAmountToDecrement + Number(payment.feeSat))
if (linkedApplication) {
await this.storage.userStorage.IncrementUserBalance(linkedApplication.owner.user_id, serviceFee)
} else {
//const appOwner = await this.storage.applicationStorage.IsApplicationOwner(userId)
}
await this.storage.userStorage.UnlockUser(userId) await this.storage.userStorage.UnlockUser(userId)
await this.storage.paymentStorage.AddUserInvoicePayment(userId, req.invoice, payAmount, Number(payment.feeSat), serviceFee) await this.storage.paymentStorage.AddUserInvoicePayment(userId, req.invoice, payAmount, Number(payment.feeSat), serviceFee)
return { return {
@ -241,7 +261,7 @@ export default class {
} }
} }
async SendUserToUserPayment(fromUserId: string, toUserId: string, amount: number) { async SendUserToUserPayment(fromUserId: string, toUserId: string, amount: number, linkedApplication?: Application) {
await this.storage.StartTransaction(async tx => { await this.storage.StartTransaction(async tx => {
const fromUser = await this.storage.userStorage.GetUser(fromUserId, tx) const fromUser = await this.storage.userStorage.GetUser(fromUserId, tx)
const toUser = await this.storage.userStorage.GetUser(toUserId, tx) const toUser = await this.storage.userStorage.GetUser(toUserId, tx)
@ -253,6 +273,9 @@ export default class {
await this.storage.userStorage.DecrementUserBalance(fromUser.user_id, amount, tx) await this.storage.userStorage.DecrementUserBalance(fromUser.user_id, amount, tx)
await this.storage.userStorage.IncrementUserBalance(toUser.user_id, toIncrement, tx) await this.storage.userStorage.IncrementUserBalance(toUser.user_id, toIncrement, tx)
await this.storage.paymentStorage.AddUserToUserPayment(fromUserId, toUserId, amount, fee) await this.storage.paymentStorage.AddUserToUserPayment(fromUserId, toUserId, amount, fee)
if (linkedApplication) {
await this.storage.userStorage.IncrementUserBalance(linkedApplication.owner.user_id, fee)
}
}) })
} }

View file

@ -15,13 +15,18 @@ export default class {
} }
DecodeUserToken(token?: string): string { DecodeUserToken(token?: string): string {
if (!token) throw new Error("empty user token provided") throw new Error("users methods temporarely disabled")
/*if (!token) throw new Error("empty user token provided")
let t = token let t = token
if (token.startsWith("Bearer ")) { if (token.startsWith("Bearer ")) {
t = token.substring("Bearer ".length) t = token.substring("Bearer ".length)
} }
if (!t) throw new Error("no user token provided") if (!t) throw new Error("no user token provided")
return (jwt.verify(token, this.settings.jwtSecret) as { userId: string }).userId const decoded = jwt.verify(token, this.settings.jwtSecret) as { userId?: string }
if (!decoded.userId) {
throw new Error("the provided token is not an app token")
}
return decoded.userId*/
} }
async AddBasicUser(req: Types.AddUserRequest): Promise<Types.AddUserResponse> { async AddBasicUser(req: Types.AddUserRequest): Promise<Types.AddUserResponse> {

View file

@ -116,6 +116,9 @@ export default (mainHandler: Main): Types.ServerMethods => {
if (err != null) throw new Error(err.message) if (err != null) throw new Error(err.message)
return mainHandler.applicationManager.AddApp(req) return mainHandler.applicationManager.AddApp(req)
}, },
GetApp: async (ctx) => {
return mainHandler.applicationManager.GetApp(ctx.app_id)
},
AddAppUser: async (ctx, req) => { AddAppUser: async (ctx, req) => {
const err = Types.AddAppUserRequestValidate(req, { const err = Types.AddAppUserRequestValidate(req, {
identifier_CustomCheck: id => id !== '' identifier_CustomCheck: id => id !== ''
@ -176,6 +179,16 @@ export default (mainHandler: Main): Types.ServerMethods => {
}) })
if (err != null) throw new Error(err.message) if (err != null) throw new Error(err.message)
return mainHandler.applicationManager.GetAppUserLNURLInfo(ctx.app_id, req) return mainHandler.applicationManager.GetAppUserLNURLInfo(ctx.app_id, req)
},
SetMockAppUserBalance: async (ctx, req) => {
const err = Types.SetMockAppUserBalanceRequestValidate(req, {
user_identifier_CustomCheck: id => id !== ''
})
if (err != null) throw new Error(err.message)
await mainHandler.applicationManager.SetMockAppUserBalance(ctx.app_id, req)
},
SetMockAppBalance: async (ctx, req) => {
await mainHandler.applicationManager.SetMockAppBalance(ctx.app_id, req)
} }
} }
} }

View file

@ -66,7 +66,6 @@ export default class {
} }
if (found.application.app_id !== appId) { if (found.application.app_id !== appId) {
console.log(found, appId)
throw new Error("requested user does not belong to requestor application") throw new Error("requested user does not belong to requestor application")
} }
return found return found
@ -75,4 +74,8 @@ export default class {
async IsApplicationUser(userId: string, entityManager = this.DB): Promise<ApplicationUser | null> { async IsApplicationUser(userId: string, entityManager = this.DB): Promise<ApplicationUser | null> {
return await entityManager.getRepository(ApplicationUser).findOne({ where: { user: { user_id: userId } } }) return await entityManager.getRepository(ApplicationUser).findOne({ where: { user: { user_id: userId } } })
} }
async IsApplicationOwner(userId: string, entityManager = this.DB) {
return entityManager.getRepository(Application).findOne({ where: { owner: { user_id: userId } } })
}
} }

View file

@ -1,6 +1,7 @@
import { Entity, PrimaryGeneratedColumn, Column, Index, Check, ManyToOne, JoinColumn, CreateDateColumn, UpdateDateColumn } from "typeorm" import { Entity, PrimaryGeneratedColumn, Column, Index, Check, ManyToOne, JoinColumn, CreateDateColumn, UpdateDateColumn } from "typeorm"
import { Product } from "./Product.js" import { Product } from "./Product.js"
import { User } from "./User.js" import { User } from "./User.js"
import { Application } from "./Application.js"
@Entity() @Entity()
export class UserReceivingInvoice { export class UserReceivingInvoice {
@ -37,6 +38,9 @@ export class UserReceivingInvoice {
@ManyToOne(type => User, { eager: true }) @ManyToOne(type => User, { eager: true })
payer: User | null payer: User | null
@ManyToOne(type => Application, { eager: true })
linkedApplication: Application | null
@CreateDateColumn() @CreateDateColumn()
created_at: Date created_at: Date

View file

@ -1,4 +1,4 @@
import { DataSource, EntityManager, MoreThan, MoreThanOrEqual } from "typeorm" import { DataSource, EntityManager, MoreThan, MoreThanOrEqual, TransactionAlreadyStartedError } from "typeorm"
import crypto from 'crypto'; import crypto from 'crypto';
import NewDB, { DbSettings, LoadDbSettingsFromEnv } from "./db.js" import NewDB, { DbSettings, LoadDbSettingsFromEnv } from "./db.js"
import { User } from "./entity/User.js" import { User } from "./entity/User.js"
@ -29,6 +29,7 @@ export default class {
applicationStorage: ApplicationStorage applicationStorage: ApplicationStorage
userStorage: UserStorage userStorage: UserStorage
paymentStorage: PaymentStorage paymentStorage: PaymentStorage
pendingTx: boolean
constructor(settings: StorageSettings) { constructor(settings: StorageSettings) {
this.settings = settings this.settings = settings
} }
@ -40,6 +41,24 @@ export default class {
this.paymentStorage = new PaymentStorage(this.DB, this.userStorage) this.paymentStorage = new PaymentStorage(this.DB, this.userStorage)
} }
StartTransaction(exec: (entityManager: EntityManager) => Promise<void>) { StartTransaction(exec: (entityManager: EntityManager) => Promise<void>) {
return this.DB.transaction(exec) if (this.pendingTx) {
throw new Error("cannot start transaction")
}
this.pendingTx = true
console.log("starting tx")
return this.DB.transaction(async tx => {
try {
await exec(tx)
console.log("tx done")
this.pendingTx = false
} catch (err) {
console.log("tx err")
this.pendingTx = false
throw err
}
})
} }
} }

View file

@ -12,7 +12,8 @@ import UserStorage from './userStorage.js';
import { AddressReceivingTransaction } from './entity/AddressReceivingTransaction.js'; import { AddressReceivingTransaction } from './entity/AddressReceivingTransaction.js';
import { UserInvoicePayment } from './entity/UserInvoicePayment.js'; import { UserInvoicePayment } from './entity/UserInvoicePayment.js';
import { UserToUserPayment } from './entity/UserToUserPayment.js'; import { UserToUserPayment } from './entity/UserToUserPayment.js';
export type InboundOptionals = { product?: Product, callbackUrl?: string, expiry: number, expectedPayer?: User } import { Application } from './entity/Application.js';
export type InboundOptionals = { product?: Product, callbackUrl?: string, expiry: number, expectedPayer?: User, linkedApplication?: Application }
export const defaultInvoiceExpiry = 60 * 60 export const defaultInvoiceExpiry = 60 * 60
export default class { export default class {
DB: DataSource | EntityManager DB: DataSource | EntityManager
@ -83,7 +84,8 @@ export default class {
user: user, user: user,
product: options.product, product: options.product,
expires_at_unix: Math.floor(Date.now() / 1000) + options.expiry, expires_at_unix: Math.floor(Date.now() / 1000) + options.expiry,
payer: options.expectedPayer payer: options.expectedPayer,
linkedApplication: options.linkedApplication
}) })
return entityManager.getRepository(UserReceivingInvoice).save(newUserInvoice) return entityManager.getRepository(UserReceivingInvoice).save(newUserInvoice)
} }

View file

@ -75,6 +75,7 @@ export default class {
} }
} }
async UnlockUser(userId: string, entityManager = this.DB) { async UnlockUser(userId: string, entityManager = this.DB) {
console.log("unlocking", userId)
const res = await entityManager.getRepository(User).update({ const res = await entityManager.getRepository(User).update({
user_id: userId user_id: userId
}, { locked: false }) }, { locked: false })
@ -102,4 +103,9 @@ export default class {
throw new Error("unaffected balance decrement for " + userId) // TODO: fix logs doxing throw new Error("unaffected balance decrement for " + userId) // TODO: fix logs doxing
} }
} }
async UpdateUser(userId: string, update: Partial<User>, entityManager = this.DB) {
const user = await this.GetUser(userId, entityManager)
await entityManager.getRepository(User).update(user.serial_id, update)
}
} }