mock lnd
This commit is contained in:
parent
85543d316d
commit
6382cce337
17 changed files with 2409 additions and 2123 deletions
File diff suppressed because it is too large
Load diff
|
|
@ -65,6 +65,20 @@ 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.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')
|
if (!opts.allowNotImplementedMethods && !methods.AddApp) throw new Error('method: AddApp is not implemented')
|
||||||
app.post('/api/admin/app/add', async (req, res) => {
|
app.post('/api/admin/app/add', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
|
|
|
||||||
|
|
@ -51,6 +51,17 @@ 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' })> => {
|
||||||
|
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)> => {
|
AddApp: async (request: Types.AddAppRequest): Promise<ResultError | ({ status: 'OK' }& Types.AddAppResponse)> => {
|
||||||
const auth = await params.retrieveAdminAuth()
|
const auth = await params.retrieveAdminAuth()
|
||||||
if (auth === null) throw new Error('retrieveAdminAuth() returned null')
|
if (auth === null) throw new Error('retrieveAdminAuth() returned null')
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -88,6 +88,12 @@ service LightningPub {
|
||||||
option (http_route) = "/api/admin/lnd/getinfo";
|
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>
|
// <App>
|
||||||
|
|
||||||
rpc AddApp(structs.AddAppRequest) returns (structs.AddAppResponse) {
|
rpc AddApp(structs.AddAppRequest) returns (structs.AddAppResponse) {
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,11 @@ message LndGetInfoRequest {
|
||||||
int64 nodeId = 1;
|
int64 nodeId = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
message SetMockInvoiceAsPaidRequest {
|
||||||
|
string invoice = 1;
|
||||||
|
int64 amount =2;
|
||||||
|
}
|
||||||
|
|
||||||
message LndGetInfoResponse {
|
message LndGetInfoResponse {
|
||||||
string alias = 1;
|
string alias = 1;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,3 +10,8 @@ export const EnvMustBeInteger = (name: string): number => {
|
||||||
}
|
}
|
||||||
return +env
|
return +env
|
||||||
}
|
}
|
||||||
|
export const EnvCanBeBoolean = (name: string): boolean => {
|
||||||
|
const env = process.env[name]
|
||||||
|
if (!env) return false
|
||||||
|
return env.toLowerCase() === 'true'
|
||||||
|
}
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
import 'dotenv/config' // TODO - test env
|
import 'dotenv/config' // TODO - test env
|
||||||
import { expect } from 'chai'
|
import { expect } from 'chai'
|
||||||
import LndHandler, { LoadLndSettingsFromEnv } from './index.js'
|
|
||||||
import * as Types from '../../../proto/autogenerated/ts/types.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 ignore = true
|
||||||
export const setup = async () => {
|
export const setup = async () => {
|
||||||
lnd = new LndHandler(LoadLndSettingsFromEnv(true), console.log, console.log)
|
lnd = NewLightningHandler(LoadLndSettingsFromEnv(true), console.log, console.log)
|
||||||
await lnd.Warmup()
|
await lnd.Warmup()
|
||||||
}
|
}
|
||||||
export const teardown = () => {
|
export const teardown = () => {
|
||||||
|
|
|
||||||
|
|
@ -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 * as Types from '../../../proto/autogenerated/ts/types.js'
|
||||||
import { LightningClient } from '../../../proto/lnd/lightning.client.js'
|
import { GetInfoResponse, NewAddressResponse, AddInvoiceResponse, PayReq, Payment, SendCoinsResponse, EstimateFeeResponse } from '../../../proto/lnd/lightning.js'
|
||||||
import { InvoicesClient } from '../../../proto/lnd/invoices.client.js'
|
import { EnvMustBeNonEmptyString, EnvMustBeInteger, EnvCanBeBoolean } from '../helpers/envParser.js'
|
||||||
import { RouterClient } from '../../../proto/lnd/router.client.js'
|
import { AddressPaidCb, DecodedInvoice, Invoice, InvoicePaidCb, LndSettings, NodeInfo, PaidInvoice } from './settings.js'
|
||||||
import { GetInfoResponse, AddressType, NewAddressResponse, AddInvoiceResponse, Invoice_InvoiceState, PayReq, Payment_PaymentStatus, Payment, PaymentFailureReason } from '../../../proto/lnd/lightning.js'
|
import LND from './lnd.js'
|
||||||
import { OpenChannelReq } from './openChannelReq.js';
|
import MockLnd from './mock.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
|
|
||||||
}
|
|
||||||
export const LoadLndSettingsFromEnv = (test = false): LndSettings => {
|
export const LoadLndSettingsFromEnv = (test = false): LndSettings => {
|
||||||
const lndAddr = EnvMustBeNonEmptyString("LND_ADDRESS")
|
const lndAddr = EnvMustBeNonEmptyString("LND_ADDRESS")
|
||||||
const lndCertPath = EnvMustBeNonEmptyString("LND_CERT_PATH")
|
const lndCertPath = EnvMustBeNonEmptyString("LND_CERT_PATH")
|
||||||
const lndMacaroonPath = EnvMustBeNonEmptyString("LND_MACAROON_PATH")
|
const lndMacaroonPath = EnvMustBeNonEmptyString("LND_MACAROON_PATH")
|
||||||
const feeRateLimit = EnvMustBeInteger("LIMIT_FEE_RATE_MILLISATS") / 1000
|
const feeRateLimit = EnvMustBeInteger("LIMIT_FEE_RATE_MILLISATS") / 1000
|
||||||
const feeFixedLimit = EnvMustBeInteger("LIMIT_FEE_FIXED_SATS")
|
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 = {
|
export interface LightningHandler {
|
||||||
hash: string
|
Stop(): void
|
||||||
index: number
|
Warmup(): Promise<void>
|
||||||
}
|
GetInfo(): Promise<NodeInfo>
|
||||||
export type AddressPaidCb = (txOutput: TxOutput, address: string, amount: number) => void
|
Health(): Promise<void>
|
||||||
export type InvoicePaidCb = (paymentRequest: string, amount: number) => void
|
NewAddress(addressType: Types.AddressType): Promise<NewAddressResponse>
|
||||||
export default class {
|
NewInvoice(value: number, memo: string, expiry: number): Promise<Invoice>
|
||||||
lightning: LightningClient
|
DecodeInvoice(paymentRequest: string): Promise<DecodedInvoice>
|
||||||
invoices: InvoicesClient
|
GetFeeLimitAmount(amount: number): number
|
||||||
router: RouterClient
|
GetMaxWithinLimit(amount: number): number
|
||||||
settings: LndSettings
|
PayInvoice(invoice: string, amount: number, feeLimit: number): Promise<PaidInvoice>
|
||||||
ready = false
|
EstimateChainFees(address: string, amount: number, targetConf: number): Promise<EstimateFeeResponse>
|
||||||
latestKnownBlockHeigh = 0
|
PayAddress(address: string, amount: number, satPerVByte: number, label?: string): Promise<SendCoinsResponse>
|
||||||
latestKnownSettleIndex = 0
|
OpenChannel(destination: string, closeAddress: string, fundingAmount: number, pushSats: number): Promise<string>
|
||||||
abortController = new AbortController()
|
SetMockInvoiceAsPaid(invoice: string, amount: number): Promise<void>
|
||||||
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 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
222
src/services/lnd/lnd.ts
Normal 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
98
src/services/lnd/mock.ts
Normal 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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
32
src/services/lnd/settings.ts
Normal file
32
src/services/lnd/settings.ts
Normal 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
|
||||||
|
}
|
||||||
|
|
@ -1,13 +1,14 @@
|
||||||
import fetch from "node-fetch"
|
import fetch from "node-fetch"
|
||||||
import Storage, { LoadStorageSettingsFromEnv } from '../storage/index.js'
|
import Storage, { LoadStorageSettingsFromEnv } from '../storage/index.js'
|
||||||
import * as Types from '../../../proto/autogenerated/ts/types.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 { EnvMustBeInteger, EnvMustBeNonEmptyString } from '../helpers/envParser.js'
|
||||||
import ProductManager from './productManager.js'
|
import ProductManager from './productManager.js'
|
||||||
import ApplicationManager from './applicationManager.js'
|
import ApplicationManager from './applicationManager.js'
|
||||||
import UserManager from './userManager.js'
|
import UserManager from './userManager.js'
|
||||||
import PaymentManager from './paymentManager.js'
|
import PaymentManager from './paymentManager.js'
|
||||||
import { MainSettings } from './settings.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 => {
|
export const LoadMainSettingsFromEnv = (test = false): MainSettings => {
|
||||||
return {
|
return {
|
||||||
lndSettings: LoadLndSettingsFromEnv(test),
|
lndSettings: LoadLndSettingsFromEnv(test),
|
||||||
|
|
@ -33,7 +34,7 @@ type UserOperationsSub = {
|
||||||
|
|
||||||
export default class {
|
export default class {
|
||||||
storage: Storage
|
storage: Storage
|
||||||
lnd: LND
|
lnd: LightningHandler
|
||||||
settings: MainSettings
|
settings: MainSettings
|
||||||
userOperationsSub: UserOperationsSub | null = null
|
userOperationsSub: UserOperationsSub | null = null
|
||||||
productManager: ProductManager
|
productManager: ProductManager
|
||||||
|
|
@ -44,7 +45,7 @@ export default class {
|
||||||
constructor(settings: MainSettings) {
|
constructor(settings: MainSettings) {
|
||||||
this.settings = settings
|
this.settings = settings
|
||||||
this.storage = new Storage(settings.storageSettings)
|
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.userManager = new UserManager(this.storage, this.settings)
|
||||||
this.paymentManager = new PaymentManager(this.storage, this.lnd, this.settings)
|
this.paymentManager = new PaymentManager(this.storage, this.lnd, this.settings)
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
import { bech32 } from 'bech32'
|
import { bech32 } from 'bech32'
|
||||||
import Storage from '../storage/index.js'
|
import Storage from '../storage/index.js'
|
||||||
import * as Types from '../../../proto/autogenerated/ts/types.js'
|
import * as Types from '../../../proto/autogenerated/ts/types.js'
|
||||||
import LND from '../lnd/index.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'
|
||||||
interface UserOperationInfo {
|
interface UserOperationInfo {
|
||||||
serial_id: number
|
serial_id: number
|
||||||
paid_amount: number
|
paid_amount: number
|
||||||
|
|
@ -12,10 +12,11 @@ interface UserOperationInfo {
|
||||||
const defaultLnurlPayMetadata = '[["text/plain", "lnurl pay to Lightning.pub"]]'
|
const defaultLnurlPayMetadata = '[["text/plain", "lnurl pay to Lightning.pub"]]'
|
||||||
|
|
||||||
export default class {
|
export default class {
|
||||||
|
|
||||||
storage: Storage
|
storage: Storage
|
||||||
settings: MainSettings
|
settings: MainSettings
|
||||||
lnd: LND
|
lnd: LightningHandler
|
||||||
constructor(storage: Storage, lnd: LND, settings: MainSettings) {
|
constructor(storage: Storage, lnd: LightningHandler, settings: MainSettings) {
|
||||||
this.storage = storage
|
this.storage = storage
|
||||||
this.settings = settings
|
this.settings = settings
|
||||||
this.lnd = lnd
|
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> {
|
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)
|
||||||
|
|
@ -48,7 +53,7 @@ export default class {
|
||||||
async NewInvoice(userId: string, req: Types.NewInvoiceRequest, options: InboundOptionals = { expiry: defaultInvoiceExpiry }): Promise<Types.NewInvoiceResponse> {
|
async NewInvoice(userId: string, req: Types.NewInvoiceRequest, options: InboundOptionals = { expiry: defaultInvoiceExpiry }): Promise<Types.NewInvoiceResponse> {
|
||||||
const user = await this.storage.userStorage.GetUser(userId)
|
const user = await this.storage.userStorage.GetUser(userId)
|
||||||
const res = await this.lnd.NewInvoice(req.amountSats, req.memo, options.expiry)
|
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 {
|
return {
|
||||||
invoice: userInvoice.invoice
|
invoice: userInvoice.invoice
|
||||||
}
|
}
|
||||||
|
|
@ -77,10 +82,10 @@ export default class {
|
||||||
}
|
}
|
||||||
async PayInvoice(userId: string, req: Types.PayInvoiceRequest): Promise<Types.PayInvoiceResponse> {
|
async PayInvoice(userId: string, req: Types.PayInvoiceRequest): Promise<Types.PayInvoiceResponse> {
|
||||||
const decoded = await this.lnd.DecodeInvoice(req.invoice)
|
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")
|
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")
|
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)
|
const payAmount = req.amount !== 0 ? req.amount : Number(decoded.numSatoshis)
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import Storage from '../storage/index.js'
|
import Storage from '../storage/index.js'
|
||||||
import * as Types from '../../../proto/autogenerated/ts/types.js'
|
import * as Types from '../../../proto/autogenerated/ts/types.js'
|
||||||
import LND from '../lnd/index.js'
|
|
||||||
|
|
||||||
import { MainSettings } from './settings.js'
|
import { MainSettings } from './settings.js'
|
||||||
import PaymentManager from './paymentManager.js'
|
import PaymentManager from './paymentManager.js'
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { StorageSettings } from '../storage/index.js'
|
import { StorageSettings } from '../storage/index.js'
|
||||||
import { LndSettings } from '../lnd/index.js'
|
import { LndSettings } from '../lnd/settings.js'
|
||||||
export type MainSettings = {
|
export type MainSettings = {
|
||||||
storageSettings: StorageSettings,
|
storageSettings: StorageSettings,
|
||||||
lndSettings: LndSettings,
|
lndSettings: LndSettings,
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,13 @@ export default (mainHandler: Main): Types.ServerMethods => {
|
||||||
const info = await mainHandler.lnd.GetInfo()
|
const info = await mainHandler.lnd.GetInfo()
|
||||||
return { alias: info.alias }
|
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) => {
|
AddUser: async (ctx, req) => {
|
||||||
const err = Types.AddUserRequestValidate(req, {
|
const err = Types.AddUserRequestValidate(req, {
|
||||||
callbackUrl_CustomCheck: url => url.startsWith("http://") || url.startsWith("https://"),
|
callbackUrl_CustomCheck: url => url.startsWith("http://") || url.startsWith("https://"),
|
||||||
|
|
@ -153,7 +160,7 @@ export default (mainHandler: Main): Types.ServerMethods => {
|
||||||
amount_CustomCheck: amount => amount > 0
|
amount_CustomCheck: amount => amount > 0
|
||||||
})
|
})
|
||||||
if (err != null) throw new Error(err.message)
|
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) => {
|
SendAppUserToAppPayment: async (ctx, req) => {
|
||||||
const err = Types.SendAppUserToAppPaymentRequestValidate(req, {
|
const err = Types.SendAppUserToAppPaymentRequestValidate(req, {
|
||||||
|
|
@ -161,7 +168,7 @@ export default (mainHandler: Main): Types.ServerMethods => {
|
||||||
amount_CustomCheck: amount => amount > 0
|
amount_CustomCheck: amount => amount > 0
|
||||||
})
|
})
|
||||||
if (err != null) throw new Error(err.message)
|
if (err != null) throw new Error(err.message)
|
||||||
mainHandler.applicationManager.SendAppUserToAppPayment(ctx.app_id, req)
|
await mainHandler.applicationManager.SendAppUserToAppPayment(ctx.app_id, req)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue