mirror of
https://github.com/aljazceru/landscape-template.git
synced 2025-12-27 11:14:33 +01:00
Add endpoint to get LNURL details and cache LNURL pay callback URL
This commit is contained in:
@@ -1,45 +1,59 @@
|
||||
const { parsePaymentRequest } = require('invoices');
|
||||
const axios = require('axios');
|
||||
const { createHash } = require('crypto');
|
||||
const { parsePaymentRequest } = require("invoices");
|
||||
const axios = require("axios");
|
||||
const { createHash } = require("crypto");
|
||||
|
||||
function hexToUint8Array (hexString) {
|
||||
function hexToUint8Array(hexString) {
|
||||
const match = hexString.match(/.{1,2}/g);
|
||||
if (match) {
|
||||
return new Uint8Array(match.map((byte) => parseInt(byte, 16)));
|
||||
}
|
||||
}
|
||||
|
||||
// TODO validate responses
|
||||
function getPaymetRequest(lightning_address, amount_in_sat) {
|
||||
// TODO: generaly validate LNURL responses
|
||||
|
||||
function getLnurlDetails(lnurl) {
|
||||
return axios.get(lnurl);
|
||||
}
|
||||
|
||||
function lightningAddressToLnurl(lightning_address) {
|
||||
const [name, domain] = lightning_address.split("@");
|
||||
const lnurl = `https://${domain}/.well-known/lnurlp/${name}`;
|
||||
return axios.get(lnurl)
|
||||
.then((response) => {
|
||||
console.log(response.data);
|
||||
const callbackUrl = response.data.callback;
|
||||
const amount = amount_in_sat * 1000; // msats
|
||||
return axios.get(callbackUrl, { params: { amount }} )
|
||||
.then(prResponse => {
|
||||
console.log(prResponse.data);
|
||||
return prResponse.data.pr;
|
||||
});
|
||||
})
|
||||
.catch(function (error) {
|
||||
console.error(error);
|
||||
})
|
||||
return `https://${domain}/.well-known/lnurlp/${name}`;
|
||||
}
|
||||
|
||||
async function getLnurlCallbackUrl(lightning_address) {
|
||||
return getLnurlDetails(lightningAddressToLnurl(lightning_address)).then(
|
||||
(response) => {
|
||||
return response.data.callback;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
async function getPaymetRequestForProject(project, amount_in_sat) {
|
||||
let lnurlCallbackUrl = project.lnurl_callback_url;
|
||||
const amount = amount_in_sat * 1000; // msats
|
||||
console.log(lnurlCallbackUrl);
|
||||
if (!lnurlCallbackUrl) {
|
||||
lnurlCallbackUrl = await getLnurlCallbackUrl(project.lightning_address);
|
||||
}
|
||||
return axios
|
||||
.get(lnurlCallbackUrl, { params: { amount } })
|
||||
.then((prResponse) => {
|
||||
console.log(prResponse.data);
|
||||
return prResponse.data.pr;
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
Query: {
|
||||
allCategories: async (_source, args, context) => {
|
||||
return context.prisma.category.findMany({
|
||||
orderBy: { title: 'desc'},
|
||||
orderBy: { title: "desc" },
|
||||
include: {
|
||||
project: {
|
||||
take: 5,
|
||||
orderBy: { votes_count: "desc" }
|
||||
}
|
||||
}
|
||||
orderBy: { votes_count: "desc" },
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
getCategory: async (_source, args, context) => {
|
||||
@@ -48,16 +62,16 @@ module.exports = {
|
||||
include: {
|
||||
project: {
|
||||
take: 5,
|
||||
orderBy: { votes_count: "desc" }
|
||||
}
|
||||
}
|
||||
orderBy: { votes_count: "desc" },
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
newProjects: async (_source, args, context) => {
|
||||
const take = args.take || 50;
|
||||
const skip = args.skip || 0;
|
||||
return context.prisma.project.findMany({
|
||||
orderBy: { created_at: 'desc' },
|
||||
orderBy: { created_at: "desc" },
|
||||
include: { category: true },
|
||||
skip,
|
||||
take,
|
||||
@@ -67,7 +81,7 @@ module.exports = {
|
||||
const take = args.take || 50;
|
||||
const skip = args.skip || 0;
|
||||
return context.prisma.project.findMany({
|
||||
orderBy: { votes_count: 'desc' },
|
||||
orderBy: { votes_count: "desc" },
|
||||
include: { category: true },
|
||||
skip,
|
||||
take,
|
||||
@@ -79,7 +93,7 @@ module.exports = {
|
||||
const categoryId = args.category_id;
|
||||
return context.prisma.project.findMany({
|
||||
where: { category_id: categoryId },
|
||||
orderBy: { votes_count: 'desc' },
|
||||
orderBy: { votes_count: "desc" },
|
||||
include: { category: true },
|
||||
skip,
|
||||
take,
|
||||
@@ -90,14 +104,47 @@ module.exports = {
|
||||
where: {
|
||||
id: args.id,
|
||||
},
|
||||
include: { category: true }
|
||||
include: { category: true },
|
||||
});
|
||||
},
|
||||
getLnurlDetailsForProject: async (_source, args, context) => {
|
||||
const project = await context.prisma.project.findUnique({
|
||||
where: {
|
||||
id: args.project_id,
|
||||
},
|
||||
});
|
||||
const lnurlDetails = await getLnurlDetails(
|
||||
lightningAddressToLnurl(project.lightning_address)
|
||||
);
|
||||
if (
|
||||
!lnurlDetails.data ||
|
||||
lnurlDetails.data.status.toLowerCase() !== "ok"
|
||||
) {
|
||||
console.error(lnurlDetails.data);
|
||||
throw new Error("Recipient not available");
|
||||
}
|
||||
|
||||
// cache the callback URL
|
||||
await context.prisma.project.update({
|
||||
where: { id: project.id },
|
||||
data: {
|
||||
lnurl_callback_url: lnurlDetails.data.callback,
|
||||
},
|
||||
});
|
||||
return {
|
||||
minSendable: parseInt(lnurlDetails.data.minSendable) / 1000,
|
||||
maxSendable: parseInt(lnurlDetails.data.maxSendable) / 1000,
|
||||
metadata: lnurlDetails.data.metadata,
|
||||
commentAllowed: lnurlDetails.data.commentAllowed,
|
||||
};
|
||||
},
|
||||
},
|
||||
Mutation: {
|
||||
vote: async (_source, args, context) => {
|
||||
const project = await context.prisma.project.findUnique({where: { id: args.project_id }});
|
||||
const pr = await getPaymetRequest(project.lightning_address, args.amount_in_sat);
|
||||
const project = await context.prisma.project.findUnique({
|
||||
where: { id: args.project_id },
|
||||
});
|
||||
const pr = await getPaymetRequestForProject(project, args.amount_in_sat);
|
||||
const invoice = parsePaymentRequest({ request: pr });
|
||||
return context.prisma.vote.create({
|
||||
data: {
|
||||
@@ -105,23 +152,32 @@ module.exports = {
|
||||
amount_in_sat: args.amount_in_sat,
|
||||
payment_request: pr,
|
||||
payment_hash: invoice.id,
|
||||
}
|
||||
},
|
||||
});
|
||||
},
|
||||
confirmVote: async (_source, args, context) => {
|
||||
const paymentHash = createHash('sha256').update(hexToUint8Array(args.preimage)).digest('hex');
|
||||
const paymentHash = createHash("sha256")
|
||||
.update(hexToUint8Array(args.preimage))
|
||||
.digest("hex");
|
||||
// look for a vote for the payment request and the calculated payment hash
|
||||
const vote = await context.prisma.vote.findFirst({where: { payment_request: args.payment_request, payment_hash: paymentHash}});
|
||||
const vote = await context.prisma.vote.findFirst({
|
||||
where: {
|
||||
payment_request: args.payment_request,
|
||||
payment_hash: paymentHash,
|
||||
},
|
||||
});
|
||||
// if we find a vote it means the preimage is correct and we update the vote and mark it as paid
|
||||
// can we write this nicer?
|
||||
if (vote) {
|
||||
const project = await context.prisma.project.findUnique({ where: { id: vote.project_id }});
|
||||
const project = await context.prisma.project.findUnique({
|
||||
where: { id: vote.project_id },
|
||||
});
|
||||
// count up votes cache
|
||||
await context.prisma.project.update({
|
||||
where: {id: project.id },
|
||||
where: { id: project.id },
|
||||
data: {
|
||||
votes_count: project.votes_count = vote.amount_in_sat,
|
||||
}
|
||||
votes_count: (project.votes_count = vote.amount_in_sat),
|
||||
},
|
||||
});
|
||||
// return the current vote
|
||||
return context.prisma.vote.update({
|
||||
@@ -129,11 +185,11 @@ module.exports = {
|
||||
data: {
|
||||
paid: true,
|
||||
preimage: args.preimage,
|
||||
}
|
||||
},
|
||||
});
|
||||
} else {
|
||||
throw new Error("Invalid preimage");
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -27,6 +27,13 @@ module.exports = gql`
|
||||
paid: Boolean!
|
||||
}
|
||||
|
||||
type LnurlDetails {
|
||||
minSendable: Int
|
||||
maxSendable: Int
|
||||
metadata: String
|
||||
commentAllowed: Int
|
||||
}
|
||||
|
||||
type Query {
|
||||
allProjects(skip: Int, take: Int): [Project]!
|
||||
newProjects(skip: Int, take: Int): [Project]!
|
||||
@@ -34,9 +41,10 @@ module.exports = gql`
|
||||
getProject(id: Int!): Project!
|
||||
allCategories: [Category]!
|
||||
getCategory(id: Int!): Category!
|
||||
getLnurlDetailsForProject(project_id: Int!): LnurlDetails!
|
||||
}
|
||||
type Mutation {
|
||||
vote (project_id: Int!, amount_in_sat: Int!): Vote!
|
||||
confirmVote (payment_request: String!, preimage: String!): Vote!
|
||||
vote(project_id: Int!, amount_in_sat: Int!): Vote!
|
||||
confirmVote(payment_request: String!, preimage: String!): Vote!
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "Project" ADD COLUMN "lnurl_callback_url" TEXT;
|
||||
@@ -14,18 +14,19 @@ model Category {
|
||||
}
|
||||
|
||||
model Project {
|
||||
id Int @id @default(autoincrement())
|
||||
title String
|
||||
description String
|
||||
website String
|
||||
thumbnail_image String?
|
||||
cover_image String?
|
||||
lightning_address String?
|
||||
category Category @relation(fields: [category_id], references: [id])
|
||||
category_id Int
|
||||
votes_count Int @default(0)
|
||||
vote Vote[]
|
||||
created_at DateTime @default(now())
|
||||
id Int @id @default(autoincrement())
|
||||
title String
|
||||
description String
|
||||
website String
|
||||
thumbnail_image String?
|
||||
cover_image String?
|
||||
lightning_address String?
|
||||
lnurl_callback_url String?
|
||||
category Category @relation(fields: [category_id], references: [id])
|
||||
category_id Int
|
||||
votes_count Int @default(0)
|
||||
vote Vote[]
|
||||
created_at DateTime @default(now())
|
||||
}
|
||||
|
||||
model Vote {
|
||||
|
||||
Reference in New Issue
Block a user