├── .travis.yml ├── backends ├── log.go ├── backend.go └── lnd.go ├── database ├── log.go └── database.go ├── notifications ├── log.go └── mail.go ├── version └── version.go ├── .github └── ISSUE_TEMPLATE.md ├── .gitignore ├── frontend ├── lightningTip_light.css ├── lightningTip.html ├── lightningTip.css └── lightningTip.js ├── go.mod ├── log.go ├── LICENSE ├── Makefile ├── cmd └── tipreport │ ├── tipreport.go │ └── commands.go ├── release.sh ├── go.sum ├── sample-lightningTip.conf ├── README.md ├── config.go └── lightningtip.go /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | go: 4 | - "1.11.4" 5 | 6 | script: 7 | - make build 8 | - make lint 9 | -------------------------------------------------------------------------------- /backends/log.go: -------------------------------------------------------------------------------- 1 | package backends 2 | 3 | import "github.com/op/go-logging" 4 | 5 | var log logging.Logger 6 | 7 | // UseLogger tells the backends package which logger to use 8 | func UseLogger(logger logging.Logger) { 9 | log = logger 10 | } 11 | -------------------------------------------------------------------------------- /database/log.go: -------------------------------------------------------------------------------- 1 | package database 2 | 3 | import "github.com/op/go-logging" 4 | 5 | var log logging.Logger 6 | 7 | // UseLogger tells the database package which logger to use 8 | func UseLogger(logger logging.Logger) { 9 | log = logger 10 | } 11 | -------------------------------------------------------------------------------- /notifications/log.go: -------------------------------------------------------------------------------- 1 | package notifications 2 | 3 | import "github.com/op/go-logging" 4 | 5 | var log logging.Logger 6 | 7 | // UseLogger tells the notifications package which logger to use 8 | func UseLogger(logger logging.Logger) { 9 | log = logger 10 | } 11 | -------------------------------------------------------------------------------- /version/version.go: -------------------------------------------------------------------------------- 1 | package version 2 | 3 | import "fmt" 4 | 5 | // Version is the version of LightningTip 6 | const Version = "1.1.0-dev" 7 | 8 | // PrintVersion prints the version of LightningTip to the console 9 | func PrintVersion() { 10 | fmt.Println("LightningTip version " + Version) 11 | } 12 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.dll 4 | *.so 5 | *.dylib 6 | /lightningTip 7 | /lightningtip 8 | /tipreport 9 | lightningTip*/ 10 | 11 | # Test binary, build with `go test -c` 12 | *.test 13 | 14 | # Output of the go coverage tool, specifically when used with LiteIDE 15 | *.out 16 | 17 | # Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736 18 | .glide/ 19 | 20 | # Jetbrain GoLand 21 | .idea 22 | 23 | # Log file 24 | lightningTip.log 25 | 26 | # Config file 27 | lightningTip.conf 28 | 29 | # Dep vendor directory 30 | vendor 31 | _vendor* -------------------------------------------------------------------------------- /frontend/lightningTip_light.css: -------------------------------------------------------------------------------- 1 | #lightningTip > a { 2 | font-weight: 600 !important; 3 | } 4 | 5 | #lightningTip { 6 | background-color: #FFFFFF !important; 7 | 8 | border: 2px #BDBDBD solid !important; 9 | 10 | color: #FFB300 !important; 11 | 12 | } 13 | 14 | .lightningTipInput { 15 | border: 1px #BDBDBD solid !important; 16 | 17 | background-color: #FFFFFF !important; 18 | } 19 | 20 | .lightningTipButton { 21 | color: #FFFFFF !important; 22 | 23 | background-color: #FFB300 !important; 24 | } 25 | 26 | #lightningTipError { 27 | color: #D50000 !important; 28 | 29 | font-weight: 600 !important; 30 | } 31 | 32 | #lightningTipCopy { 33 | border-right: 1px solid #FFFFFF !important; 34 | } 35 | 36 | #lightningTipExpiry { 37 | font-weight: 600 !important; 38 | } -------------------------------------------------------------------------------- /backends/backend.go: -------------------------------------------------------------------------------- 1 | package backends 2 | 3 | // PublishInvoiceSettled is a callback for a settled invoice 4 | type PublishInvoiceSettled func(invoice string) 5 | 6 | // RescanPendingInvoices is a callbacks when reconnecting 7 | type RescanPendingInvoices func() 8 | 9 | // Backend is an interface that would allow for different implementations of Lightning to be used as backend 10 | type Backend interface { 11 | Connect() error 12 | 13 | // The amount is denominated in satoshis and the expiry in seconds 14 | GetInvoice(description string, amount int64, expiry int64) (invoice string, rHash string, err error) 15 | 16 | InvoiceSettled(rHash string) (settled bool, err error) 17 | 18 | SubscribeInvoices(publish PublishInvoiceSettled, rescan RescanPendingInvoices) error 19 | 20 | KeepAliveRequest() error 21 | } 22 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/michael1011/lightningtip 2 | 3 | require ( 4 | github.com/donovanhide/eventsource v0.0.0-20171031113327-3ed64d21fb0b 5 | github.com/golang/protobuf v1.2.0 // indirect 6 | github.com/grpc-ecosystem/grpc-gateway v0.0.0-20170724004829-f2862b476edc // indirect 7 | github.com/jessevdk/go-flags v1.4.0 8 | github.com/lightningnetwork/lnd v0.0.0-20180827212353-73af09a06ae9 9 | github.com/mattn/go-sqlite3 v1.9.0 10 | github.com/op/go-logging v0.0.0-20160211212156-b2cb9fa56473 11 | github.com/urfave/cli v1.20.0 12 | golang.org/x/net v0.0.0-20180311174755-ae89d30ce0c6 // indirect 13 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 // indirect 14 | golang.org/x/text v0.3.0 // indirect 15 | google.golang.org/genproto v0.0.0-20180306020942-df60624c1e9b // indirect 16 | google.golang.org/grpc v1.5.2 17 | ) 18 | -------------------------------------------------------------------------------- /log.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/op/go-logging" 7 | ) 8 | 9 | var log = logging.MustGetLogger("") 10 | var logFormat = logging.MustStringFormatter("%{time:2006-01-02 15:04:05} [%{level}] %{message}") 11 | 12 | var backendConsole = logging.NewLogBackend(os.Stdout, "", 0) 13 | 14 | func initLog() { 15 | logging.SetFormatter(logFormat) 16 | 17 | logging.SetBackend(backendConsole) 18 | } 19 | 20 | func initLogger(logFile string, level logging.Level) error { 21 | file, err := os.OpenFile(logFile, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0600) 22 | 23 | if err != nil { 24 | return err 25 | } 26 | 27 | backendFile := logging.NewLogBackend(file, "", 0) 28 | 29 | backendFileLeveled := logging.AddModuleLevel(backendFile) 30 | backendFileLeveled.SetLevel(level, "") 31 | 32 | backendConsoleLeveled := logging.AddModuleLevel(backendConsole) 33 | backendConsoleLeveled.SetLevel(level, "") 34 | 35 | logging.SetBackend(backendConsoleLeveled, backendFileLeveled) 36 | 37 | return nil 38 | } 39 | -------------------------------------------------------------------------------- /database/database.go: -------------------------------------------------------------------------------- 1 | package database 2 | 3 | import ( 4 | "database/sql" 5 | "fmt" 6 | "time" 7 | 8 | // The sqlite drivers have to be imported to establish a connection to the database 9 | _ "github.com/mattn/go-sqlite3" 10 | ) 11 | 12 | var db *sql.DB 13 | 14 | // InitDatabase is initializing the database 15 | func InitDatabase(databaseFile string) (err error) { 16 | db, err = sql.Open("sqlite3", databaseFile) 17 | 18 | if err == nil { 19 | db.Exec("CREATE TABLE IF NOT EXISTS `tips` (`date` INTEGER, `amount` INTEGER, `message` VARCHAR)") 20 | } 21 | 22 | return err 23 | } 24 | 25 | // AddSettledInvoice is adding a settled invoice to the database 26 | func AddSettledInvoice(amount int64, message string) { 27 | stmt, err := db.Prepare("INSERT INTO tips(date, amount, message) values(?, ?, ?)") 28 | 29 | if err == nil { 30 | _, err = stmt.Exec(time.Now().Unix(), amount, message) 31 | } 32 | 33 | if err == nil { 34 | stmt.Close() 35 | 36 | } else { 37 | log.Error("Could not insert into database: " + fmt.Sprint(err)) 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /frontend/lightningTip.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |⚡
12 | Send a tip via Lightning 13 | 14 | 26 | 27 |
8 |
9 | ## How to install
10 |
11 | To get all necessary files for setting up LightningTip you can either [download a prebuilt version](https://github.com/michael1011/lightningtip/releases) or [compile from source](#how-to-build).
12 |
13 | LightningTip is using [LND](https://github.com/lightningnetwork/lnd) as backend. Please make sure it is installed and fully synced before you install LightningTip.
14 |
15 | The default config file location is `$HOME/.lightningtip/lightningTip.conf`. The [sample config](https://github.com/michael1011/lightningtip/blob/master/sample-lightningTip.conf) contains everything you need to know about the configuration. To use a custom config file location use the flag `--config filename`. You can use all keys in the config as command line flag. Command line flags *always* override values in the config.
16 |
17 | The next step is embedding LightningTip on your website. Upload all files excluding `lightningTip.html` to your webserver. Copy the contents of the head tag from `lightningTip.html` 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.
18 |
19 | There is a light theme available for LightningTip. If you want to use it **add** this to the head tag of your HTML file:
20 |
21 | ```html
22 |
23 | ```
24 |
25 | **Do not use LightningTip on XHTML** sites. That causes some weird scaling issues.
26 |
27 | Make sure that the executable of **LightningTip is always running** in the background. It is an [API](https://github.com/michael1011/lightningtip/wiki/API-documentation) to connect LND and the widget on your website. **Do not open the URL you are running LightingTip on in your browser.** All this will do is show an error.
28 |
29 | If you are not running LightningTip on the same domain or IP address as your webserver, or not on port 8081, change the variable `requestUrl` (which is in the first line) in the file `lightningTip.js` accordingly.
30 |
31 | When using LightningTip behind a proxy make sure the proxy supports [EventSource](https://developer.mozilla.org/en-US/docs/Web/API/EventSource). Without support for it the users will not see the "Thank you for your tip!" screen.
32 |
33 | That's it! The only two things you need to take care about is keeping the LND node online and making sure that your incoming channels are sufficiently funded to receive tips. LightningTip will take care of everything else.
34 |
35 | ## How to build
36 |
37 | First of all make sure [Golang](https://golang.org/) version 1.11 or newer is correctly installed.
38 |
39 | ```bash
40 | go get -d github.com/michael1011/lightningtip
41 | cd $GOPATH/src/github.com/michael1011/lightningtip
42 |
43 | make && make install
44 | ```
45 |
46 | To start run `$GOPATH/bin/lightningtip` or follow the instructions below to setup a service to run LightningTip automatically.
47 |
48 | ## Upgrading
49 |
50 | Make sure you stop any running LightningTip process before upgrading, then pull from source as follows:
51 |
52 | ```bash
53 | cd $GOPATH/src/github.com/michael1011/lightningtip
54 | git pull
55 |
56 | make && make install
57 | ```
58 |
59 | ## Starting LightningTip Automatically
60 |
61 | LightningTip can be started automatically via Systemd, or Supervisord, as outlined in the following wiki documentation:
62 |
63 | * [Running LightningTip with systemd](https://github.com/michael1011/lightningtip/wiki/Running-LightningTip-with-systemd)
64 | * [Running LightningTip with supervisord](https://github.com/michael1011/lightningtip/wiki/Running-LightningTip-with-supervisord)
65 |
66 | ## Reverse Proxy Recipes
67 |
68 | In instances where the default LightningTip SSL configuration options are not working, you may want to explore running a reverse proxy to LightningTip as outlined in the following wiki documentation:
69 |
70 | * [LightningTip via Apache2 reverse proxy](https://github.com/michael1011/lightningtip/wiki/LightningTip-via-Apache2-reverse-proxy)
71 | * [LightningTip via Nginx reverse proxy](https://github.com/michael1011/lightningtip/wiki/LightningTip-via-Nginx-reverse-proxy)
72 |
--------------------------------------------------------------------------------
/notifications/mail.go:
--------------------------------------------------------------------------------
1 | package notifications
2 |
3 | import (
4 | "crypto/tls"
5 | "fmt"
6 | "net"
7 | "net/smtp"
8 | "os/exec"
9 | "strconv"
10 | )
11 |
12 | // Mail contains all values needed to be able to send a mail
13 | type Mail struct {
14 | Recipient string `long:"recipient" Description:"Email address to which notifications get sent"`
15 | Sender string `long:"sender" Description:"Email address from which notifications get sent"`
16 |
17 | SMTPServer string `long:"server" Description:"SMTP server with port for sending mails"`
18 |
19 | SMTPSSL bool `long:"ssl" Description:"Whether SSL should be used or not"`
20 | SMTPUser string `long:"user" Description:"User for authenticating the SMTP connection"`
21 | SMTPPassword string `long:"password" Description:"Password for authenticating the SMTP connection"`
22 | }
23 |
24 | const sendmail = "/usr/bin/mail"
25 |
26 | const newLine = "\r\n"
27 |
28 | const subject = "You received a tip"
29 |
30 | // SendMail sends a mail
31 | func (mail *Mail) SendMail(amount int64, message string) {
32 | body := "You received a tip of " + strconv.FormatInt(amount, 10) + " satoshis"
33 |
34 | if message != "" {
35 | body += " with the message \"" + message + "\""
36 | }
37 |
38 | if mail.SMTPServer == "" {
39 | // "mail" command will be used for sending
40 | mail.sendMailCommand(body)
41 |
42 | } else {
43 | // SMTP server will be used
44 | mail.sendMailSMTP(body)
45 | }
46 |
47 | }
48 |
49 | // Sends a mail with the "mail" command
50 | func (mail *Mail) sendMailCommand(body string) {
51 | var cmd *exec.Cmd
52 |
53 | if mail.Sender == "" {
54 | cmd = exec.Command(sendmail, "-s", subject, mail.Recipient)
55 |
56 | } else {
57 | // Append "From" header
58 | cmd = exec.Command(sendmail, "-s", subject, "-a", "From: "+mail.Sender, mail.Recipient)
59 | }
60 |
61 | writer, err := cmd.StdinPipe()
62 |
63 | if err == nil {
64 | err = cmd.Start()
65 |
66 | if err == nil {
67 | _, err = writer.Write([]byte(body))
68 |
69 | if err == nil {
70 | err = writer.Close()
71 |
72 | if err == nil {
73 | err = cmd.Wait()
74 |
75 | if err == nil {
76 | logSent()
77 |
78 | return
79 | }
80 |
81 | }
82 |
83 | }
84 |
85 | }
86 |
87 | }
88 |
89 | logSendingFailed(err)
90 | }
91 |
92 | func (mail *Mail) sendMailSMTP(body string) {
93 | // Because the SMTP method doesn't have a dedicated field for the subject
94 | // it will be in the body of the message
95 | body = "Subject: " + subject + newLine + body
96 |
97 | var auth smtp.Auth
98 |
99 | host, _, err := net.SplitHostPort(mail.SMTPServer)
100 |
101 | if err != nil {
102 | log.Error("Failed to parse host of SMTP server: " + mail.SMTPServer)
103 |
104 | return
105 | }
106 |
107 | if mail.SMTPUser != "" {
108 | // If there are credentials they are used
109 | auth = smtp.PlainAuth(
110 | "",
111 | mail.SMTPUser,
112 | mail.SMTPPassword,
113 | host,
114 | )
115 |
116 | }
117 |
118 | if mail.SMTPSSL {
119 | body = "From: " + mail.Sender + newLine + "To: " + mail.Recipient + newLine + body
120 |
121 | tlsConfig := &tls.Config{
122 | ServerName: host,
123 | InsecureSkipVerify: true,
124 | }
125 |
126 | con, err := tls.Dial("tcp", mail.SMTPServer, tlsConfig)
127 |
128 | defer con.Close()
129 |
130 | if err != nil {
131 | log.Error("Failed to connect to connect to SMTP server: " + fmt.Sprint(err))
132 |
133 | return
134 | }
135 |
136 | client, err := smtp.NewClient(con, host)
137 |
138 | defer client.Close()
139 |
140 | if err != nil {
141 | log.Error("Failed to create SMTP client: " + fmt.Sprint(err))
142 |
143 | return
144 | }
145 |
146 | err = client.Auth(auth)
147 |
148 | if err != nil {
149 | log.Error("Failed to authenticate SMTP client: " + fmt.Sprint(err))
150 |
151 | return
152 | }
153 |
154 | err = client.Mail(mail.Sender)
155 | err = client.Rcpt(mail.Recipient)
156 |
157 | writer, err := client.Data()
158 |
159 | defer writer.Close()
160 |
161 | if err == nil {
162 | _, err = writer.Write([]byte(body))
163 | }
164 |
165 | if err == nil {
166 | logSent()
167 |
168 | } else {
169 | logSendingFailed(err)
170 | }
171 |
172 | } else {
173 | err := smtp.SendMail(
174 | mail.SMTPServer,
175 | auth,
176 | mail.Sender,
177 | []string{mail.Recipient},
178 | []byte(body),
179 | )
180 |
181 | if err == nil {
182 | logSent()
183 |
184 | } else {
185 | logSendingFailed(err)
186 | }
187 |
188 | }
189 |
190 | }
191 |
192 | func logSent() {
193 | log.Debug("Sent email")
194 | }
195 |
196 | func logSendingFailed(err error) {
197 | log.Error("Failed to send mail: " + fmt.Sprint(err))
198 | }
199 |
--------------------------------------------------------------------------------
/config.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "io/ioutil"
6 | "os"
7 | "os/user"
8 | "path"
9 | "path/filepath"
10 | "runtime"
11 | "strings"
12 |
13 | flags "github.com/jessevdk/go-flags"
14 | "github.com/michael1011/lightningtip/backends"
15 | "github.com/michael1011/lightningtip/database"
16 | "github.com/michael1011/lightningtip/notifications"
17 | "github.com/michael1011/lightningtip/version"
18 | logging "github.com/op/go-logging"
19 | )
20 |
21 | const (
22 | defaultConfigFile = "lightningTip.conf"
23 |
24 | defaultDataDir = "LightningTip"
25 |
26 | defaultLogFile = "lightningTip.log"
27 | defaultLogLevel = "info"
28 |
29 | defaultDatabaseFile = "tips.db"
30 |
31 | defaultRESTHost = "0.0.0.0:8081"
32 | defaultTLSCertFile = ""
33 | defaultTLSKeyFile = ""
34 |
35 | defaultAccessDomain = ""
36 |
37 | defaultTipExpiry = 3600
38 |
39 | defaultReconnectInterval = 0
40 | defaultKeepaliveInterval = 0
41 |
42 | defaultLndGRPCHost = "localhost:10009"
43 | defaultLndCertFile = "tls.cert"
44 | defaultMacaroonFile = "invoice.macaroon"
45 |
46 | defaultRecipient = ""
47 | defaultSender = ""
48 |
49 | defaultSTMPServer = ""
50 | defaultSTMPSSL = false
51 | defaultSTMPUser = ""
52 | defaultSTMPPassword = ""
53 | )
54 |
55 | type helpOptions struct {
56 | ShowHelp bool `long:"help" short:"h" description:"Display this help message"`
57 | ShowVersion bool `long:"version" short:"v" description:"Display version and exit"`
58 | }
59 |
60 | type config struct {
61 | ConfigFile string `long:"config" description:"Location of the config file"`
62 |
63 | DataDir string `long:"datadir" description:"Location of the data stored by LightningTip"`
64 |
65 | LogFile string `long:"logfile" description:"Location of the log file"`
66 | LogLevel string `long:"loglevel" description:"Log level: debug, info, warning, error"`
67 |
68 | DatabaseFile string `long:"databasefile" description:"Location of the database file to store settled invoices"`
69 |
70 | RESTHost string `long:"resthost" description:"Host for the REST interface of LightningTip"`
71 | TLSCertFile string `long:"tlscertfile" description:"Certificate for using LightningTip via HTTPS"`
72 | TLSKeyFile string `long:"tlskeyfile" description:"Certificate for using LightningTip via HTTPS"`
73 |
74 | AccessDomain string `long:"accessdomain" description:"The domain you are using LightningTip from"`
75 |
76 | TipExpiry int64 `long:"tipexpiry" description:"Invoice expiry time in seconds"`
77 |
78 | ReconnectInterval int64 `long:"reconnectinterval" description:"Reconnect interval to LND in seconds"`
79 | KeepAliveInterval int64 `long:"keepaliveinterval" description:"Send a dummy request to LND to prevent timeouts "`
80 |
81 | LND *backends.LND `group:"LND" namespace:"lnd"`
82 |
83 | Mail *notifications.Mail `group:"Mail" namespace:"mail"`
84 |
85 | Help *helpOptions `group:"Help Options"`
86 | }
87 |
88 | var cfg config
89 |
90 | var backend backends.Backend
91 |
92 | func initConfig() {
93 | cfg = config{
94 | ConfigFile: path.Join(getDefaultDataDir(), defaultConfigFile),
95 |
96 | DataDir: getDefaultDataDir(),
97 |
98 | LogFile: path.Join(getDefaultDataDir(), defaultLogFile),
99 | LogLevel: defaultLogLevel,
100 |
101 | DatabaseFile: path.Join(getDefaultDataDir(), defaultDatabaseFile),
102 |
103 | RESTHost: defaultRESTHost,
104 | TLSCertFile: defaultTLSCertFile,
105 | TLSKeyFile: defaultTLSKeyFile,
106 |
107 | AccessDomain: defaultAccessDomain,
108 |
109 | TipExpiry: defaultTipExpiry,
110 |
111 | ReconnectInterval: defaultReconnectInterval,
112 | KeepAliveInterval: defaultKeepaliveInterval,
113 |
114 | LND: &backends.LND{
115 | GRPCHost: defaultLndGRPCHost,
116 | CertFile: path.Join(getDefaultLndDir(), defaultLndCertFile),
117 | MacaroonFile: getDefaultMacaroon(),
118 | },
119 |
120 | Mail: ¬ifications.Mail{
121 | Recipient: defaultRecipient,
122 | Sender: defaultSender,
123 |
124 | SMTPServer: defaultSTMPServer,
125 | SMTPSSL: defaultSTMPSSL,
126 | SMTPUser: defaultSTMPUser,
127 | SMTPPassword: defaultSTMPPassword,
128 | },
129 | }
130 |
131 | // Ignore unknown flags the first time parsing command line flags to prevent showing the unknown flag error twice
132 | parser := flags.NewParser(&cfg, flags.IgnoreUnknown)
133 | parser.Parse()
134 |
135 | errFile := flags.IniParse(cfg.ConfigFile, &cfg)
136 |
137 | // If the user just wants to see the version initializing everything else is irrelevant
138 | if cfg.Help.ShowVersion {
139 | version.PrintVersion()
140 | os.Exit(0)
141 | }
142 |
143 | // If the user just wants to see the help message
144 | if cfg.Help.ShowHelp {
145 | parser.WriteHelp(os.Stdout)
146 | os.Exit(0)
147 | }
148 |
149 | // Parse flags again to override config file
150 | _, err := flags.Parse(&cfg)
151 |
152 | // Default log level if parsing fails
153 | logLevel := logging.DEBUG
154 |
155 | switch strings.ToLower(cfg.LogLevel) {
156 | case "info":
157 | logLevel = logging.INFO
158 |
159 | case "warning":
160 | logLevel = logging.WARNING
161 |
162 | case "error":
163 | logLevel = logging.ERROR
164 | }
165 |
166 | // Create data directory
167 | var errDataDir error
168 | var dataDirCreated bool
169 |
170 | if _, err := os.Stat(getDefaultDataDir()); os.IsNotExist(err) {
171 | errDataDir = os.Mkdir(getDefaultDataDir(), 0700)
172 |
173 | dataDirCreated = true
174 | }
175 |
176 | errLogFile := initLogger(cfg.LogFile, logLevel)
177 |
178 | // Show error messages
179 | if err != nil {
180 | log.Error("Failed to parse command line flags")
181 | }
182 |
183 | if errDataDir != nil {
184 | log.Error("Could not create data directory")
185 | log.Debug("Data directory path: " + getDefaultDataDir())
186 |
187 | } else if dataDirCreated {
188 | log.Debug("Created data directory: " + getDefaultDataDir())
189 | }
190 |
191 | if errFile != nil {
192 | log.Warning("Failed to parse config file: " + fmt.Sprint(errFile))
193 | } else {
194 | log.Debug("Parsed config file: " + cfg.ConfigFile)
195 | }
196 |
197 | if errLogFile != nil {
198 | log.Error("Failed to initialize log file: " + fmt.Sprint(err))
199 |
200 | } else {
201 | log.Debug("Initialized log file: " + cfg.LogFile)
202 | }
203 |
204 | database.UseLogger(*log)
205 | backends.UseLogger(*log)
206 | notifications.UseLogger(*log)
207 |
208 | backend = cfg.LND
209 | }
210 |
211 | func getDefaultDataDir() (dir string) {
212 | homeDir := getHomeDir()
213 |
214 | switch runtime.GOOS {
215 | case "windows":
216 | fallthrough
217 |
218 | case "darwin":
219 | dir = path.Join(homeDir, defaultDataDir)
220 |
221 | default:
222 | dir = path.Join(homeDir, "."+strings.ToLower(defaultDataDir))
223 | }
224 |
225 | return cleanPath(dir)
226 | }
227 |
228 | // If the mainnet macaroon does exists it is preffered over all others
229 | func getDefaultMacaroon() string {
230 | networksDir := filepath.Join(getDefaultLndDir(), "/data/chain/bitcoin/")
231 | mainnetMacaroon := filepath.Join(networksDir, "mainnet/", defaultMacaroonFile)
232 |
233 | if _, err := os.Stat(mainnetMacaroon); err == nil {
234 | return mainnetMacaroon
235 | }
236 |
237 | networks, err := ioutil.ReadDir(networksDir)
238 |
239 | if err == nil && len(networks) != 0 {
240 | for _, subDir := range networks {
241 | if subDir.IsDir() {
242 | return filepath.Join(networksDir, networks[0].Name(), defaultMacaroonFile)
243 | }
244 | }
245 | }
246 |
247 | return ""
248 | }
249 |
250 | func getDefaultLndDir() (dir string) {
251 | homeDir := getHomeDir()
252 |
253 | switch runtime.GOOS {
254 | case "darwin":
255 | fallthrough
256 |
257 | case "windows":
258 | dir = path.Join(homeDir, "Lnd")
259 |
260 | default:
261 | dir = path.Join(homeDir, ".lnd")
262 | }
263 |
264 | return cleanPath(dir)
265 | }
266 |
267 | func getHomeDir() (dir string) {
268 | usr, err := user.Current()
269 |
270 | if err == nil {
271 | switch runtime.GOOS {
272 | case "darwin":
273 | dir = path.Join(usr.HomeDir, "Library/Application Support")
274 |
275 | case "windows":
276 | dir = path.Join(usr.HomeDir, "AppData/Local")
277 |
278 | default:
279 | dir = usr.HomeDir
280 | }
281 |
282 | }
283 |
284 | return cleanPath(dir)
285 | }
286 |
287 | func cleanPath(path string) string {
288 | path = filepath.Clean(os.ExpandEnv(path))
289 |
290 | return strings.Replace(path, "\\", "/", -1)
291 | }
292 |
--------------------------------------------------------------------------------
/lightningtip.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "encoding/json"
5 | "fmt"
6 | "io/ioutil"
7 | "net/http"
8 | "os"
9 | "strconv"
10 | "strings"
11 | "time"
12 |
13 | "github.com/donovanhide/eventsource"
14 | "github.com/michael1011/lightningtip/database"
15 | )
16 |
17 | // PendingInvoice is for keeping alist of unpaid invoices
18 | type PendingInvoice struct {
19 | Invoice string
20 | Amount int64
21 | Message string
22 | RHash string
23 | Expiry time.Time
24 | }
25 |
26 | const eventChannel = "invoiceSettled"
27 |
28 | const couldNotParseError = "Could not parse values from request"
29 |
30 | var eventSrv *eventsource.Server
31 |
32 | var pendingInvoices []PendingInvoice
33 |
34 | // To use the pendingInvoice type as event for the EventSource stream
35 |
36 | // Id gets the ID of the event which is not neede in our scenario
37 | func (pending PendingInvoice) Id() string { return "" } // nolint: golint
38 |
39 | // Event is for using a different type of event than "data"
40 | func (pending PendingInvoice) Event() string { return "" }
41 |
42 | // Data tells EventSource what data to write
43 | func (pending PendingInvoice) Data() string { return pending.RHash }
44 |
45 | type invoiceRequest struct {
46 | Amount int64
47 | Message string
48 | }
49 |
50 | type invoiceResponse struct {
51 | Invoice string
52 | RHash string
53 | Expiry int64
54 | }
55 |
56 | type invoiceSettledRequest struct {
57 | RHash string
58 | }
59 |
60 | type invoiceSettledResponse struct {
61 | Settled bool
62 | }
63 |
64 | type errorResponse struct {
65 | Error string
66 | }
67 |
68 | // TODO: add version flag
69 | // TODO: don't start when "--help" flag is provided
70 | func main() {
71 | initLog()
72 |
73 | initConfig()
74 |
75 | dbErr := database.InitDatabase(cfg.DatabaseFile)
76 |
77 | if dbErr != nil {
78 | log.Error("Failed to initialize database: " + fmt.Sprint(dbErr))
79 |
80 | os.Exit(1)
81 |
82 | } else {
83 | log.Debug("Opened SQLite database: " + cfg.DatabaseFile)
84 | }
85 |
86 | err := backend.Connect()
87 |
88 | if err == nil {
89 | log.Info("Starting EventSource stream")
90 |
91 | eventSrv = eventsource.NewServer()
92 |
93 | defer eventSrv.Close()
94 |
95 | http.Handle("/", handleHeaders(notFoundHandler))
96 | http.Handle("/getinvoice", handleHeaders(getInvoiceHandler))
97 | http.Handle("/eventsource", handleHeaders(eventSrv.Handler(eventChannel)))
98 |
99 | // Alternative for browsers which don't support EventSource (Internet Explorer and Edge)
100 | http.Handle("/invoicesettled", handleHeaders(invoiceSettledHandler))
101 |
102 | log.Debug("Starting ticker to clear expired invoices")
103 |
104 | // A bit longer than the expiry time to make sure the invoice doesn't show as settled if it isn't (affects just invoiceSettledHandler)
105 | expiryInterval := time.Duration(cfg.TipExpiry + 10)
106 | expiryTicker := time.Tick(expiryInterval * time.Second)
107 |
108 | go func() {
109 | for {
110 | select {
111 | case <-expiryTicker:
112 | now := time.Now()
113 |
114 | for index := len(pendingInvoices) - 1; index >= 0; index-- {
115 | invoice := pendingInvoices[index]
116 |
117 | if now.Sub(invoice.Expiry) > 0 {
118 | log.Debug("Invoice expired: " + invoice.Invoice)
119 |
120 | pendingInvoices = append(pendingInvoices[:index], pendingInvoices[index+1:]...)
121 | }
122 |
123 | }
124 |
125 | }
126 |
127 | }
128 |
129 | }()
130 |
131 | go func() {
132 | subscribeToInvoices()
133 | }()
134 |
135 | if cfg.KeepAliveInterval > 0 {
136 | log.Debug("Starting ticker to send keepalive requests")
137 |
138 | interval := time.Duration(cfg.KeepAliveInterval)
139 | keepAliveTicker := time.Tick(interval * time.Second)
140 |
141 | go func() {
142 | for {
143 | select {
144 | case <-keepAliveTicker:
145 | backend.KeepAliveRequest()
146 | }
147 | }
148 | }()
149 |
150 | }
151 |
152 | log.Info("Starting HTTP server")
153 |
154 | go func() {
155 | var err error
156 |
157 | if cfg.TLSCertFile != "" && cfg.TLSKeyFile != "" {
158 | err = http.ListenAndServeTLS(cfg.RESTHost, cfg.TLSCertFile, cfg.TLSKeyFile, nil)
159 |
160 | } else {
161 | err = http.ListenAndServe(cfg.RESTHost, nil)
162 | }
163 |
164 | if err != nil {
165 | log.Errorf("Failed to start HTTP server: " + fmt.Sprint(err))
166 |
167 | os.Exit(1)
168 | }
169 |
170 | }()
171 |
172 | select {}
173 |
174 | }
175 |
176 | }
177 |
178 | func subscribeToInvoices() {
179 | log.Info("Subscribing to invoices")
180 |
181 | err := backend.SubscribeInvoices(publishInvoiceSettled, rescanPendingInvoices)
182 |
183 | log.Error("Failed to subscribe to invoices: " + fmt.Sprint(err))
184 |
185 | if err != nil {
186 | if cfg.ReconnectInterval != 0 {
187 | reconnectToBackend()
188 |
189 | } else {
190 | os.Exit(1)
191 | }
192 |
193 | }
194 |
195 | }
196 |
197 | func reconnectToBackend() {
198 | time.Sleep(time.Duration(cfg.ReconnectInterval) * time.Second)
199 |
200 | log.Info("Trying to reconnect to LND")
201 |
202 | backend = cfg.LND
203 |
204 | err := backend.Connect()
205 |
206 | if err == nil {
207 | err = backend.KeepAliveRequest()
208 |
209 | // The default macaroon file used by LightningTip "invoice.macaroon" allows only creating and checking status of invoices
210 | // The keep alive request doesn't have to be successful as long as it can establish a connection to LND
211 | if err == nil || fmt.Sprint(err) == "rpc error: code = Unknown desc = permission denied" {
212 | log.Info("Reconnected to LND")
213 |
214 | subscribeToInvoices()
215 | }
216 |
217 | }
218 |
219 | log.Info("Connection failed")
220 |
221 | log.Debug(fmt.Sprint(err))
222 |
223 | reconnectToBackend()
224 | }
225 |
226 | func rescanPendingInvoices() {
227 | if len(pendingInvoices) > 0 {
228 | log.Debug("Rescanning pending invoices")
229 |
230 | for _, invoice := range pendingInvoices {
231 | settled, err := backend.InvoiceSettled(invoice.RHash)
232 |
233 | if err == nil {
234 | if settled {
235 | publishInvoiceSettled(invoice.Invoice)
236 | }
237 |
238 | } else {
239 | log.Warning("Failed to check if invoice settled: " + fmt.Sprint(err))
240 | }
241 |
242 | }
243 |
244 | }
245 |
246 | }
247 |
248 | func publishInvoiceSettled(invoice string) {
249 | for index, settled := range pendingInvoices {
250 | if settled.Invoice == invoice {
251 | log.Info("Invoice settled: " + invoice)
252 |
253 | eventSrv.Publish([]string{eventChannel}, settled)
254 |
255 | database.AddSettledInvoice(settled.Amount, settled.Message)
256 |
257 | if cfg.Mail.Recipient != "" {
258 | cfg.Mail.SendMail(settled.Amount, settled.Message)
259 | }
260 |
261 | pendingInvoices = append(pendingInvoices[:index], pendingInvoices[index+1:]...)
262 |
263 | break
264 | }
265 |
266 | }
267 |
268 | }
269 |
270 | func invoiceSettledHandler(writer http.ResponseWriter, request *http.Request) {
271 | errorMessage := couldNotParseError
272 |
273 | if request.Method == http.MethodPost {
274 | var body invoiceSettledRequest
275 |
276 | data, _ := ioutil.ReadAll(request.Body)
277 |
278 | err := json.Unmarshal(data, &body)
279 |
280 | if err == nil {
281 | if body.RHash != "" {
282 | settled := true
283 |
284 | for _, pending := range pendingInvoices {
285 | if pending.RHash == body.RHash {
286 | settled = false
287 |
288 | break
289 | }
290 |
291 | }
292 |
293 | writer.Write(marshalJSON(invoiceSettledResponse{
294 | Settled: settled,
295 | }))
296 |
297 | return
298 |
299 | }
300 |
301 | }
302 |
303 | }
304 |
305 | log.Error(errorMessage)
306 |
307 | writeError(writer, errorMessage)
308 | }
309 |
310 | func getInvoiceHandler(writer http.ResponseWriter, request *http.Request) {
311 | errorMessage := couldNotParseError
312 |
313 | if request.Method == http.MethodPost {
314 | var body invoiceRequest
315 |
316 | data, _ := ioutil.ReadAll(request.Body)
317 |
318 | err := json.Unmarshal(data, &body)
319 |
320 | if err == nil {
321 | if body.Amount != 0 {
322 | invoice, paymentHash, err := backend.GetInvoice(body.Message, body.Amount, cfg.TipExpiry)
323 |
324 | if err == nil {
325 | logMessage := "Created invoice with amount of " + strconv.FormatInt(body.Amount, 10) + " satoshis"
326 |
327 | if body.Message != "" {
328 | // Deletes new lines at the end of the messages
329 | body.Message = strings.TrimSuffix(body.Message, "\n")
330 |
331 | logMessage += " with message \"" + body.Message + "\""
332 | }
333 |
334 | expiryDuration := time.Duration(cfg.TipExpiry) * time.Second
335 |
336 | log.Info(logMessage)
337 |
338 | pendingInvoices = append(pendingInvoices, PendingInvoice{
339 | Invoice: invoice,
340 | Amount: body.Amount,
341 | Message: body.Message,
342 | RHash: string(paymentHash),
343 | Expiry: time.Now().Add(expiryDuration),
344 | })
345 |
346 | writer.Write(marshalJSON(invoiceResponse{
347 | Invoice: invoice,
348 | RHash: paymentHash,
349 | Expiry: cfg.TipExpiry,
350 | }))
351 |
352 | return
353 | }
354 |
355 | errorMessage = "Failed to create invoice"
356 |
357 | // This is way too hacky
358 | // Maybe a cast to the gRPC error and get its error message directly
359 | if fmt.Sprint(err)[:47] == "rpc error: code = Unknown desc = memo too large" {
360 | errorMessage += ": message too long"
361 | }
362 |
363 | }
364 |
365 | }
366 |
367 | }
368 |
369 | log.Error(errorMessage)
370 |
371 | writeError(writer, errorMessage)
372 | }
373 |
374 | func notFoundHandler(writer http.ResponseWriter, request *http.Request) {
375 | if request.RequestURI == "/" {
376 | writeError(writer, "This is an API to connect LND and your website. You should not open this in your browser")
377 |
378 | } else {
379 | writeError(writer, "Not found")
380 | }
381 | }
382 |
383 | func handleHeaders(handler func(w http.ResponseWriter, r *http.Request)) http.Handler {
384 | return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
385 | if cfg.AccessDomain != "" {
386 | writer.Header().Add("Access-Control-Allow-Origin", cfg.AccessDomain)
387 | }
388 |
389 | handler(writer, request)
390 | })
391 | }
392 |
393 | func writeError(writer http.ResponseWriter, message string) {
394 | writer.WriteHeader(http.StatusBadRequest)
395 |
396 | writer.Write(marshalJSON(errorResponse{
397 | Error: message,
398 | }))
399 | }
400 |
401 | func marshalJSON(data interface{}) []byte {
402 | response, _ := json.MarshalIndent(data, "", " ")
403 |
404 | return response
405 | }
406 |
--------------------------------------------------------------------------------
/frontend/lightningTip.js:
--------------------------------------------------------------------------------
1 | // Edit this variable if you are not running LightningTip on the same domain or IP address as your webserver or not on port 8081
2 | // Don't forget the "/" at the end!
3 | var requestUrl = window.location.protocol + "//" + window.location.hostname + ":8081/";
4 |
5 | // Used for development
6 | if (window.location.protocol === "file:") {
7 | requestUrl = "http://localhost:8081/"
8 | }
9 |
10 | // To prohibit multiple requests at the same time
11 | var requestPending = false;
12 |
13 | var invoice;
14 | var qrCode;
15 |
16 | var defaultGetInvoice;
17 |
18 | // Data capacities for QR codes with mode byte and error correction level L (7%)
19 | // Shortest invoice: 194 characters
20 | // Longest invoice: 1223 characters (as far as I know)
21 | var qrCodeDataCapacities = [
22 | {"typeNumber": 9, "capacity": 230},
23 | {"typeNumber": 10, "capacity": 271},
24 | {"typeNumber": 11, "capacity": 321},
25 | {"typeNumber": 12, "capacity": 367},
26 | {"typeNumber": 13, "capacity": 425},
27 | {"typeNumber": 14, "capacity": 458},
28 | {"typeNumber": 15, "capacity": 520},
29 | {"typeNumber": 16, "capacity": 586},
30 | {"typeNumber": 17, "capacity": 644},
31 | {"typeNumber": 18, "capacity": 718},
32 | {"typeNumber": 19, "capacity": 792},
33 | {"typeNumber": 20, "capacity": 858},
34 | {"typeNumber": 21, "capacity": 929},
35 | {"typeNumber": 22, "capacity": 1003},
36 | {"typeNumber": 23, "capacity": 1091},
37 | {"typeNumber": 24, "capacity": 1171},
38 | {"typeNumber": 25, "capacity": 1273}
39 | ];
40 |
41 | // TODO: solve this without JavaScript
42 | // Fixes weird bug which moved the button up one pixel when its content was changed
43 | window.onload = function () {
44 | var button = document.getElementById("lightningTipGetInvoice");
45 |
46 | button.style.height = (button.clientHeight + 1) + "px";
47 | button.style.width = (button.clientWidth + 1) + "px";
48 | };
49 |
50 | // TODO: show invoice even if JavaScript is disabled
51 | // TODO: fix scaling on phones
52 | // TODO: show price in dollar?
53 | function getInvoice() {
54 | if (!requestPending) {
55 | requestPending = true;
56 |
57 | var tipValue = document.getElementById("lightningTipAmount");
58 |
59 | if (tipValue.value !== "") {
60 | if (!isNaN(tipValue.value)) {
61 | var data = JSON.stringify({"Amount": parseInt(tipValue.value), "Message": document.getElementById("lightningTipMessage").innerText});
62 |
63 | var request = new XMLHttpRequest();
64 |
65 | request.onreadystatechange = function () {
66 | if (request.readyState === 4) {
67 | try {
68 | var json = JSON.parse(request.responseText);
69 |
70 | if (request.status === 200) {
71 | console.log("Got invoice: " + json.Invoice);
72 | console.log("Invoice expires in: " + json.Expiry);
73 | console.log("Got rHash of invoice: " + json.RHash);
74 |
75 | console.log("Starting listening for invoice to get settled");
76 |
77 | listenInvoiceSettled(json.RHash);
78 |
79 | invoice = json.Invoice;
80 |
81 | // Update UI
82 | var wrapper = document.getElementById("lightningTip");
83 |
84 | wrapper.innerHTML = "Your tip request";
85 | wrapper.innerHTML += "";
86 | wrapper.innerHTML += "";
87 |
88 | wrapper.innerHTML += "";
93 |
94 | starTimer(json.Expiry, document.getElementById("lightningTipExpiry"));
95 |
96 | // Fixes bug which caused the content of #lightningTipTools to be visually outside of #lightningTip
97 | document.getElementById("lightningTipTools").style.height = document.getElementById("lightningTipCopy").clientHeight + "px";
98 |
99 | document.getElementById("lightningTipOpen").onclick = function () {
100 | location.href = "lightning:" + json.Invoice;
101 | };
102 |
103 | showQRCode();
104 |
105 | } else {
106 | showErrorMessage(json.Error);
107 | }
108 |
109 | } catch (exception) {
110 | console.error(exception);
111 |
112 | showErrorMessage("Failed to reach backend");
113 | }
114 |
115 | requestPending = false;
116 | }
117 |
118 | };
119 |
120 | request.open("POST", requestUrl + "getinvoice", true);
121 | request.send(data);
122 |
123 | var button = document.getElementById("lightningTipGetInvoice");
124 |
125 | defaultGetInvoice = button.innerHTML;
126 |
127 | button.innerHTML = "";
128 |
129 | } else {
130 | showErrorMessage("Tip amount must be a number");
131 | }
132 |
133 | } else {
134 | showErrorMessage("No tip amount set");
135 | }
136 |
137 | } else {
138 | console.warn("Last request still pending");
139 | }
140 |
141 | }
142 |
143 | function listenInvoiceSettled(rHash) {
144 | try {
145 | var eventSrc = new EventSource(requestUrl + "eventsource");
146 |
147 | eventSrc.onmessage = function (event) {
148 | if (event.data === rHash) {
149 | console.log("Invoice settled");
150 |
151 | eventSrc.close();
152 |
153 | showThankYouScreen();
154 | }
155 |
156 | };
157 |
158 | } catch (e) {
159 | console.error(e);
160 | console.warn("Your browser does not support EventSource. Sending a request to the server every two second to check if the invoice settled");
161 |
162 | var interval = setInterval(function () {
163 | if (!requestPending) {
164 | requestPending = true;
165 |
166 | var request = new XMLHttpRequest();
167 |
168 | request.onreadystatechange = function () {
169 | if (request.readyState === 4) {
170 | if (request.status === 200) {
171 | var json = JSON.parse(request.responseText);
172 |
173 | if (json.Settled) {
174 | console.log("Invoice settled");
175 |
176 | clearInterval(interval);
177 |
178 | showThankYouScreen();
179 | }
180 |
181 | }
182 |
183 | requestPending = false;
184 | }
185 |
186 | };
187 |
188 | request.open("POST", requestUrl + "invoicesettled", true);
189 | request.send(JSON.stringify({"RHash": rHash}));
190 | }
191 |
192 | }, 2000);
193 |
194 | }
195 |
196 | }
197 |
198 | function showThankYouScreen() {
199 | var wrapper = document.getElementById("lightningTip");
200 |
201 | wrapper.innerHTML = "⚡
"; 202 | wrapper.innerHTML += "Thank you for your tip!"; 203 | } 204 | 205 | function starTimer(duration, element) { 206 | showTimer(duration, element); 207 | 208 | var interval = setInterval(function () { 209 | if (duration > 1) { 210 | duration--; 211 | 212 | showTimer(duration, element); 213 | 214 | } else { 215 | showExpired(); 216 | 217 | clearInterval(interval); 218 | } 219 | 220 | }, 1000); 221 | 222 | } 223 | 224 | function showTimer(duration, element) { 225 | var seconds = Math.floor(duration % 60); 226 | var minutes = Math.floor((duration / 60) % 60); 227 | var hours = Math.floor((duration / (60 * 60)) % 24); 228 | 229 | seconds = addLeadingZeros(seconds); 230 | minutes = addLeadingZeros(minutes); 231 | 232 | if (hours > 0) { 233 | element.innerHTML = hours + ":" + minutes + ":" + seconds; 234 | 235 | } else { 236 | element.innerHTML = minutes + ":" + seconds; 237 | } 238 | 239 | } 240 | 241 | function showExpired() { 242 | var wrapper = document.getElementById("lightningTip"); 243 | 244 | wrapper.innerHTML = "⚡
"; 245 | wrapper.innerHTML += "Your tip request expired!"; 246 | } 247 | 248 | function addLeadingZeros(value) { 249 | return ("0" + value).slice(-2); 250 | } 251 | 252 | function showQRCode() { 253 | var element = document.getElementById("lightningTipQR"); 254 | 255 | createQRCode(); 256 | 257 | element.innerHTML = qrCode; 258 | 259 | var size = document.getElementById("lightningTipInvoice").clientWidth + "px"; 260 | 261 | var qrElement = element.children[0]; 262 | 263 | qrElement.style.height = size; 264 | qrElement.style.width = size; 265 | } 266 | 267 | function createQRCode() { 268 | var invoiceLength = invoice.length; 269 | 270 | // Just in case an invoice bigger than expected gets created 271 | var typeNumber = 26; 272 | 273 | for (var i = 0; i < qrCodeDataCapacities.length; i++) { 274 | var dataCapacity = qrCodeDataCapacities[i]; 275 | 276 | if (invoiceLength < dataCapacity.capacity) { 277 | typeNumber = dataCapacity.typeNumber; 278 | 279 | break; 280 | } 281 | 282 | } 283 | 284 | console.log("Creating QR code with type number: " + typeNumber); 285 | 286 | var qr = qrcode(typeNumber, "L"); 287 | 288 | qr.addData(invoice); 289 | qr.make(); 290 | 291 | qrCode = qr.createImgTag(6, 6); 292 | } 293 | 294 | function copyInvoiceToClipboard() { 295 | var element = document.getElementById("lightningTipInvoice"); 296 | 297 | element.select(); 298 | 299 | document.execCommand('copy'); 300 | 301 | console.log("Copied invoice to clipboard"); 302 | } 303 | 304 | function showErrorMessage(message) { 305 | requestPending = false; 306 | 307 | console.error(message); 308 | 309 | var error = document.getElementById("lightningTipError"); 310 | 311 | error.parentElement.style.marginTop = "0.5em"; 312 | error.innerHTML = message; 313 | 314 | var button = document.getElementById("lightningTipGetInvoice"); 315 | 316 | // Only necessary if it has a child (div with class spinner) 317 | if (button.children.length !== 0) { 318 | button.innerHTML = defaultGetInvoice; 319 | } 320 | 321 | } 322 | 323 | function divRestorePlaceholder(element) { 324 | //