├── .github
└── ISSUE_TEMPLATE.md
├── LICENSE
├── README.md
└── frontend
├── lightningTip.css
├── lightningTip.js
├── lightningTip.php
└── lightningTip_light.css
/.github/ISSUE_TEMPLATE.md:
--------------------------------------------------------------------------------
1 | ### Background
2 |
3 | Describe your issue here.
4 |
5 | ### Your environment
6 |
7 | * which operating system?
8 | * any other relevant environment details?
9 | * are you running LightningTip behind a reverse proxy?
10 |
11 | ### Steps to reproduce
12 |
13 | Tell us how to reproduce this issue. Please provide stacktraces and links to code in question.
14 |
15 | ### Expected behaviour
16 |
17 | Tell us what should happen.
18 |
19 | ### Actual behaviour
20 |
21 | Tell us what happens instead.
22 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018 michael1011, 2018 robclark56
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # LightningTip-PHP
2 | A simple way to accept tips via the Lightning Network on your website.
3 |
4 | If want to tip me you can use my LightningTip as below.
5 |
6 | * [mainnet](http://raspibolt.epizy.com/LT/lightningTip.php)
7 | * [testnet](http://raspibolt.epizy.com/LT/lightningTip.php?testnet=1)
8 |
9 |
10 |
11 | ## Credit ##
12 | Kudos to [michael1011](https://github.com/michael1011/lightningtip) for the original [LightningTip](https://github.com/michael1011/lightningtip/blob/master/README.md). The difference between the two projects are shown in this table.
13 |
14 | ||LightningTip|LightningTip-PHP|
15 | |--|--|--|
16 | |Backend|An executable
(always running)|PHP|
17 | |Email notification|Yes|Yes|
18 | |lnd communication|gRPC|REST|
19 | |testnet/mainnet selection|No|Yes|
20 | |Keeps track
of tips?|Yes|No|
21 |
22 | ## Requirements ##
23 | * one [lnd](https://github.com/lightningnetwork/lnd) instance
24 | * a webserver that supports [PHP](http://www.php.net/) and [curl](https://curl.haxx.se/)
25 | ## Why PHP? ##
26 | Installing an executable either on the lnd host, or on a 3rd party web host can be problematic. Using PHP improves portability and removes the need for a separate executable running as a service.
27 | ## Security ##
28 | The _invoice.macroon_ file limits the functionality available to LightningTip.php to only invoice related functions. Importantly, if someone steals your _invoice.macaroon_, they can NOT spend any of your funds.
29 | ## Prepare LND ##
30 | * Enable REST on your lnd instance(s). See the _restlisten_ parameter in the [lnd documentation](https://github.com/lightningnetwork/lnd/blob/master/sample-lnd.conf).
31 | * Open any necessary firewall ports on your lnd host, and router port-forwards as needed.
32 | * Generate a hex version of the _invoice.macaroon_ file on your lnd instance.
33 | * Linux: `xxd -ps -u -c 1000 /path/to/invoice.macaroon `
34 | * Generic: [http://tomeko.net/online_tools/file_to_hex.php?lang=en](http://tomeko.net/online_tools/file_to_hex.php?lang=en)
35 |
36 | ## Prepare Web Server ##
37 | Your webserver will need to have the _php-curl_ package installed.
38 |
39 | On a typical Linux webserver you can check as follows. The example below shows that it is installed.
40 | ```bash
41 | $ dpkg -l php-curl
42 | Desired=Unknown/Install/Remove/Purge/Hold
43 | | Status=Not/Inst/Conf-files/Unpacked/halF-conf/Half-inst/trig-aWait/Trig-pend
44 | |/ Err?=(none)/Reinst-required (Status,Err: uppercase=bad)
45 | ||/ Name Version Architecture Description
46 | +++-======================-================-================-=================================================
47 | ii php-curl 1:7.0+49 all CURL module for PHP [default]
48 | ```
49 | If you see `no packages found matching php-curl` then install as follows.
50 | ```
51 | $ sudo apt-get update
52 | $ sudo apt-get install php-curl
53 | ```
54 |
55 |
56 | ## How to install ##
57 | * Download the [latest release](https://github.com/robclark56/lightningtip/releases), and unzip.
58 | * From the _frontend_ folder: Upload these files to your webserver:
59 | * lightningTip.php
60 | * lightningTip.js
61 | * lightningTip.css
62 | * lightningTip_light.css (Optional)
63 | * Edit the _CHANGE ME_ section of `lightningTip.php`. This is where you enter the HEX version of your _invoice.macaroon_.
64 | * Edit the _CHANGE ME_ section of `lightningTip.js`.
65 | * Copy the contents of the head tag from `lightningTip.php` into the head section of the HTML file you want to show LightningTip in. The div below the head tag is LightningTip itself. Paste it into any place in the already edited HTML file on your server.
66 |
67 |
68 | There is a light theme available for LightningTip. If you want to use it **add** this to the head tag of your HTML file:
69 |
70 | ```
71 |
72 | ```
73 |
74 | **Do not use LightningTip on XHTML** sites. That causes some weird scaling issues.
75 |
76 | That's it! The only things you need to take care of is keeping the LND node and web server online. LightningTip will take care of everything else.
77 |
78 | ## How to run ##
79 | Use your browser to visit either of these:
80 |
81 | * `https://your.web.server/path/lightningTip.php`
82 | * `https://your.web.server/path/lightningTip.php?testnet=1`
83 |
84 |
85 |
--------------------------------------------------------------------------------
/frontend/lightningTip.css:
--------------------------------------------------------------------------------
1 | #lightningTip {
2 | width: 12em;
3 |
4 | padding: 1em;
5 |
6 | background-color: #212121;
7 |
8 | border-radius: 4px;
9 |
10 | color: #F5F5F5;
11 |
12 | font-size: 20px;
13 | font-family: Arial, Helvetica, sans-serif;
14 |
15 | text-align: center;
16 | }
17 |
18 | .lightningTipInput {
19 | width: 100%;
20 |
21 | display: inline-block;
22 |
23 | padding: 6px 10px;
24 |
25 | border: none;
26 | border-radius: 4px;
27 |
28 | font-size: 15px;
29 |
30 | color: #212121;
31 |
32 | background-color: #F5F5F5;
33 |
34 | outline: none;
35 | resize: none;
36 |
37 | overflow-y: hidden;
38 | }
39 |
40 | .lightningTipButton {
41 | padding: 0.4em 1em;
42 |
43 | font-size: 17px;
44 |
45 | color: #212121;
46 |
47 | background-color: #FFC83D;
48 |
49 | border: none;
50 | border-radius: 4px;
51 |
52 | outline: none;
53 | cursor: pointer;
54 | }
55 |
56 | .lightningTipButton:focus {
57 | outline: none;
58 | }
59 |
60 | .lightningTipButton::-moz-focus-inner {
61 | outline: none;
62 |
63 | border: 0;
64 | }
65 |
66 | #lightningTipLogo {
67 | margin-top: 0;
68 | margin-bottom: 0.6em;
69 |
70 | font-size: 25px;
71 | }
72 |
73 | #lightningTipInputs {
74 | margin-top: 0.8em;
75 | }
76 |
77 | #lightningTipMessage {
78 | min-height: 55px;
79 |
80 | margin-top: 0.5em;
81 | padding: 8px 10px;
82 |
83 | display: inline-block;
84 | box-sizing: border-box;
85 |
86 | text-align: left;
87 |
88 | font-family: Arial, Helvetica, sans-serif;
89 | }
90 |
91 | /* Hack for using placeholders on divs */
92 | #lightningTipMessage:empty:before {
93 | content: attr(placeholder);
94 | color: gray;
95 | }
96 |
97 | #lightningTipGetInvoice {
98 | margin-top: 1em;
99 | }
100 |
101 | #lightningTipError {
102 | font-size: 17px;
103 |
104 | color: #F44336;
105 | }
106 |
107 | #lightningTipInvoice {
108 | margin-top: 1em;
109 | margin-bottom: 0.5em;
110 | }
111 |
112 | #lightningTipQR {
113 | margin-bottom: 0.8em;
114 | }
115 |
116 | #lightningTipTools {
117 | height: 100px;
118 | }
119 |
120 | #lightningTipCopy {
121 | border-right: 1px solid #F5F5F5;
122 |
123 | border-top-right-radius: 0;
124 | border-bottom-right-radius: 0;
125 |
126 | float: left;
127 | }
128 |
129 | #lightningTipOpen {
130 | border-top-left-radius: 0;
131 | border-bottom-left-radius: 0;
132 |
133 | float: left;
134 | }
135 |
136 | #lightningTipExpiry {
137 | padding: 0.3em 0;
138 |
139 | float: right;
140 | }
141 |
142 | #lightningTipFinished {
143 | margin-bottom: 0.2em;
144 |
145 | display: block;
146 | }
147 |
148 | .spinner {
149 | width: 12px;
150 | height: 12px;
151 |
152 | display: inline-block;
153 |
154 | border: 3px solid #F5F5F5;
155 | border-top: 3px solid #212121;
156 | border-radius: 50%;
157 |
158 | animation: spin 1.5s linear infinite;
159 | }
160 |
161 | @keyframes spin {
162 | 0% {
163 | transform: rotate(0deg);
164 | }
165 | 100% {
166 | transform: rotate(360deg);
167 | }
168 | }
169 |
170 | #lightningTip.testnet {
171 | background-color: #27b09d;
172 | }
173 |
--------------------------------------------------------------------------------
/frontend/lightningTip.js:
--------------------------------------------------------------------------------
1 | ///////// CHANGE ME ////////
2 | var requestUrl = window.location.protocol + "//" + window.location.hostname + "/lightningTip.php";
3 | ///////// END CHANGE ME ////////
4 |
5 | // To prohibit multiple requests at the same time
6 | var running = false;
7 |
8 | var invoice;
9 | var qrCode;
10 | var defaultGetInvoice;
11 |
12 | // Data capacities for QR codes with mode byte and error correction level L (7%)
13 | // Shortest invoice: 194 characters
14 | // Longest invoice: 1223 characters (as far as I know)
15 | var qrCodeDataCapacities = [
16 | {"typeNumber": 9, "capacity": 230},
17 | {"typeNumber": 10, "capacity": 271},
18 | {"typeNumber": 11, "capacity": 321},
19 | {"typeNumber": 12, "capacity": 367},
20 | {"typeNumber": 13, "capacity": 425},
21 | {"typeNumber": 14, "capacity": 458},
22 | {"typeNumber": 15, "capacity": 520},
23 | {"typeNumber": 16, "capacity": 586},
24 | {"typeNumber": 17, "capacity": 644},
25 | {"typeNumber": 18, "capacity": 718},
26 | {"typeNumber": 19, "capacity": 792},
27 | {"typeNumber": 20, "capacity": 858},
28 | {"typeNumber": 21, "capacity": 929},
29 | {"typeNumber": 22, "capacity": 1003},
30 | {"typeNumber": 23, "capacity": 1091},
31 | {"typeNumber": 24, "capacity": 1171},
32 | {"typeNumber": 25, "capacity": 1273}
33 | ];
34 |
35 | // TODO: solve this without JavaScript
36 | // Fixes weird bug which moved the button up one pixel when its content was changed
37 | window.onload = function () {
38 | var button = document.getElementById("lightningTipGetInvoice");
39 |
40 | button.style.height = (button.clientHeight + 1) + "px";
41 | button.style.width = (button.clientWidth + 1) + "px";
42 |
43 | };
44 |
45 | var testnet = getVal('testnet'); //see if GET param testnet is set (URL?testnet=1)
46 | console.log('Testnet = ' + testnet);
47 | if ( testnet !== null ) {
48 | requestUrl = requestUrl + '?testnet=1';
49 | console.log('requestUrl = ' + requestUrl );
50 | }
51 |
52 | // TODO: show invoice even if JavaScript is disabled
53 | // TODO: fix scaling on phones
54 | // TODO: show price in dollar?
55 | function getInvoice() {
56 | if (running === false) {
57 | running = true;
58 |
59 | var tipValue = document.getElementById("lightningTipAmount").value.trim();
60 |
61 | if (tipValue === "") {
62 | showErrorMessage("No tip amount set");
63 | return;
64 | } else if (isNaN(tipValue)) {
65 | showErrorMessage("Tip amount must be a number");
66 | return;
67 | } else if (tipValue < 1) {
68 | showErrorMessage("Tip amount must be at least 1 sat");
69 | return;
70 | } else if (parseInt(tipValue).toString() != tipValue) {
71 | showErrorMessage("Tip amount should be a whole number of sats (no decimals)");
72 | return;
73 | }
74 |
75 | var request = new XMLHttpRequest();
76 |
77 | request.onreadystatechange = function () {
78 | if (request.readyState === 4) {
79 | //console.log("RESPONSE: " + request.responseText);
80 | try {
81 | var json = JSON.parse(request.responseText);
82 |
83 | if (request.status === 200) {
84 | console.log("Got invoice: " + json.Invoice);
85 | console.log("Invoice expires in: " + json.Expiry);
86 | console.log("Starting listening for invoice to get settled");
87 |
88 | listenInvoiceSettled(json.r_hash_str);
89 |
90 | invoice = json.Invoice;
91 |
92 | // Update UI
93 | var wrapper = document.getElementById("lightningTip");
94 |
95 | wrapper.innerHTML = "Your tip request";
96 | wrapper.innerHTML += "";
97 | wrapper.innerHTML += "
⚡
"; 193 | wrapper.innerHTML += "Thank you for your tip!"; 194 | } 195 | 196 | function startTimer(duration, element) { 197 | showTimer(duration, element); 198 | 199 | var interval = setInterval(function () { 200 | if (duration > 1) { 201 | duration--; 202 | 203 | showTimer(duration, element); 204 | 205 | } else { 206 | showExpired(); 207 | 208 | clearInterval(interval); 209 | } 210 | 211 | }, 1000); 212 | 213 | } 214 | 215 | function showTimer(duration, element) { 216 | var seconds = Math.floor(duration % 60); 217 | var minutes = Math.floor((duration / 60) % 60); 218 | var hours = Math.floor((duration / (60 * 60)) % 24); 219 | 220 | seconds = addLeadingZeros(seconds); 221 | minutes = addLeadingZeros(minutes); 222 | 223 | if (hours > 0) { 224 | element.innerHTML = hours + ":" + minutes + ":" + seconds; 225 | 226 | } else { 227 | element.innerHTML = minutes + ":" + seconds; 228 | } 229 | 230 | } 231 | 232 | function showExpired() { 233 | var wrapper = document.getElementById("lightningTip"); 234 | 235 | wrapper.innerHTML = "⚡
"; 236 | wrapper.innerHTML += "Your tip request expired!"; 237 | } 238 | 239 | function addLeadingZeros(value) { 240 | return ("0" + value).slice(-2); 241 | } 242 | 243 | function showQRCode() { 244 | var element = document.getElementById("lightningTipQR"); 245 | 246 | createQRCode(); 247 | 248 | element.innerHTML = qrCode; 249 | 250 | var size = document.getElementById("lightningTipInvoice").clientWidth + "px"; 251 | 252 | var qrElement = element.children[0]; 253 | 254 | qrElement.style.height = size; 255 | qrElement.style.width = size; 256 | } 257 | 258 | function createQRCode() { 259 | var invoiceLength = invoice.length; 260 | 261 | // Just in case an invoice bigger than expected gets created 262 | var typeNumber = 26; 263 | 264 | for (var i = 0; i < qrCodeDataCapacities.length; i++) { 265 | var dataCapacity = qrCodeDataCapacities[i]; 266 | 267 | if (invoiceLength < dataCapacity.capacity) { 268 | typeNumber = dataCapacity.typeNumber; 269 | 270 | break; 271 | } 272 | 273 | } 274 | 275 | console.log("Creating QR code with type number: " + typeNumber); 276 | 277 | var qr = qrcode(typeNumber, "L"); 278 | 279 | qr.addData(invoice); 280 | qr.make(); 281 | 282 | qrCode = qr.createImgTag(6, 6); 283 | } 284 | 285 | function copyInvoiceToClipboard() { 286 | var element = document.getElementById("lightningTipInvoice"); 287 | 288 | element.select(); 289 | 290 | document.execCommand('copy'); 291 | 292 | console.log("Copied invoice to clipboard"); 293 | } 294 | 295 | function showErrorMessage(message) { 296 | running = false; 297 | 298 | console.error(message); 299 | 300 | var error = document.getElementById("lightningTipError"); 301 | 302 | error.parentElement.style.marginTop = "0.5em"; 303 | error.innerHTML = message; 304 | 305 | var button = document.getElementById("lightningTipGetInvoice"); 306 | 307 | // Only necessary if it has a child (div with class spinner) 308 | if (button.children.length !== 0) { 309 | button.innerHTML = defaultGetInvoice; 310 | } 311 | 312 | } 313 | 314 | function divRestorePlaceholder(element) { 315 | //⚡
186 | Send a tip via Lightning 187 | 188 | 200 | 201 |