Browse Source

Add LNURL-pay server for donations

refactor
Hampus Sjöberg 4 years ago
parent
commit
ff2f4591cd
  1. 1
      .gitignore
  2. 60
      api.ts
  3. 3
      config/config.ts_TEMPLATE
  4. 107
      frontend/components/Donation.tsx
  5. 2
      frontend/pages/index.tsx
  6. 4
      utils.ts

1
.gitignore

@ -2,6 +2,7 @@
.vscode
.aleph
dist
addinvoice_payload.json
config/config.ts
pageviews.json

60
api.ts

@ -1,5 +1,8 @@
import { Router } from "https://deno.land/x/oak/mod.ts";
import { exec, OutputMode } from "https://deno.land/x/exec/mod.ts";
import { ensureFile } from "https://deno.land/std/fs/mod.ts";
import * as base64 from "https://denopkg.com/chiefbiiko/base64/mod.ts";
import { sha256 } from "https://denopkg.com/chiefbiiko/sha256/mod.ts";
import { getBlocks } from "./blocks.ts";
import { getblockchaininfo, getblockcount, getblockhash } from "./jsonrpc/index.ts";
@ -41,7 +44,8 @@ router.get("/blocks", (context) => {
context.response.body = blocks;
});
router.get("/invoice", async (context) => {
const responseMetadata = JSON.stringify([["text/plain", "Donation on taproot.watch"]]);
router.get("/invoice", (context) => {
if (!config.donation) {
context.response.status = 400;
context.response.body = JSON.stringify({
@ -51,14 +55,60 @@ router.get("/invoice", async (context) => {
return;
}
context.response.body = JSON.stringify({
tag: "payRequest",
callback: `${config.donation.lnurlPayUrl}/callback`,
maxSendable: 100000,
minSendable: 1000,
metadata: responseMetadata,
// commentAllowed: 200,
});
});
await ensureFile("./addinvoice_payload.json");
router.get("/invoice/callback", async (context) => {
if (!config.donation) {
context.response.status = 400;
context.response.body = JSON.stringify({
status: "ERROR",
reason: "Donation is not configured",
});
return;
}
const amountString = context.request.url.searchParams.get("amount");
if (amountString === null) {
context.response.body = JSON.stringify({
status: "ERROR",
reason: "Missing amount parameter",
});
return;
}
const amount = Number.parseInt(amountString);
await Deno.writeTextFile(
"./addinvoice_payload.json",
JSON.stringify({
memo: "Donation",
value_msat: amount,
description_hash: sha256(responseMetadata, "utf8", "base64"),
})
);
const macaroonHeader = bytesToHexString(await Deno.readFile(config.donation.data.macaroon));
// -d '{"memo":"Donation"}'
const command = `curl -X POST --cacert ${config.donation.data.cert} --header "Grpc-Metadata-macaroon: ${macaroonHeader}" ${config.donation.data.server}/v1/invoices`;
const command = `curl -X POST --cacert ${config.donation.data.cert} --header "Grpc-Metadata-macaroon: ${macaroonHeader}" -d @addinvoice_payload.json ${config.donation.data.server}/v1/invoices`;
const result = await exec(command, {
output: OutputMode.Capture,
});
context.response.body = JSON.parse(result.output).payment_request;
const paymentRequest = JSON.parse(result.output).payment_request;
context.response.body = JSON.stringify({
pr: paymentRequest,
successAction: {
tag: "message",
message: "Cheers!",
},
disposable: true,
});
});
export default router;

3
config/config.ts_TEMPLATE

@ -43,6 +43,8 @@ interface Config {
// Path to invoice.macaroon
macaroon: string;
};
// URL to the LNURL-pay endpoint
lnurlPayUrl: string;
};
}
@ -73,6 +75,7 @@ const config: Config = {
// macaroon: "/path/to/invoice.macaroon",
// },
// },
// lnurlPayUrl: "https://domain.com/invoice",
};
export default config;

107
frontend/components/Donation.tsx

@ -1,44 +1,115 @@
import React, { useState } from "https://esm.sh/react@17.0.2";
import styled from "https://esm.sh/styled-components";
import { QRCode } from "https://esm.sh/react-qr-svg";
import { bech32 } from "https://esm.sh/bech32";
import config from "../back/config/config.ts";
import { bytesToString } from "../back/utils.ts";
export const DonateContainer = styled.div`
margin: 0 auto 100px;
width: 400px;
text-align: center;
margin-bottom: 100px;
`;
export const DonateText = styled.a`
display: block;
text-align: center;
color: #404040;
text-shadow: #000 1px 1px 0px;
cursor: pointer;
`;
export const Bolt11Text = styled.a`
display: block;
overflow-wrap: anywhere;
font-size: 11px;
color: #aaa;
cursor: pointer;
margin-top: 5px;
margin-bottom: 15px;
`;
export const InvoiceText = styled.p`
text-align: center;
color: #aaa;
text-shadow: #000 1px 1px 0px;
`;
export const ChangeToBolt11 = styled.a`
display: block;
text-align: center;
color: #aaa;
text-shadow: #000 1px 1px 0px;
cursor: pointer;
text-decoration: underline;
font-size: 14px;
`;
const lnurlPayBech32 = bech32.encode(
"lnurl",
bech32.toWords(new TextEncoder().encode(config.donation?.lnurlPayUrl)),
1024
);
export function Donation() {
const [type, setType] = useState<"lnurl-pay" | "bolt11">("lnurl-pay");
const [invoice, setInvoice] = useState<string | undefined>(undefined);
const fetchInvoice = async () => {
const result = await fetch("/invoice");
setInvoice(await result.text());
const showLnUrlPay = () => {
setType("lnurl-pay");
setInvoice(lnurlPayBech32);
};
const decodeLnUrlPay = async () => {
const decodedBech32 = bech32.decode(invoice!, 1024);
const decodedUrl = bytesToString(bech32.fromWords(decodedBech32.words));
const result = await fetch(decodedUrl);
const resultJson = await result.json();
const amount = prompt(
`Choose an amount between ${resultJson.minSendable} sats and ${resultJson.maxSendable} sats`,
resultJson.minSendable
);
const callback = resultJson.callback;
const resultCallback = await fetch(callback + "?amount=" + amount);
const resultCallbackJson = await resultCallback.json();
setType("bolt11");
setInvoice(resultCallbackJson.pr);
};
const onClickInvoice = () => {
window.location.replace("lightning:" + invoice);
};
return (
<DonateContainer>
{!invoice && <DonateText onClick={fetchInvoice}>Donate via Lightning Network</DonateText>}
{!invoice && <DonateText onClick={showLnUrlPay}>Donate via Lightning Network</DonateText>}
{invoice && (
<div
style={{ cursor: "pointer" }}
onClick={() => {
window.location.replace("lightning:" + invoice);
}}
>
<QRCode
bgColor="#FFFFFF"
fgColor="#000000"
value={invoice.toUpperCase()}
style={{ border: "8px solid #fff", width: 200 }}
/>
</div>
<>
<InvoiceText>
{type === "lnurl-pay" && <>LNURL-pay</>}
{type === "bolt11" && <>LN Invoice</>} QR code:
</InvoiceText>
<div style={{ cursor: "pointer" }} onClick={onClickInvoice}>
<QRCode
bgColor="#FFFFFF"
fgColor="#000000"
value={invoice.toUpperCase()}
style={{ border: "8px solid #fff", width: 200 }}
/>
</div>
<Bolt11Text onClick={onClickInvoice}>{invoice}</Bolt11Text>
{type == "lnurl-pay" && (
<ChangeToBolt11 onClick={decodeLnUrlPay}>
Unsupported wallet? Click to change to normal BOLT11 invoice
</ChangeToBolt11>
)}
</>
)}
</DonateContainer>
);

2
frontend/pages/index.tsx

@ -114,7 +114,7 @@ export default function Blocks() {
<Block key={i} height={block.height} signals={block.signals} />
))}
</BlockContainer>
<Donation />
{config.donation && <Donation />}
</Content>
</Container>
);

4
utils.ts

@ -3,3 +3,7 @@ export const bytesToHexString = (bytes: Uint8Array) => {
return memo + ("0" + i.toString(16)).slice(-2); //padd with leading 0 if <16
}, "");
};
export const bytesToString = (bytes: number[]) => {
return String.fromCharCode.apply(null, bytes);
};

Loading…
Cancel
Save