Skip to content

Commit

Permalink
refactor: lightning payment controller (#300)
Browse files Browse the repository at this point in the history
* 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.
  • Loading branch information
Extheoisah committed Aug 6, 2024
1 parent 6f48e9e commit 9999e78
Show file tree
Hide file tree
Showing 7 changed files with 149 additions and 27 deletions.
2 changes: 2 additions & 0 deletions api/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,5 @@ migrate:
undo-migrate:
yarn run migrate:undo

create-migration:
npx sequelize-cli migration:generate --name $(name)
70 changes: 48 additions & 22 deletions api/app/controllers/lightning.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,12 @@ import { Request, Response } from "express";

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

import { Transaction, Wallet } from "../db/models";
import { Review, Transaction, Wallet } from "../db/models";
import { payInvoice } from "../helpers/lightning";
import { TRANSACTION_STATUS, TRANSACTION_TYPE } from "../types/transaction";
import { PICO_BTC_TO_SATS } from "../utils/constants";
import { generateTransactionId } from "../utils/transaction";
import { sequelize } from "../db";

export async function payInvoiceController(req: Request, res: Response) {
const { invoice, userId } = req.body;
Expand Down Expand Up @@ -53,42 +54,67 @@ export async function payInvoiceController(req: Request, res: Response) {
});
}

const transactionId = generateTransactionId();
const transaction = {
id: transactionId,
reviewId: 10, //FIXME: reviewId is not nullable
amount: newAmount,
transactionType: TRANSACTION_TYPE.DEBIT,
transactionStatus: TRANSACTION_STATUS.PENDING,
walletId: userWallet.id,
timestamp: new Date(),
};

const sequelizeTransaction = await sequelize.transaction();
try {
const result = await Transaction.create(transaction);
// We need to choose a random user review to associate with the transaction
// given that reviewId cannot be null and user will always have a review
// after a successful credit transaction
const review = await Review.findOne({
where: {
userId,
},
});
if (!review) {
throw new Error(
`Could not create transaction: review with userId=${userId} does not exist`
);
}
const transactionId = generateTransactionId();
const transaction = {
id: transactionId,
reviewId: review?.id,
amount: newAmount,
transactionType: TRANSACTION_TYPE.DEBIT,
transactionStatus: TRANSACTION_STATUS.PENDING,
invoice: invoice,
walletId: userWallet.id,
timestamp: new Date(),
};
const result = await Transaction.create(transaction, {
transaction: sequelizeTransaction,
});
if (!result) {
throw new Error("Transaction failed");
}

const response = await payInvoice(invoice);
if (response?.error) {
throw new Error("Payment failed");
if (
(response?.error && response?.error instanceof Error) ||
!response?.data
) {
throw new Error(response?.error?.message || "Payment failed");
}

await Transaction.update(
{ transactionStatus: TRANSACTION_STATUS.SUCCESS },
{ where: { id: transactionId } }
{ where: { id: transactionId }, transaction: sequelizeTransaction }
);
await Wallet.update(
{ balance: balance - newAmount },
{ where: { id: userWallet.id } }
{ where: { id: userWallet.id }, transaction: sequelizeTransaction }
);
res.status(200).json({ status: 200, message: "Invoice paid successfully" });
sequelizeTransaction.commit();
res.status(200).json({
status: 200,
message: "Invoice paid successfully",
data: {
transactionId,
paymentPreimage: response.data[0].payment_preimage,
paymentHash: response.data[0].payment_hash,
},
});
} catch (err) {
Transaction.update(
{ transactionStatus: TRANSACTION_STATUS.FAILED },
{ where: { id: transactionId } }
);
sequelizeTransaction.rollback();
console.error(err);
return res
.status(500)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
"use strict";

/** @type {import('sequelize-cli').Migration} */
module.exports = {
async up(queryInterface, Sequelize) {
/**
* Add altering commands here.
*
* Example:
* await queryInterface.createTable('users', { id: Sequelize.INTEGER });
*/
await queryInterface.addColumn("transactions", "invoice", {
type: Sequelize.TEXT,
allowNull: true,
});
},

async down(queryInterface, Sequelize) {
/**
* Add reverting commands here.
*
* Example:
* await queryInterface.dropTable('users');
*/
await queryInterface.removeColumn("transactions", "invoice");
},
};
6 changes: 6 additions & 0 deletions api/app/db/models/transaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,12 @@ export class Transaction extends Model<TransactionAttributes> {
})
timestamp!: Date;

@Column({
type: DataType.TEXT,
allowNull: true,
})
invoice!: string;

@BelongsTo(() => Wallet)
wallet!: Wallet;

Expand Down
52 changes: 47 additions & 5 deletions api/app/helpers/lightning.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import axios from "axios";
import axios, { AxiosError } from "axios";
import https from "https";

import { CreateInvoiceResponse } from "../types/lightning";
import { CreateInvoiceResponse, PayInvoiceResponse } from "../types/lightning";
import { FEE_LIMIT_SAT, INVOICE_TIME_OUT } from "../utils/constants";
import { Logger } from "./logger";

const MACAROON = process.env.MACAROON;
const LND_URL = process.env.LND_URL;
Expand All @@ -29,11 +30,52 @@ const payInvoice = async (invoice: string) => {
fee_limit_sat: FEE_LIMIT_SAT,
});
if (res.status === 200) {
return { success: true, data: res.data };
const data = res.data;
const responseJsonStrings = data
.split("\n")
.filter((str: string) => str.trim() !== "");
const jsonObjects = responseJsonStrings.map((str: string) =>
JSON.parse(str)
);
const jsonArray = jsonObjects.map(
(obj: { result: PayInvoiceResponse }) => obj.result
);
// loop through the json array and check if the payment preimage is not
// "0000000000000000000000000000000000000000000000000000000000000000"
// check the status of the objects in the filtered array if its "SUCCEEDED"
const unsuccessfulPreimage =
"0000000000000000000000000000000000000000000000000000000000000000";
const filteredArray = jsonArray.filter(
(obj: PayInvoiceResponse) =>
obj.payment_preimage !== unsuccessfulPreimage &&
obj.status === "SUCCEEDED"
) as PayInvoiceResponse[];
if (filteredArray.length > 0) {
Logger.info(`Payment successful: ${filteredArray[0].payment_preimage}`);
return {
success: true,
data: filteredArray,
error: null,
};
} else {
Logger.error(
`Payment failed for ${jsonArray[0].payment_hash} with reason: ${
jsonArray[jsonArray.length - 1].failure_reason
}`
);
return {
success: false,
data: null,
error: new Error("Payment failed"),
};
}
}
} catch (err) {
console.error(err);
return { error: err, data: null };
Logger.error({
message: `Payment failed for invoice: ${invoice}`,
error: JSON.stringify(err),
});
return { success: false, error: err as AxiosError | Error, data: null };
}
};

Expand Down
18 changes: 18 additions & 0 deletions api/app/types/lightning.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,3 +50,21 @@ export type CreateInvoiceResponse = {
add_index: string;
payment_addr: string;
};

export type PayInvoiceResponse = {
payment_hash: string;
payment_preimage: string;
payment_request: string;
payment_index: string;
status: "SUCCEEDED" | "FAILED" | "IN_FLIGHT";
fee: string;
fee_msat: string;
fee_sat: string;
value: string;
value_msat: string;
value_sat: string;
htlcs: Array<{}>;
failure_reason: "FAILURE_REASON_NONE" | string;
creation_time_ns: string;
creation_date: string;
};
1 change: 1 addition & 0 deletions api/app/types/transaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export interface TransactionAttributes {
walletId: string;
reviewId: number | null;
amount: number;
invoice?: string;
transactionType: TRANSACTION_TYPE;
transactionStatus: TRANSACTION_STATUS;
timestamp: Date;
Expand Down

0 comments on commit 9999e78

Please sign in to comment.