Skip to content

Commit 74e3a93

Browse files
authored
refactor: lightning payment controller (#300)
* refactor: add validation to lightning payment function This commit adds an extra validation check for successful and failed lightning payments using the data returned from the lnd node. It also introduces the use of sequelize db transactions to ensure the integrity of the data depending of the outcome of the invoice payment * feat: add invoice column to transactions table This commit adds a new invoice column to the transaction table to store debit payment ln invoices. it also adds a script called create-migration to the Makefile so its easier for developers to create new migrations for the database schema changes.
1 parent 9e4bf01 commit 74e3a93

File tree

7 files changed

+149
-27
lines changed

7 files changed

+149
-27
lines changed

api/Makefile

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,3 +29,5 @@ migrate:
2929
undo-migrate:
3030
yarn run migrate:undo
3131

32+
create-migration:
33+
npx sequelize-cli migration:generate --name $(name)

api/app/controllers/lightning.controller.ts

Lines changed: 48 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,12 @@ import { Request, Response } from "express";
22

33
import { decode } from "@node-lightning/invoice";
44

5-
import { Transaction, Wallet } from "../db/models";
5+
import { Review, Transaction, Wallet } from "../db/models";
66
import { payInvoice } from "../helpers/lightning";
77
import { TRANSACTION_STATUS, TRANSACTION_TYPE } from "../types/transaction";
88
import { PICO_BTC_TO_SATS } from "../utils/constants";
99
import { generateTransactionId } from "../utils/transaction";
10+
import { sequelize } from "../db";
1011

1112
export async function payInvoiceController(req: Request, res: Response) {
1213
const { invoice, userId } = req.body;
@@ -53,42 +54,67 @@ export async function payInvoiceController(req: Request, res: Response) {
5354
});
5455
}
5556

56-
const transactionId = generateTransactionId();
57-
const transaction = {
58-
id: transactionId,
59-
reviewId: 10, //FIXME: reviewId is not nullable
60-
amount: newAmount,
61-
transactionType: TRANSACTION_TYPE.DEBIT,
62-
transactionStatus: TRANSACTION_STATUS.PENDING,
63-
walletId: userWallet.id,
64-
timestamp: new Date(),
65-
};
66-
57+
const sequelizeTransaction = await sequelize.transaction();
6758
try {
68-
const result = await Transaction.create(transaction);
59+
// We need to choose a random user review to associate with the transaction
60+
// given that reviewId cannot be null and user will always have a review
61+
// after a successful credit transaction
62+
const review = await Review.findOne({
63+
where: {
64+
userId,
65+
},
66+
});
67+
if (!review) {
68+
throw new Error(
69+
`Could not create transaction: review with userId=${userId} does not exist`
70+
);
71+
}
72+
const transactionId = generateTransactionId();
73+
const transaction = {
74+
id: transactionId,
75+
reviewId: review?.id,
76+
amount: newAmount,
77+
transactionType: TRANSACTION_TYPE.DEBIT,
78+
transactionStatus: TRANSACTION_STATUS.PENDING,
79+
invoice: invoice,
80+
walletId: userWallet.id,
81+
timestamp: new Date(),
82+
};
83+
const result = await Transaction.create(transaction, {
84+
transaction: sequelizeTransaction,
85+
});
6986
if (!result) {
7087
throw new Error("Transaction failed");
7188
}
7289

7390
const response = await payInvoice(invoice);
74-
if (response?.error) {
75-
throw new Error("Payment failed");
91+
if (
92+
(response?.error && response?.error instanceof Error) ||
93+
!response?.data
94+
) {
95+
throw new Error(response?.error?.message || "Payment failed");
7696
}
7797

7898
await Transaction.update(
7999
{ transactionStatus: TRANSACTION_STATUS.SUCCESS },
80-
{ where: { id: transactionId } }
100+
{ where: { id: transactionId }, transaction: sequelizeTransaction }
81101
);
82102
await Wallet.update(
83103
{ balance: balance - newAmount },
84-
{ where: { id: userWallet.id } }
104+
{ where: { id: userWallet.id }, transaction: sequelizeTransaction }
85105
);
86-
res.status(200).json({ status: 200, message: "Invoice paid successfully" });
106+
sequelizeTransaction.commit();
107+
res.status(200).json({
108+
status: 200,
109+
message: "Invoice paid successfully",
110+
data: {
111+
transactionId,
112+
paymentPreimage: response.data[0].payment_preimage,
113+
paymentHash: response.data[0].payment_hash,
114+
},
115+
});
87116
} catch (err) {
88-
Transaction.update(
89-
{ transactionStatus: TRANSACTION_STATUS.FAILED },
90-
{ where: { id: transactionId } }
91-
);
117+
sequelizeTransaction.rollback();
92118
console.error(err);
93119
return res
94120
.status(500)
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
"use strict";
2+
3+
/** @type {import('sequelize-cli').Migration} */
4+
module.exports = {
5+
async up(queryInterface, Sequelize) {
6+
/**
7+
* Add altering commands here.
8+
*
9+
* Example:
10+
* await queryInterface.createTable('users', { id: Sequelize.INTEGER });
11+
*/
12+
await queryInterface.addColumn("transactions", "invoice", {
13+
type: Sequelize.TEXT,
14+
allowNull: true,
15+
});
16+
},
17+
18+
async down(queryInterface, Sequelize) {
19+
/**
20+
* Add reverting commands here.
21+
*
22+
* Example:
23+
* await queryInterface.dropTable('users');
24+
*/
25+
await queryInterface.removeColumn("transactions", "invoice");
26+
},
27+
};

api/app/db/models/transaction.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,12 @@ export class Transaction extends Model<TransactionAttributes> {
6969
})
7070
timestamp!: Date;
7171

72+
@Column({
73+
type: DataType.TEXT,
74+
allowNull: true,
75+
})
76+
invoice!: string;
77+
7278
@BelongsTo(() => Wallet)
7379
wallet!: Wallet;
7480

api/app/helpers/lightning.ts

Lines changed: 47 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1-
import axios from "axios";
1+
import axios, { AxiosError } from "axios";
22
import https from "https";
33

4-
import { CreateInvoiceResponse } from "../types/lightning";
4+
import { CreateInvoiceResponse, PayInvoiceResponse } from "../types/lightning";
55
import { FEE_LIMIT_SAT, INVOICE_TIME_OUT } from "../utils/constants";
6+
import { Logger } from "./logger";
67

78
const MACAROON = process.env.MACAROON;
89
const LND_URL = process.env.LND_URL;
@@ -29,11 +30,52 @@ const payInvoice = async (invoice: string) => {
2930
fee_limit_sat: FEE_LIMIT_SAT,
3031
});
3132
if (res.status === 200) {
32-
return { success: true, data: res.data };
33+
const data = res.data;
34+
const responseJsonStrings = data
35+
.split("\n")
36+
.filter((str: string) => str.trim() !== "");
37+
const jsonObjects = responseJsonStrings.map((str: string) =>
38+
JSON.parse(str)
39+
);
40+
const jsonArray = jsonObjects.map(
41+
(obj: { result: PayInvoiceResponse }) => obj.result
42+
);
43+
// loop through the json array and check if the payment preimage is not
44+
// "0000000000000000000000000000000000000000000000000000000000000000"
45+
// check the status of the objects in the filtered array if its "SUCCEEDED"
46+
const unsuccessfulPreimage =
47+
"0000000000000000000000000000000000000000000000000000000000000000";
48+
const filteredArray = jsonArray.filter(
49+
(obj: PayInvoiceResponse) =>
50+
obj.payment_preimage !== unsuccessfulPreimage &&
51+
obj.status === "SUCCEEDED"
52+
) as PayInvoiceResponse[];
53+
if (filteredArray.length > 0) {
54+
Logger.info(`Payment successful: ${filteredArray[0].payment_preimage}`);
55+
return {
56+
success: true,
57+
data: filteredArray,
58+
error: null,
59+
};
60+
} else {
61+
Logger.error(
62+
`Payment failed for ${jsonArray[0].payment_hash} with reason: ${
63+
jsonArray[jsonArray.length - 1].failure_reason
64+
}`
65+
);
66+
return {
67+
success: false,
68+
data: null,
69+
error: new Error("Payment failed"),
70+
};
71+
}
3372
}
3473
} catch (err) {
35-
console.error(err);
36-
return { error: err, data: null };
74+
Logger.error({
75+
message: `Payment failed for invoice: ${invoice}`,
76+
error: JSON.stringify(err),
77+
});
78+
return { success: false, error: err as AxiosError | Error, data: null };
3779
}
3880
};
3981

api/app/types/lightning.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,3 +50,21 @@ export type CreateInvoiceResponse = {
5050
add_index: string;
5151
payment_addr: string;
5252
};
53+
54+
export type PayInvoiceResponse = {
55+
payment_hash: string;
56+
payment_preimage: string;
57+
payment_request: string;
58+
payment_index: string;
59+
status: "SUCCEEDED" | "FAILED" | "IN_FLIGHT";
60+
fee: string;
61+
fee_msat: string;
62+
fee_sat: string;
63+
value: string;
64+
value_msat: string;
65+
value_sat: string;
66+
htlcs: Array<{}>;
67+
failure_reason: "FAILURE_REASON_NONE" | string;
68+
creation_time_ns: string;
69+
creation_date: string;
70+
};

api/app/types/transaction.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ export interface TransactionAttributes {
1414
walletId: string;
1515
reviewId: number | null;
1616
amount: number;
17+
invoice?: string;
1718
transactionType: TRANSACTION_TYPE;
1819
transactionStatus: TRANSACTION_STATUS;
1920
timestamp: Date;

0 commit comments

Comments
 (0)