Add endpoint to get LNURL details and cache LNURL pay callback URL

This commit is contained in:
Michael Bumann
2021-11-28 23:07:03 -06:00
parent 23c6fc3a32
commit 90c132bebe
4 changed files with 124 additions and 57 deletions

View File

@@ -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");
}
}
},
},
};

View File

@@ -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!
}
`;

View File

@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "Project" ADD COLUMN "lnurl_callback_url" TEXT;

View File

@@ -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 {