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})
|
||||
} 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 {
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -16,6 +16,11 @@ message LndGetInfoRequest {
|
|||
int64 nodeId = 1;
|
||||
}
|
||||
|
||||
message SetMockInvoiceAsPaidRequest {
|
||||
string invoice = 1;
|
||||
int64 amount =2;
|
||||
}
|
||||
|
||||
message LndGetInfoResponse {
|
||||
string alias = 1;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,3 +10,8 @@ export const EnvMustBeInteger = (name: string): number => {
|
|||
}
|
||||
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 { 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 = () => {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
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>
|
||||
}
|
||||
|
||||
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")
|
||||
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)
|
||||
}
|
||||
}
|
||||
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)
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
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 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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue