This commit is contained in:
hatim 2023-05-09 17:08:05 +02:00
parent 85543d316d
commit 6382cce337
17 changed files with 2409 additions and 2123 deletions

File diff suppressed because it is too large Load diff

View file

@ -65,6 +65,20 @@ 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.SetMockInvoiceAsPaid) throw new Error('method: SetMockInvoiceAsPaid is not implemented')
app.post('/api/admin/lnd/mock/invoice/paid', async (req, res) => {
try {
if (!methods.SetMockInvoiceAsPaid) throw new Error('method: SetMockInvoiceAsPaid is not implemented')
const authContext = await opts.AdminAuthGuard(req.headers['authorization'])
const request = req.body
const error = Types.SetMockInvoiceAsPaidRequestValidate(request)
if (error !== null) return logErrorAndReturnResponse(error, 'invalid request body', res, logger)
const query = req.query
const params = req.params
await methods.SetMockInvoiceAsPaid({ ...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.AddApp) throw new Error('method: AddApp is not implemented')
app.post('/api/admin/app/add', async (req, res) => {
try {

View file

@ -51,6 +51,17 @@ export default (params: ClientParams) => ({
}
return { status: 'ERROR', reason: 'invalid response' }
},
SetMockInvoiceAsPaid: async (request: Types.SetMockInvoiceAsPaidRequest): Promise<ResultError | ({ status: 'OK' })> => {
const auth = await params.retrieveAdminAuth()
if (auth === null) throw new Error('retrieveAdminAuth() returned null')
let finalRoute = '/api/admin/lnd/mock/invoice/paid'
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' }
},
AddApp: async (request: Types.AddAppRequest): Promise<ResultError | ({ status: 'OK' }& Types.AddAppResponse)> => {
const auth = await params.retrieveAdminAuth()
if (auth === null) throw new Error('retrieveAdminAuth() returned null')

File diff suppressed because it is too large Load diff

View file

@ -88,6 +88,12 @@ service LightningPub {
option (http_route) = "/api/admin/lnd/getinfo";
};
rpc SetMockInvoiceAsPaid(structs.SetMockInvoiceAsPaidRequest) returns (structs.Empty) {
option (auth_type) = "Admin";
option (http_method) = "post";
option (http_route) = "/api/admin/lnd/mock/invoice/paid";
}
// <App>
rpc AddApp(structs.AddAppRequest) returns (structs.AddAppResponse) {

View file

@ -16,6 +16,11 @@ message LndGetInfoRequest {
int64 nodeId = 1;
}
message SetMockInvoiceAsPaidRequest {
string invoice = 1;
int64 amount =2;
}
message LndGetInfoResponse {
string alias = 1;
}

View file

@ -9,4 +9,9 @@ export const EnvMustBeInteger = (name: string): number => {
throw new Error(`${name} ENV must be an integer number`);
}
return +env
}
export const EnvCanBeBoolean = (name: string): boolean => {
const env = process.env[name]
if (!env) return false
return env.toLowerCase() === 'true'
}

View file

@ -1,11 +1,11 @@
import 'dotenv/config' // TODO - test env
import { expect } from 'chai'
import LndHandler, { LoadLndSettingsFromEnv } from './index.js'
import * as Types from '../../../proto/autogenerated/ts/types.js';
let lnd: LndHandler
import NewLightningHandler, { LightningHandler, LoadLndSettingsFromEnv } from '../lnd/index.js'
let lnd: LightningHandler
export const ignore = true
export const setup = async () => {
lnd = new LndHandler(LoadLndSettingsFromEnv(true), console.log, console.log)
lnd = NewLightningHandler(LoadLndSettingsFromEnv(true), console.log, console.log)
await lnd.Warmup()
}
export const teardown = () => {

View file

@ -1,239 +1,39 @@
//const grpc = require('@grpc/grpc-js');
import { credentials, Metadata } from '@grpc/grpc-js'
import { GrpcTransport } from "@protobuf-ts/grpc-transport";
import fs from 'fs'
import * as Types from '../../../proto/autogenerated/ts/types.js'
import { LightningClient } from '../../../proto/lnd/lightning.client.js'
import { InvoicesClient } from '../../../proto/lnd/invoices.client.js'
import { RouterClient } from '../../../proto/lnd/router.client.js'
import { GetInfoResponse, AddressType, NewAddressResponse, AddInvoiceResponse, Invoice_InvoiceState, PayReq, Payment_PaymentStatus, Payment, PaymentFailureReason } from '../../../proto/lnd/lightning.js'
import { OpenChannelReq } from './openChannelReq.js';
import { AddInvoiceReq } from './addInvoiceReq.js';
import { PayInvoiceReq } from './payInvoiceReq.js';
import { SendCoinsReq } from './sendCoinsReq.js';
import { EnvMustBeInteger, EnvMustBeNonEmptyString } from '../helpers/envParser.js';
const DeadLineMetadata = (deadline = 10 * 1000) => ({ deadline: Date.now() + deadline })
export type LndSettings = {
lndAddr: string
lndCertPath: string
lndMacaroonPath: string
feeRateLimit: number
feeFixedLimit: number
}
import { GetInfoResponse, NewAddressResponse, AddInvoiceResponse, PayReq, Payment, SendCoinsResponse, EstimateFeeResponse } from '../../../proto/lnd/lightning.js'
import { EnvMustBeNonEmptyString, EnvMustBeInteger, EnvCanBeBoolean } from '../helpers/envParser.js'
import { AddressPaidCb, DecodedInvoice, Invoice, InvoicePaidCb, LndSettings, NodeInfo, PaidInvoice } from './settings.js'
import LND from './lnd.js'
import MockLnd from './mock.js'
export const LoadLndSettingsFromEnv = (test = false): LndSettings => {
const lndAddr = EnvMustBeNonEmptyString("LND_ADDRESS")
const lndCertPath = EnvMustBeNonEmptyString("LND_CERT_PATH")
const lndMacaroonPath = EnvMustBeNonEmptyString("LND_MACAROON_PATH")
const feeRateLimit = EnvMustBeInteger("LIMIT_FEE_RATE_MILLISATS") / 1000
const feeFixedLimit = EnvMustBeInteger("LIMIT_FEE_FIXED_SATS")
return { lndAddr, lndCertPath, lndMacaroonPath, feeRateLimit, feeFixedLimit }
const mockLnd = EnvCanBeBoolean("MOCK_LND")
return { lndAddr, lndCertPath, lndMacaroonPath, feeRateLimit, feeFixedLimit, mockLnd }
}
type TxOutput = {
hash: string
index: number
}
export type AddressPaidCb = (txOutput: TxOutput, address: string, amount: number) => void
export type InvoicePaidCb = (paymentRequest: string, amount: number) => void
export default class {
lightning: LightningClient
invoices: InvoicesClient
router: RouterClient
settings: LndSettings
ready = false
latestKnownBlockHeigh = 0
latestKnownSettleIndex = 0
abortController = new AbortController()
addressPaidCb: AddressPaidCb
invoicePaidCb: InvoicePaidCb
constructor(settings: LndSettings, addressPaidCb: AddressPaidCb, invoicePaidCb: InvoicePaidCb) {
this.settings = settings
this.addressPaidCb = addressPaidCb
this.invoicePaidCb = invoicePaidCb
const { lndAddr, lndCertPath, lndMacaroonPath } = this.settings
const lndCert = fs.readFileSync(lndCertPath);
const macaroon = fs.readFileSync(lndMacaroonPath).toString('hex');
const sslCreds = credentials.createSsl(lndCert);
const macaroonCreds = credentials.createFromMetadataGenerator(
function (args: any, callback: any) {
let metadata = new Metadata();
metadata.add('macaroon', macaroon);
callback(null, metadata);
},
);
const creds = credentials.combineChannelCredentials(
sslCreds,
macaroonCreds,
);
const transport = new GrpcTransport({ host: lndAddr, channelCredentials: creds })
this.lightning = new LightningClient(transport)
this.invoices = new InvoicesClient(transport)
this.router = new RouterClient(transport)
this.SubscribeAddressPaid()
this.SubscribeInvoicePaid()
}
Stop() {
this.abortController.abort()
}
async Warmup() { this.ready = true }
async GetInfo(): Promise<GetInfoResponse> {
const res = await this.lightning.getInfo({}, DeadLineMetadata())
return res.response
}
async Health() {
if (!this.ready) {
throw new Error("not ready")
}
const info = await this.GetInfo()
if (!info.syncedToChain || !info.syncedToGraph) {
throw new Error("not ready")
}
}
checkReady() {
if (!this.ready) throw new Error("lnd not ready, warmup required before usage")
}
SubscribeAddressPaid() {
const stream = this.lightning.subscribeTransactions({
account: "",
endHeight: 0,
startHeight: this.latestKnownBlockHeigh,
}, { abort: this.abortController.signal })
stream.responses.onMessage(tx => {
if (tx.blockHeight > this.latestKnownBlockHeigh) {
this.latestKnownBlockHeigh = tx.blockHeight
}
if (tx.numConfirmations > 0) {
tx.outputDetails.forEach(output => {
if (output.isOurAddress) {
this.addressPaidCb({ hash: tx.txHash, index: Number(output.outputIndex) }, output.address, Number(output.amount))
}
})
}
})
stream.responses.onError(error => {
// TODO...
})
}
SubscribeInvoicePaid() {
const stream = this.lightning.subscribeInvoices({
settleIndex: BigInt(this.latestKnownSettleIndex),
addIndex: 0n,
}, { abort: this.abortController.signal })
stream.responses.onMessage(invoice => {
if (invoice.state === Invoice_InvoiceState.SETTLED) {
this.latestKnownSettleIndex = Number(invoice.settleIndex)
this.invoicePaidCb(invoice.paymentRequest, Number(invoice.amtPaidSat))
}
})
stream.responses.onError(error => {
// TODO...
})
}
async NewAddress(addressType: Types.AddressType): Promise<NewAddressResponse> {
this.checkReady()
let lndAddressType: AddressType
switch (addressType) {
case Types.AddressType.NESTED_PUBKEY_HASH:
lndAddressType = AddressType.NESTED_PUBKEY_HASH
break;
case Types.AddressType.WITNESS_PUBKEY_HASH:
lndAddressType = AddressType.WITNESS_PUBKEY_HASH
break;
case Types.AddressType.TAPROOT_PUBKEY:
lndAddressType = AddressType.TAPROOT_PUBKEY
break;
default:
throw new Error("unknown address type " + addressType)
}
const res = await this.lightning.newAddress({ account: "", type: lndAddressType }, DeadLineMetadata())
return res.response
}
async NewInvoice(value: number, memo: string, expiry: number): Promise<AddInvoiceResponse> {
this.checkReady()
const res = await this.lightning.addInvoice(AddInvoiceReq(value, memo, expiry), DeadLineMetadata())
return res.response
}
async DecodeInvoice(paymentRequest: string): Promise<PayReq> {
const res = await this.lightning.decodePayReq({ payReq: paymentRequest }, DeadLineMetadata())
return res.response
}
GetFeeLimitAmount(amount: number) {
return Math.ceil(amount * this.settings.feeRateLimit + this.settings.feeFixedLimit);
}
GetMaxWithinLimit(amount: number) {
return Math.max(0, Math.floor(amount * (1 - this.settings.feeRateLimit) - this.settings.feeFixedLimit))
}
async PayInvoice(invoice: string, amount: number, feeLimit: number): Promise<Payment> {
this.checkReady()
const abortController = new AbortController()
const req = PayInvoiceReq(invoice, amount, feeLimit)
console.log(req)
const stream = this.router.sendPaymentV2(req, { abort: abortController.signal })
return new Promise((res, rej) => {
stream.responses.onError(error => {
rej(error)
})
stream.responses.onMessage(payment => {
console.log(payment)
switch (payment.status) {
case Payment_PaymentStatus.FAILED:
rej(PaymentFailureReason[payment.failureReason])
return
case Payment_PaymentStatus.SUCCEEDED:
res(payment)
}
})
})
}
async EstimateChainFees(address: string, amount: number, targetConf: number) {
this.checkReady()
const res = await this.lightning.estimateFee({
addrToAmount: { [address]: BigInt(amount) },
minConfs: 1,
spendUnconfirmed: false,
targetConf: targetConf
})
return res.response
}
async PayAddress(address: string, amount: number, satPerVByte: number, label = "") {
this.checkReady()
const res = await this.lightning.sendCoins(SendCoinsReq(address, amount, satPerVByte, label), DeadLineMetadata())
return res.response
}
async OpenChannel(destination: string, closeAddress: string, fundingAmount: number, pushSats: number): Promise<string> {
this.checkReady()
const abortController = new AbortController()
const req = OpenChannelReq(destination, closeAddress, fundingAmount, pushSats)
const stream = this.lightning.openChannel(req, { abort: abortController.signal })
return new Promise((res, rej) => {
stream.responses.onMessage(message => {
switch (message.update.oneofKind) {
case 'chanPending':
abortController.abort()
res(Buffer.from(message.pendingChanId).toString('base64'))
break
default:
abortController.abort()
rej("unexpected state response: " + message.update.oneofKind)
}
})
stream.responses.onError(error => {
rej(error)
})
})
}
export interface LightningHandler {
Stop(): void
Warmup(): Promise<void>
GetInfo(): Promise<NodeInfo>
Health(): Promise<void>
NewAddress(addressType: Types.AddressType): Promise<NewAddressResponse>
NewInvoice(value: number, memo: string, expiry: number): Promise<Invoice>
DecodeInvoice(paymentRequest: string): Promise<DecodedInvoice>
GetFeeLimitAmount(amount: number): number
GetMaxWithinLimit(amount: number): number
PayInvoice(invoice: string, amount: number, feeLimit: number): Promise<PaidInvoice>
EstimateChainFees(address: string, amount: number, targetConf: number): Promise<EstimateFeeResponse>
PayAddress(address: string, amount: number, satPerVByte: number, label?: string): Promise<SendCoinsResponse>
OpenChannel(destination: string, closeAddress: string, fundingAmount: number, pushSats: number): Promise<string>
SetMockInvoiceAsPaid(invoice: string, amount: number): Promise<void>
}
export default (settings: LndSettings, addressPaidCb: AddressPaidCb, invoicePaidCb: InvoicePaidCb): LightningHandler => {
if (settings.mockLnd) {
return new MockLnd(settings, addressPaidCb, invoicePaidCb)
} else {
return new LND(settings, addressPaidCb, invoicePaidCb)
}
}

222
src/services/lnd/lnd.ts Normal file
View file

@ -0,0 +1,222 @@
//const grpc = require('@grpc/grpc-js');
import { credentials, Metadata } from '@grpc/grpc-js'
import { GrpcTransport } from "@protobuf-ts/grpc-transport";
import fs from 'fs'
import * as Types from '../../../proto/autogenerated/ts/types.js'
import { LightningClient } from '../../../proto/lnd/lightning.client.js'
import { InvoicesClient } from '../../../proto/lnd/invoices.client.js'
import { RouterClient } from '../../../proto/lnd/router.client.js'
import { GetInfoResponse, AddressType, NewAddressResponse, AddInvoiceResponse, Invoice_InvoiceState, PayReq, Payment_PaymentStatus, Payment, PaymentFailureReason, SendCoinsResponse, EstimateFeeResponse } from '../../../proto/lnd/lightning.js'
import { OpenChannelReq } from './openChannelReq.js';
import { AddInvoiceReq } from './addInvoiceReq.js';
import { PayInvoiceReq } from './payInvoiceReq.js';
import { SendCoinsReq } from './sendCoinsReq.js';
import { LndSettings, AddressPaidCb, InvoicePaidCb, NodeInfo, Invoice, DecodedInvoice, PaidInvoice } from './settings.js';
const DeadLineMetadata = (deadline = 10 * 1000) => ({ deadline: Date.now() + deadline })
export default class {
lightning: LightningClient
invoices: InvoicesClient
router: RouterClient
settings: LndSettings
ready = false
latestKnownBlockHeigh = 0
latestKnownSettleIndex = 0
abortController = new AbortController()
addressPaidCb: AddressPaidCb
invoicePaidCb: InvoicePaidCb
constructor(settings: LndSettings, addressPaidCb: AddressPaidCb, invoicePaidCb: InvoicePaidCb) {
this.settings = settings
this.addressPaidCb = addressPaidCb
this.invoicePaidCb = invoicePaidCb
const { lndAddr, lndCertPath, lndMacaroonPath } = this.settings
const lndCert = fs.readFileSync(lndCertPath);
const macaroon = fs.readFileSync(lndMacaroonPath).toString('hex');
const sslCreds = credentials.createSsl(lndCert);
const macaroonCreds = credentials.createFromMetadataGenerator(
function (args: any, callback: any) {
let metadata = new Metadata();
metadata.add('macaroon', macaroon);
callback(null, metadata);
},
);
const creds = credentials.combineChannelCredentials(
sslCreds,
macaroonCreds,
);
const transport = new GrpcTransport({ host: lndAddr, channelCredentials: creds })
this.lightning = new LightningClient(transport)
this.invoices = new InvoicesClient(transport)
this.router = new RouterClient(transport)
this.SubscribeAddressPaid()
this.SubscribeInvoicePaid()
}
SetMockInvoiceAsPaid(invoice: string, amount: number): Promise<void> {
throw new Error("SetMockInvoiceAsPaid only available in mock mode")
}
Stop() {
this.abortController.abort()
}
async Warmup() { this.ready = true }
async GetInfo(): Promise<NodeInfo> {
const res = await this.lightning.getInfo({}, DeadLineMetadata())
return res.response
}
async Health(): Promise<void> {
if (!this.ready) {
throw new Error("not ready")
}
const info = await this.GetInfo()
if (!info.syncedToChain || !info.syncedToGraph) {
throw new Error("not ready")
}
}
checkReady(): void {
if (!this.ready) throw new Error("lnd not ready, warmup required before usage")
}
SubscribeAddressPaid(): void {
const stream = this.lightning.subscribeTransactions({
account: "",
endHeight: 0,
startHeight: this.latestKnownBlockHeigh,
}, { abort: this.abortController.signal })
stream.responses.onMessage(tx => {
if (tx.blockHeight > this.latestKnownBlockHeigh) {
this.latestKnownBlockHeigh = tx.blockHeight
}
if (tx.numConfirmations > 0) {
tx.outputDetails.forEach(output => {
if (output.isOurAddress) {
this.addressPaidCb({ hash: tx.txHash, index: Number(output.outputIndex) }, output.address, Number(output.amount))
}
})
}
})
stream.responses.onError(error => {
// TODO...
})
}
SubscribeInvoicePaid(): void {
const stream = this.lightning.subscribeInvoices({
settleIndex: BigInt(this.latestKnownSettleIndex),
addIndex: 0n,
}, { abort: this.abortController.signal })
stream.responses.onMessage(invoice => {
if (invoice.state === Invoice_InvoiceState.SETTLED) {
this.latestKnownSettleIndex = Number(invoice.settleIndex)
this.invoicePaidCb(invoice.paymentRequest, Number(invoice.amtPaidSat))
}
})
stream.responses.onError(error => {
// TODO...
})
}
async NewAddress(addressType: Types.AddressType): Promise<NewAddressResponse> {
this.checkReady()
let lndAddressType: AddressType
switch (addressType) {
case Types.AddressType.NESTED_PUBKEY_HASH:
lndAddressType = AddressType.NESTED_PUBKEY_HASH
break;
case Types.AddressType.WITNESS_PUBKEY_HASH:
lndAddressType = AddressType.WITNESS_PUBKEY_HASH
break;
case Types.AddressType.TAPROOT_PUBKEY:
lndAddressType = AddressType.TAPROOT_PUBKEY
break;
default:
throw new Error("unknown address type " + addressType)
}
const res = await this.lightning.newAddress({ account: "", type: lndAddressType }, DeadLineMetadata())
return res.response
}
async NewInvoice(value: number, memo: string, expiry: number): Promise<Invoice> {
this.checkReady()
const res = await this.lightning.addInvoice(AddInvoiceReq(value, memo, expiry), DeadLineMetadata())
return { payRequest: res.response.paymentRequest }
}
async DecodeInvoice(paymentRequest: string): Promise<DecodedInvoice> {
const res = await this.lightning.decodePayReq({ payReq: paymentRequest }, DeadLineMetadata())
return { numSatoshis: Number(res.response.numSatoshis) }
}
GetFeeLimitAmount(amount: number): number {
return Math.ceil(amount * this.settings.feeRateLimit + this.settings.feeFixedLimit);
}
GetMaxWithinLimit(amount: number): number {
return Math.max(0, Math.floor(amount * (1 - this.settings.feeRateLimit) - this.settings.feeFixedLimit))
}
async PayInvoice(invoice: string, amount: number, feeLimit: number): Promise<PaidInvoice> {
this.checkReady()
const abortController = new AbortController()
const req = PayInvoiceReq(invoice, amount, feeLimit)
console.log(req)
const stream = this.router.sendPaymentV2(req, { abort: abortController.signal })
return new Promise((res, rej) => {
stream.responses.onError(error => {
rej(error)
})
stream.responses.onMessage(payment => {
console.log(payment)
switch (payment.status) {
case Payment_PaymentStatus.FAILED:
rej(PaymentFailureReason[payment.failureReason])
return
case Payment_PaymentStatus.SUCCEEDED:
res({ feeSat: Number(payment.feeSat), valueSat: Number(payment.valueSat), paymentPreimage: payment.paymentPreimage })
}
})
})
}
async EstimateChainFees(address: string, amount: number, targetConf: number): Promise<EstimateFeeResponse> {
this.checkReady()
const res = await this.lightning.estimateFee({
addrToAmount: { [address]: BigInt(amount) },
minConfs: 1,
spendUnconfirmed: false,
targetConf: targetConf
})
return res.response
}
async PayAddress(address: string, amount: number, satPerVByte: number, label = ""): Promise<SendCoinsResponse> {
this.checkReady()
const res = await this.lightning.sendCoins(SendCoinsReq(address, amount, satPerVByte, label), DeadLineMetadata())
return res.response
}
async OpenChannel(destination: string, closeAddress: string, fundingAmount: number, pushSats: number): Promise<string> {
this.checkReady()
const abortController = new AbortController()
const req = OpenChannelReq(destination, closeAddress, fundingAmount, pushSats)
const stream = this.lightning.openChannel(req, { abort: abortController.signal })
return new Promise((res, rej) => {
stream.responses.onMessage(message => {
switch (message.update.oneofKind) {
case 'chanPending':
abortController.abort()
res(Buffer.from(message.pendingChanId).toString('base64'))
break
default:
abortController.abort()
rej("unexpected state response: " + message.update.oneofKind)
}
})
stream.responses.onError(error => {
rej(error)
})
})
}
}

98
src/services/lnd/mock.ts Normal file
View file

@ -0,0 +1,98 @@
//const grpc = require('@grpc/grpc-js');
import { credentials, Metadata } from '@grpc/grpc-js'
import { GrpcTransport } from "@protobuf-ts/grpc-transport";
import fs from 'fs'
import crypto from 'crypto'
import * as Types from '../../../proto/autogenerated/ts/types.js'
import { LightningClient } from '../../../proto/lnd/lightning.client.js'
import { InvoicesClient } from '../../../proto/lnd/invoices.client.js'
import { RouterClient } from '../../../proto/lnd/router.client.js'
import { GetInfoResponse, AddressType, NewAddressResponse, AddInvoiceResponse, Invoice_InvoiceState, PayReq, Payment_PaymentStatus, Payment, PaymentFailureReason, SendCoinsResponse, EstimateFeeResponse } from '../../../proto/lnd/lightning.js'
import { OpenChannelReq } from './openChannelReq.js';
import { AddInvoiceReq } from './addInvoiceReq.js';
import { PayInvoiceReq } from './payInvoiceReq.js';
import { SendCoinsReq } from './sendCoinsReq.js';
import { LndSettings, AddressPaidCb, InvoicePaidCb, NodeInfo, Invoice, DecodedInvoice, PaidInvoice } from './settings.js';
export default class {
invoicesAwaiting: Record<string /* invoice */, { value: number, memo: string, expiryUnix: number }>
settings: LndSettings
abortController = new AbortController()
addressPaidCb: AddressPaidCb
invoicePaidCb: InvoicePaidCb
constructor(settings: LndSettings, addressPaidCb: AddressPaidCb, invoicePaidCb: InvoicePaidCb) {
this.settings = settings
this.addressPaidCb = addressPaidCb
this.invoicePaidCb = invoicePaidCb
}
async SetMockInvoiceAsPaid(invoice: string, amount: number): Promise<void> {
const decoded = await this.DecodeInvoice(invoice)
if (decoded.numSatoshis && amount) {
throw new Error("non zero amount provided to pay invoice but invoice has value already")
}
this.invoicePaidCb(invoice, decoded.numSatoshis || amount)
delete this.invoicesAwaiting[invoice]
}
Stop() { }
async Warmup() { }
async GetInfo(): Promise<NodeInfo> {
return { alias: "mock", syncedToChain: true, syncedToGraph: true }
}
async Health(): Promise<void> { }
async NewAddress(addressType: Types.AddressType): Promise<NewAddressResponse> {
throw new Error("NewAddress disabled in mock mode")
}
async NewInvoice(value: number, memo: string, expiry: number): Promise<Invoice> {
const mockInvoice = "lnbcrtmock" + crypto.randomBytes(32).toString('hex')
this.invoicesAwaiting[mockInvoice] = { value, memo, expiryUnix: expiry + Date.now() / 1000 }
return { payRequest: mockInvoice }
}
async DecodeInvoice(paymentRequest: string): Promise<DecodedInvoice> {
const i = this.invoicesAwaiting[paymentRequest]
if (!i) {
throw new Error("invoice not found")
}
return { numSatoshis: i.value }
}
GetFeeLimitAmount(amount: number): number {
return Math.ceil(amount * this.settings.feeRateLimit + this.settings.feeFixedLimit);
}
GetMaxWithinLimit(amount: number): number {
return Math.max(0, Math.floor(amount * (1 - this.settings.feeRateLimit) - this.settings.feeFixedLimit))
}
async PayInvoice(invoice: string, amount: number, feeLimit: number): Promise<PaidInvoice> {
if (!invoice.startsWith('lnbcrtmock')) {
throw new Error("invalid mock invoice provided for payment")
}
const amt = invoice.substring('lnbcrtmock'.length)
if (isNaN(+amt)) {
throw new Error("invalid mock invoice provided for payment")
}
return { feeSat: 0, paymentPreimage: "all_good", valueSat: +amt || amount }
}
async EstimateChainFees(address: string, amount: number, targetConf: number): Promise<EstimateFeeResponse> {
throw new Error("EstimateChainFees disabled in mock mode")
}
async PayAddress(address: string, amount: number, satPerVByte: number, label = ""): Promise<SendCoinsResponse> {
throw new Error("PayAddress disabled in mock mode")
}
async OpenChannel(destination: string, closeAddress: string, fundingAmount: number, pushSats: number): Promise<string> {
throw new Error("OpenChannel disabled in mock mode")
}
}

View file

@ -0,0 +1,32 @@
export type LndSettings = {
lndAddr: string
lndCertPath: string
lndMacaroonPath: string
feeRateLimit: number
feeFixedLimit: number
mockLnd: boolean
}
type TxOutput = {
hash: string
index: number
}
export type AddressPaidCb = (txOutput: TxOutput, address: string, amount: number) => void
export type InvoicePaidCb = (paymentRequest: string, amount: number) => void
export type NodeInfo = {
alias: string
syncedToChain: boolean
syncedToGraph: boolean
}
export type Invoice = {
payRequest: string
}
export type DecodedInvoice = {
numSatoshis: number
}
export type PaidInvoice = {
feeSat: number
valueSat: number
paymentPreimage: string
}

View file

@ -1,13 +1,14 @@
import fetch from "node-fetch"
import Storage, { LoadStorageSettingsFromEnv } from '../storage/index.js'
import * as Types from '../../../proto/autogenerated/ts/types.js'
import LND, { AddressPaidCb, InvoicePaidCb, LoadLndSettingsFromEnv } from '../lnd/index.js'
import { EnvMustBeInteger, EnvMustBeNonEmptyString } from '../helpers/envParser.js'
import ProductManager from './productManager.js'
import ApplicationManager from './applicationManager.js'
import UserManager from './userManager.js'
import PaymentManager from './paymentManager.js'
import { MainSettings } from './settings.js'
import NewLightningHandler, { LoadLndSettingsFromEnv, LightningHandler } from "../lnd/index.js"
import { AddressPaidCb, InvoicePaidCb } from "../lnd/settings.js"
export const LoadMainSettingsFromEnv = (test = false): MainSettings => {
return {
lndSettings: LoadLndSettingsFromEnv(test),
@ -33,7 +34,7 @@ type UserOperationsSub = {
export default class {
storage: Storage
lnd: LND
lnd: LightningHandler
settings: MainSettings
userOperationsSub: UserOperationsSub | null = null
productManager: ProductManager
@ -44,7 +45,7 @@ export default class {
constructor(settings: MainSettings) {
this.settings = settings
this.storage = new Storage(settings.storageSettings)
this.lnd = new LND(settings.lndSettings, this.addressPaidCb, this.invoicePaidCb)
this.lnd = NewLightningHandler(settings.lndSettings, this.addressPaidCb, this.invoicePaidCb)
this.userManager = new UserManager(this.storage, this.settings)
this.paymentManager = new PaymentManager(this.storage, this.lnd, this.settings)

View file

@ -1,9 +1,9 @@
import { bech32 } from 'bech32'
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'
import { InboundOptionals, defaultInvoiceExpiry } from '../storage/paymentStorage.js'
import { LightningHandler } from '../lnd/index.js'
interface UserOperationInfo {
serial_id: number
paid_amount: number
@ -12,10 +12,11 @@ interface UserOperationInfo {
const defaultLnurlPayMetadata = '[["text/plain", "lnurl pay to Lightning.pub"]]'
export default class {
storage: Storage
settings: MainSettings
lnd: LND
constructor(storage: Storage, lnd: LND, settings: MainSettings) {
lnd: LightningHandler
constructor(storage: Storage, lnd: LightningHandler, settings: MainSettings) {
this.storage = storage
this.settings = settings
this.lnd = lnd
@ -37,6 +38,10 @@ export default class {
}
}
async SetMockInvoiceAsPaid(req: Types.SetMockInvoiceAsPaidRequest) {
await this.lnd.SetMockInvoiceAsPaid(req.invoice, req.amount)
}
async NewAddress(userId: string, req: Types.NewAddressRequest): Promise<Types.NewAddressResponse> {
const res = await this.lnd.NewAddress(req.addressType)
const userAddress = await this.storage.paymentStorage.AddUserAddress(userId, res.address)
@ -48,7 +53,7 @@ export default class {
async NewInvoice(userId: string, req: Types.NewInvoiceRequest, options: InboundOptionals = { expiry: defaultInvoiceExpiry }): Promise<Types.NewInvoiceResponse> {
const user = await this.storage.userStorage.GetUser(userId)
const res = await this.lnd.NewInvoice(req.amountSats, req.memo, options.expiry)
const userInvoice = await this.storage.paymentStorage.AddUserInvoice(user, res.paymentRequest, options)
const userInvoice = await this.storage.paymentStorage.AddUserInvoice(user, res.payRequest, options)
return {
invoice: userInvoice.invoice
}
@ -77,10 +82,10 @@ export default class {
}
async PayInvoice(userId: string, req: Types.PayInvoiceRequest): Promise<Types.PayInvoiceResponse> {
const decoded = await this.lnd.DecodeInvoice(req.invoice)
if (decoded.numSatoshis !== 0n && req.amount !== 0) {
if (decoded.numSatoshis !== 0 && req.amount !== 0) {
throw new Error("invoice has value, do not provide amount the the request")
}
if (decoded.numSatoshis === 0n && req.amount === 0) {
if (decoded.numSatoshis === 0 && req.amount === 0) {
throw new Error("invoice has no value, an amount must be provided in the request")
}
const payAmount = req.amount !== 0 ? req.amount : Number(decoded.numSatoshis)

View file

@ -1,6 +1,6 @@
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'
import PaymentManager from './paymentManager.js'

View file

@ -1,5 +1,5 @@
import { StorageSettings } from '../storage/index.js'
import { LndSettings } from '../lnd/index.js'
import { LndSettings } from '../lnd/settings.js'
export type MainSettings = {
storageSettings: StorageSettings,
lndSettings: LndSettings,

View file

@ -8,6 +8,13 @@ export default (mainHandler: Main): Types.ServerMethods => {
const info = await mainHandler.lnd.GetInfo()
return { alias: info.alias }
},
SetMockInvoiceAsPaid: async (ctx, req) => {
const err = Types.SetMockInvoiceAsPaidRequestValidate(req, {
invoice_CustomCheck: invoice => invoice !== '',
})
if (err != null) throw new Error(err.message)
await mainHandler.paymentManager.SetMockInvoiceAsPaid(req)
},
AddUser: async (ctx, req) => {
const err = Types.AddUserRequestValidate(req, {
callbackUrl_CustomCheck: url => url.startsWith("http://") || url.startsWith("https://"),
@ -153,7 +160,7 @@ export default (mainHandler: Main): Types.ServerMethods => {
amount_CustomCheck: amount => amount > 0
})
if (err != null) throw new Error(err.message)
mainHandler.applicationManager.SendAppUserToAppUserPayment(ctx.app_id, req)
await mainHandler.applicationManager.SendAppUserToAppUserPayment(ctx.app_id, req)
},
SendAppUserToAppPayment: async (ctx, req) => {
const err = Types.SendAppUserToAppPaymentRequestValidate(req, {
@ -161,7 +168,7 @@ export default (mainHandler: Main): Types.ServerMethods => {
amount_CustomCheck: amount => amount > 0
})
if (err != null) throw new Error(err.message)
mainHandler.applicationManager.SendAppUserToAppPayment(ctx.app_id, req)
await mainHandler.applicationManager.SendAppUserToAppPayment(ctx.app_id, req)
}
}
}