Compare commits
115 Commits
1.0.0-pre
...
dependabot
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3cd6b82267 | ||
| 530c7c0942 | |||
|
|
23a134b28e | ||
| c6dc9308c3 | |||
|
|
897217995a | ||
| a21e8b96b7 | |||
| a383a747ef | |||
|
|
9ceea9e6bf | ||
| d42c12be9b | |||
|
|
53253eba76 | ||
| a3675ec746 | |||
| 3c8a76a1c1 | |||
| f6d089c194 | |||
| ae9f643fbf | |||
| 5fd10bc647 | |||
| 5d2c9cd639 | |||
| bdd3b70e1f | |||
|
|
a9a3e8b073 | ||
| e400782449 | |||
| 488eef551f | |||
| fae94fc021 | |||
| c2b11e3146 | |||
| 1fbfdf059a | |||
|
|
f8f8525b01 | ||
|
|
1ea6ce082b | ||
|
|
31c8b4d080 | ||
|
|
77d21734c3 | ||
| c4b4430387 | |||
| c864602c1f | |||
|
|
cffdded688 | ||
| b100455e09 | |||
| 6f2b72caa7 | |||
|
|
51a8d1ca22 | ||
| d1be2e20aa | |||
| bf44f314dd | |||
| 5ffac5e077 | |||
| 399144acf5 | |||
| 6548504333 | |||
| 324d5b3b43 | |||
|
|
44befafc6d | ||
|
|
e3bf2c0537 | ||
|
|
2b095bbba3 | ||
| 6b7ab33efe | |||
| 36d4dec5fd | |||
| 0fd121d130 | |||
| 9ae6153d47 | |||
| 3b7ac31615 | |||
| 22bc75d564 | |||
|
|
2bcd2fd96f | ||
| 9ec4db5144 | |||
| 4a4614d8fb | |||
|
|
00b33e3bd9 | ||
|
|
109fa20593 | ||
| 24cdca056f | |||
|
|
03117dc326 | ||
|
|
1fe4dd3dd6 | ||
|
|
5dd2c2cdb5 | ||
|
|
3200622b97 | ||
| 561610a7b9 | |||
| 6569ac0ded | |||
| 2a4b943887 | |||
| 4fb8fa9a73 | |||
| 492ffb2d3f | |||
| 148dfcba22 | |||
| 1292e5498f | |||
| 9756507514 | |||
| ced2d68630 | |||
| d36f7a49f5 | |||
| 1fff67684a | |||
| 8ff22327c9 | |||
| 7efef4a2b6 | |||
| 031daed750 | |||
| 7283544b40 | |||
| 20e2d8b7e9 | |||
| 1515765c3f | |||
| d403c19aab | |||
| 6e4af5610f | |||
| 19210cc3ce | |||
| 71f6c862fa | |||
| 91c62ed7ed | |||
| fec31b2bd1 | |||
| 6355ce2cc5 | |||
| 5ca9e72ab0 | |||
| aeb1ffc669 | |||
| d3a3bf287b | |||
| 38b7e6ed3e | |||
| 518a834066 | |||
| 5c8d604a20 | |||
| b58897dfe5 | |||
| 3c97f31e03 | |||
| a0dd7420de | |||
|
|
a75497103c | ||
| 2c0b3b7ee1 | |||
| a87eb60051 | |||
| 3390971f84 | |||
| e98a06f22f | |||
| 494aae0a06 | |||
| f066978374 | |||
| 8ff67efa8b | |||
| f79fb1747f | |||
| 89ecea4d4a | |||
| 9c78c91ab5 | |||
| 1e40a617f9 | |||
| 77a96bdd83 | |||
| f81937a7dd | |||
| 38b60b8383 | |||
| dcaa4f6cc4 | |||
|
|
e7acaf7821 | ||
| e96522a8f9 | |||
| ea4b7f6b2e | |||
| bda984622d | |||
| 7cac6ba7fc | |||
| ad6f109c95 | |||
| cb0386059a | |||
| 559838d49a |
1
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1 @@
|
||||
custom: https://ln.michaelbumann.com?lightning=lnurlp:ln.michaelbumann.com/lnurlp/github
|
||||
43
.github/workflows/on-push.yml
vendored
Normal file
@@ -0,0 +1,43 @@
|
||||
name: Build on push
|
||||
|
||||
permissions:
|
||||
packages: write
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- master
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build image
|
||||
runs-on: ubuntu-20.04
|
||||
|
||||
steps:
|
||||
- name: Checkout project
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Set env variables
|
||||
run: |
|
||||
echo "BRANCH=$(echo ${GITHUB_REF#refs/heads/} | sed 's/\//-/g')" >> $GITHUB_ENV
|
||||
IMAGE_NAME="${GITHUB_REPOSITORY#*/}"
|
||||
echo "IMAGE_NAME=${IMAGE_NAME//docker-/}" >> $GITHUB_ENV
|
||||
|
||||
- name: Login to Docker Hub
|
||||
run: echo ${{ secrets.GITHUB_TOKEN }} | docker login ghcr.io -u ${{ github.repository_owner }} --password-stdin
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v1
|
||||
id: qemu
|
||||
|
||||
- name: Setup Docker buildx action
|
||||
uses: docker/setup-buildx-action@v1
|
||||
id: buildx
|
||||
|
||||
- name: Run Docker buildx
|
||||
run: |
|
||||
docker buildx build \
|
||||
--platform linux/amd64,linux/arm64 \
|
||||
--tag ghcr.io/${{ github.repository_owner }}/$IMAGE_NAME:$BRANCH \
|
||||
--output "type=registry" ./
|
||||
43
.github/workflows/on-tag.yml
vendored
Normal file
@@ -0,0 +1,43 @@
|
||||
name: Build on tag
|
||||
|
||||
permissions:
|
||||
packages: write
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- v[0-9]+.[0-9]+.[0-9]+
|
||||
- v[0-9]+.[0-9]+.[0-9]+-*
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build image
|
||||
runs-on: ubuntu-20.04
|
||||
|
||||
steps:
|
||||
- name: Checkout project
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Set env variables
|
||||
run: |
|
||||
echo "TAG=${GITHUB_REF/refs\/tags\//}" >> $GITHUB_ENV
|
||||
IMAGE_NAME="${GITHUB_REPOSITORY#*/}"
|
||||
echo "IMAGE_NAME=${IMAGE_NAME//docker-/}" >> $GITHUB_ENV
|
||||
|
||||
- name: Login to Docker Hub
|
||||
run: echo ${{ secrets.GITHUB_TOKEN }} | docker login ghcr.io -u ${{ github.repository_owner }} --password-stdin
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v1
|
||||
id: qemu
|
||||
|
||||
- name: Setup Docker buildx action
|
||||
uses: docker/setup-buildx-action@v1
|
||||
id: buildx
|
||||
|
||||
- name: Run Docker buildx
|
||||
run: |
|
||||
docker buildx build \
|
||||
--platform linux/amd64,linux/arm64 \
|
||||
--tag ghcr.io/${{ github.repository_owner }}/$IMAGE_NAME:$BRANCH \
|
||||
--output "type=registry" ./
|
||||
5
.gitignore
vendored
@@ -1 +1,4 @@
|
||||
vendor
|
||||
vendor
|
||||
config.toml
|
||||
|
||||
data-dir*
|
||||
1
.lndonate
Normal file
@@ -0,0 +1 @@
|
||||
lnurlp://ln.michaelbumann.com/.well-known/lnurlp/bumi
|
||||
18
Dockerfile
Normal file
@@ -0,0 +1,18 @@
|
||||
# Go base image
|
||||
FROM golang:1.17-alpine as builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY . .
|
||||
|
||||
RUN go install
|
||||
|
||||
RUN go build
|
||||
|
||||
FROM alpine:3.14
|
||||
|
||||
COPY --from=builder /app/lnme /lnme
|
||||
|
||||
EXPOSE 1323
|
||||
|
||||
CMD ["/lnme"]
|
||||
21
LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2021 Michael Bumann
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
251
README.md
@@ -1,55 +1,214 @@
|
||||
# LnMe - your friendly ⚡ payment page
|
||||
<p align="center"><br/><br/><img src="https://raw.githubusercontent.com/bumi/lnme/master/icon/lnme-icon-rrect.svg" height="128" width="128" /><br/><br/></p>
|
||||
<h1 align="center">LnMe - your friendly payment page</h1><br/><br/>
|
||||
|
||||
LnMe is a personal Bitcoin Lightning payment website and payment widget.
|
||||
LnMe is a personal Bitcoin Lightning payment page/widget and self-hosted [Lightning Address](https://lightningaddress.com/) server.
|
||||
|
||||

|
||||
|
||||
It is a small service written in Go that connects to a [lnd node](https://github.com/lightningnetwork/lnd/blob/master/docs/INSTALL.md) and exposes a simple HTTP JSON API to create and monitor invoices.
|
||||
It comes with a configurable personal payment website and offers a JavaScript widget to integrate in existing websites.
|
||||
**See it in action: [ln.michaelbumann.com](http://ln.michaelbumann.com/) - my lightning address: bumi@ln.michaelbumann.com**
|
||||
|
||||
If [webln](https://github.com/wbobeirne/webln) is available the widget automatically use webln to request the payment;
|
||||
otherwise an overlay will be shown with the payment request and a QR code.
|
||||
LnMe focusses on simplicity and ease of deployment. It connects to an existing lightning node (currently LND is supported).
|
||||
|
||||
## Motivation
|
||||
LnMe is one [simple executable](https://github.com/bumi/lnme/releases) file that can be deployed anywhere with no dependencies. (on your own node or for example with [one click on Heroku](#heroku))
|
||||
|
||||
I wanted a simple way for people to send Lightning payments using my own lightning node.
|
||||
|
||||
BTCPay Server is too big and hard to run for that and I do not need most of its features.
|
||||
## Features
|
||||
|
||||
- [x] Embeded payment page - customizable (see demo)
|
||||
- [x] [Lightning Address](https://lightningaddress.com/) support
|
||||
- [x] WebLN integration - if [WebLN](https://webln.dev/) is not available a QRcode and the invoice will be shown
|
||||
- [x] [JavaScript widget](#javascript-widget-integration) for existing websites
|
||||
- [x] [Invoice API](https://github.com/bumi/lnme/wiki/API) - simple REST API to create LN invoices from existing JS code
|
||||
- [x] [LNURL-pay](https://github.com/fiatjaf/lnurl-rfc/blob/luds/06.md) support
|
||||
- [x] [LNURL-pay comment](https://github.com/fiatjaf/lnurl-rfc/blob/luds/12.md) support
|
||||
|
||||
## Installation
|
||||
|
||||
LnMe connects to your [LND node](https://github.com/lightningnetwork/lnd/blob/master/docs/INSTALL.md), so a running LND node is required.
|
||||
LnMe can easily run next to LND on the same system.
|
||||
LnMe connects to your [LND node](https://github.com/lightningnetwork/lnd/blob/master/docs/INSTALL.md), so a running LND node is required.
|
||||
LnMe can easily run next to LND on the same system or any other hosting provider.
|
||||
|
||||
There are no other dependencies. Simply download the binary and run it!
|
||||
|
||||
1. Download the latest [release](https://github.com/bumi/lnme/releases)
|
||||
2. Run `lnme` (to run it as systemd service have a look at the [systemd service example config](https://github.com/bumi/lnme/blob/master/examples/lnme.service))
|
||||
2. Run `lnme`
|
||||
3. Done.
|
||||
|
||||
### Build from source
|
||||
|
||||
1. `$ git clone https://github.com/bumi/lnme.git && cd lnme`
|
||||
2. `$ go install`
|
||||
3. `$ go build`
|
||||
4. `$ ./lnme`
|
||||
|
||||
### Running in Docker
|
||||
|
||||
LnMe is now available in docker too.
|
||||
You can start LnMe like this: `docker run -it --rm ghcr.io/bumi/lnme:master`
|
||||
A list of existing tags is available [here](https://github.com/bumi/lnme/pkgs/container/lnme).
|
||||
|
||||
### Configuration
|
||||
|
||||
#### LND configuration
|
||||
|
||||
To connect to the lnd node the cert, macaroon and address of the lnd node has to be configured. LnMe uses the LND defaults.
|
||||
|
||||
* `address`: Host and port of the lnd gRPC service. default: localhost:10009
|
||||
* `cert`: Path to the lnd cert file. default: ~/.lnd/tls.cert
|
||||
* `macaroon`: Path to the macaroon file. default: ~/.lnd/data/chain/bitcoin/mainnet/invoice.macaroon
|
||||
- `lnd-address`: Host and port of the LND gRPC service. default: localhost:10009
|
||||
- `lnd-cert-path`: Path to the LND TLS cert file. default: ~/.lnd/tls.cert
|
||||
- `lnd-macaroon-path`: Path to the LND macaroon file. default: ~/.lnd/data/chain/bitcoin/mainnet/invoice.macaroon (invoice.macaroon is recommended)
|
||||
|
||||
Instead of the path to the macaroon and cert files you can also provide the hex strings:
|
||||
|
||||
- `lnd-cert`: LND TLS cert as HEX string.
|
||||
- `lnd-macaroon`: LND macaroon HEX string. (invoice.macaroon is recommended)
|
||||
|
||||
#### Other configuration
|
||||
|
||||
* `static-path`: Path to a folder that you want to serve with LnMe (e.g. /home/bitcoin/lnme/website). Use this if you want to customize your ⚡website. default: disabled
|
||||
* `disable-website`: Disable the default LnMe website. Disable the website if you only want to embed the LnMe widget on your existing website.
|
||||
* `disable-cors`: Disable CORS headers. (default: false)
|
||||
* `bind`: Host and port to listen on. (default: :1323)
|
||||
* `request-limit`: Limit the allowed requests per second. (default: 5)
|
||||
- `static-path`: Path to a folder that you want to serve with LnMe (e.g. /home/bitcoin/lnme/website). Use this if you want to customize your ⚡website. default: disabled
|
||||
- `lnurlp-comment-allowed`: Allowed length of LNURL-pay comments, maximum around [~2000 characters](https://stackoverflow.com/a/417184). (default: 210)
|
||||
- `disable-website`: Disable the default LnMe website. Disable the website if you only want to embed the LnMe widget on your existing website.
|
||||
- `disable-cors`: Disable CORS headers. (default: false)
|
||||
- `disable-ln-address`: Disable [Lightning Address](https://lightningaddress.com/) handling.
|
||||
- `port`: Port to listen on. (default: 1323)
|
||||
- `listen`: IP and port to listen to. Supersedes `port`. (default: :1323).
|
||||
- `request-limit`: Limit the allowed requests per second. (default: 5)
|
||||
|
||||
Depending on your deployment needs LnMe can be configured using the following options:
|
||||
|
||||
1. Command line flags
|
||||
2. Environment variables
|
||||
3. Config TOML file
|
||||
|
||||
#### Examples:
|
||||
|
||||
##### Command line flags:
|
||||
|
||||
$ lnme --help
|
||||
$ lnme --address=lndhost.com:10009 --bind=localhost:4711
|
||||
$ lnme --lnd-address=lndhost.com:10009 --port=4711
|
||||
$ lnme --disable-website
|
||||
|
||||
##### TOML config file
|
||||
|
||||
See [config.toml.example](./toml.config.example) for an example file.
|
||||
|
||||
$ lnme --config=/path/to/config.toml
|
||||
|
||||
##### Environment variables
|
||||
|
||||
All environment variables must be prefixed by `LNME_` use `_` instead of `-`
|
||||
|
||||
$ LNME_LND_ADDRESS=127.0.0.1:10005 lnme
|
||||
|
||||
### LND Permissions
|
||||
|
||||
LnMe needs the following LND permissions:
|
||||
|
||||
- Read/Write permission for `invoices`
|
||||
- Write permission for `address` (if you want to use the onchain option)
|
||||
|
||||
Use the LND [macaroon bakery](http://macaroon-bakery.freedomnode.com/) to create a new macaroon for LnMe.
|
||||
|
||||
To get the HEX versions of the files use `xxd -plain` e.g. `xxd -plain invoice.macaroon | tr -d '\n'`
|
||||
|
||||
### TOR
|
||||
|
||||
LnMe can connect to your lightning node through [Tor](https://www.torproject.org/). You need to have Tor installed on your system and then simply provide your LND `.onion` address (don't forget to specify the port).
|
||||
|
||||
## Deployment
|
||||
|
||||
It is the easiest to run LnMe on the same node as LND. But you can run it anywhere as long as your LND node is accessible. Simply run the binary and make sure the PORT is accessible.
|
||||
|
||||
If you run LNMe on a different server you will need your LND address, the LND TLS certificate (HEX) and the macaroon (HEX). (see above)
|
||||
|
||||
When getting the HEX of the LND files use `xxd -plain YOUR_FILE.cert | tr -d '\n'`. For example for the TLS certificate, use `xxd -plain tls.cert | tr -d '\n'`.
|
||||
|
||||
The TLS cert is located in the lnd directory:
|
||||
|
||||
- ~/umbrel/lnd/tls.cert on Umbrel
|
||||
- /mnt/hdd/lnd/tls.cert on Raspiblitz
|
||||
- Can also be located in ~/.lnd
|
||||
|
||||
You should find the macaroon files in the LND data dir (e.g. ~.lnd/data/chain/bitcoin/mainnet/) or see "LND Permissions" how to create a new one.
|
||||
|
||||
### Heroku
|
||||
|
||||
One click deployment with Heroku:
|
||||
|
||||
[](https://heroku.com/deploy?template=https://github.com/bumi/lnme)
|
||||
|
||||
Here is a [Video Demo of the Heroku deployment](https://www.youtube.com/watch?v=hSFXhnLp_Rc)
|
||||
|
||||
In order to run Tor on Heroku, the Heroku deployment includes a non-official buildpack: https://github.com/iamashks/heroku-buildpack-tor-proxy
|
||||
This buildpack can be disabled and removed if not needed or desired, through the Settings tab on the Heroku dashboard, or by editing app.json and removing the buildpack.
|
||||
|
||||
Lastly, using the Heroku deployment, you can link the app to your own domain by following the directions here: https://help.heroku.com/MTG1BIA7/how-do-i-connect-a-domain-to-my-heroku-app
|
||||
|
||||
|
||||
### Fly.io
|
||||
|
||||
#### 0. Clone the repo
|
||||
|
||||
$ git clone https://github.com/bumi/lnme.git
|
||||
$ cd lnme
|
||||
|
||||
#### 1. Create a new app
|
||||
|
||||
$ flyctl launch --generate-name // or set a custom app name: flyctl launch --name lnme-test-1
|
||||
|
||||
You will be asked a few things:
|
||||
|
||||
* Copy the configuration to the new app
|
||||
* You do NOT need to create a Postgresql Database
|
||||
* Do NOT deploy it directly, we first need to set some configs
|
||||
|
||||
#### 2. Set the configuration using environment variables:
|
||||
|
||||
The LND config variablse are required. Others are optional:
|
||||
|
||||
$ flyctl secrets set LNME_LND_ADDRESS="xxx.xxx.xxx.xxx:10009" LNME_LND_CERT=xxx LNME_LND_MACAROON=xxx
|
||||
$ flyctl secrets set DISABLE_WEBSITE=1 // etc.
|
||||
|
||||
#### 3. Launch the app:
|
||||
|
||||
$ flyctl deploy
|
||||
|
||||
#### 4.Configure your domain
|
||||
|
||||
To configure a custom domain check the [fly.io guides](https://fly.io/docs/app-guides/custom-domains-with-fly/)
|
||||
|
||||
|
||||
### Custom deployment notes
|
||||
|
||||
To run LnMe as systemd service have a look at the [systemd service example config](https://github.com/bumi/lnme/blob/master/examples/lnme.service)
|
||||
|
||||
I am running LnMe behind a reverse proxy using [caddy](https://caddyserver.com/) which comes with [fully-managed HTTPS](https://caddyserver.com/docs/quick-starts/https) via [letsencrypt](https://letsencrypt.org/).
|
||||
|
||||
Example Caddyfile:
|
||||
|
||||
```
|
||||
lnme.michaelbumann.com {
|
||||
reverse_proxy 127.0.0.1:1323
|
||||
}
|
||||
```
|
||||
|
||||
`$ caddy --config /etc/caddy/Caddyfile`
|
||||
|
||||
## Feature Usage
|
||||
|
||||
### Lightning Address
|
||||
|
||||
The Lightning Address is an Internet Identifier that allows anyone to send you Bitcoin over the Lightning Network.
|
||||
Lightning Address builds on [LNURL-pay](https://github.com/fiatjaf/lnurl-rfc/blob/luds/06.md) LnMe handles the necessary requests for you.
|
||||
|
||||
For more information check out the website: [lightningaddress.com](https://lightningaddress.com/)
|
||||
|
||||
Your Lightning Address: `{anything}@{your domain}`
|
||||
|
||||
### LNURL
|
||||
|
||||
if you got the Lightning Address enabled you also get a LNURL-pay URL:
|
||||
|
||||
https://`{your domain}/lnurlp/{anything}`
|
||||
|
||||
If you need an bech32 encoded version you can use this online tool: [https://lnurl.fiatjaf.com/codec/](https://lnurl.fiatjaf.com/codec/)
|
||||
|
||||
### Customize your ⚡ website
|
||||
|
||||
@@ -61,6 +220,12 @@ Take a look at the [embedded default website](https://github.com/bumi/lnme/blob/
|
||||
2. Create your index.html
|
||||
3. Run lnme: `lnme --static-path=/home/satoshi/my-ln-page
|
||||
|
||||
### Usage with 21 Payment Widgets
|
||||
|
||||
[widgets.twentyuno.net](https://widgets.twentyuno.net/) is a beautiful embeddable payment widget for any existing website.
|
||||
You can use your LnMe instance with the widget by using your [LnMe LNURL](https://github.com/bumi/lnme#lnurl) with the widget.
|
||||
|
||||
Use your bech32 encoded [LNURL](https://github.com/bumi/lnme#lnurl) as `Receiver` in the [widget configuration](https://widgets.twentyuno.net/get-started)
|
||||
|
||||
### JavaScript Widget integration
|
||||
|
||||
@@ -69,7 +234,10 @@ You can integrate the LnMe widget in your existing website.
|
||||
#### 1. Add the LnMe JavaScript files
|
||||
|
||||
```html
|
||||
<script data-lnme-base-url="https://your-lnme-host.com:1323" src="https://your-lnme-host.com/lnme/lnme.js"></script>
|
||||
<script
|
||||
data-lnme-base-url="https://your-lnme-host.com:1323"
|
||||
src="https://your-lnme-host.com/lnme/lnme.js"
|
||||
></script>
|
||||
```
|
||||
|
||||
#### 2. Usage
|
||||
@@ -77,56 +245,65 @@ You can integrate the LnMe widget in your existing website.
|
||||
To request a lightning payment simply call `request()` on a `new LnMe({value: value, memo: memo})`:
|
||||
|
||||
```js
|
||||
var lnme = new LnMe({ value: 1000, memo: 'high5' });
|
||||
var lnme = new LnMe({ value: 1000, memo: "high5" });
|
||||
lnme.request();
|
||||
```
|
||||
|
||||
Use it from a plain HTML link:
|
||||
|
||||
```html
|
||||
<a href="#" onclick="javascript:new LnMe({ value: 1000, memo: 'high5' }).request();return false;">Tip me</a>
|
||||
<a
|
||||
href="#"
|
||||
onclick="javascript:new LnMe({ value: 1000, memo: 'high5' }).request();return false;"
|
||||
>Tip me</a
|
||||
>
|
||||
```
|
||||
|
||||
##### More advanced JS API:
|
||||
|
||||
```js
|
||||
let lnme = new LnMe({ value: 1000, memo: 'high5' });
|
||||
let lnme = new LnMe({ value: 1000, memo: "high5" });
|
||||
|
||||
// get a new invoice and watch for a payment
|
||||
// promise resolves if the invoice is settled
|
||||
lnme.requestPayment().then(invoice => {
|
||||
alert('YAY, thanks!');
|
||||
lnme.requestPayment().then((invoice) => {
|
||||
alert("YAY, thanks!");
|
||||
});
|
||||
|
||||
// create a new invoice
|
||||
lnme.addInvoice().then(invoice => {
|
||||
console.log(invoice.PaymentRequest)
|
||||
lnme.addInvoice().then((invoice) => {
|
||||
console.log(invoice.PaymentRequest);
|
||||
});
|
||||
|
||||
// periodically watch if an invoice is settled
|
||||
lnme.watchPayment().then(invoice => {
|
||||
alert('YAY, thanks!');
|
||||
lnme.watchPayment().then((invoice) => {
|
||||
alert("YAY, thanks!");
|
||||
});
|
||||
|
||||
```
|
||||
|
||||
## Motivation
|
||||
|
||||
I wanted a simple way for people to send Lightning payments using my own lightning node.
|
||||
BTCPay Server is too big and hard to run for that and I do not need most of its features.
|
||||
|
||||
## Development
|
||||
|
||||
Use `go run` to ron the service locally:
|
||||
Use `go run` to run the service locally:
|
||||
|
||||
$ go run lnme.go --address=127.0.0.1:10009 --cert=/home/bitcoin/lightning/tls.cert --macaroon=/home/bitcoin/lightning/invoice.macaroon
|
||||
|
||||
## Build
|
||||
|
||||
LnMe uses [go.rice](https://github.com/GeertJohan/go.rice) to embed assets (HTML, JS, and CSS files). run `rice embed-go`
|
||||
|
||||
$ rice embed-go
|
||||
$ go build
|
||||
|
||||
## Contributing
|
||||
|
||||
Bug reports and pull requests are welcome on GitHub at https://github.com/bumi/lnme
|
||||
|
||||
## Support
|
||||
|
||||
If you like this software and it is valuable for you, you can send sats to ⚡️bumi@getalby.com
|
||||
|
||||
## License
|
||||
|
||||
Available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
|
||||
|
||||
33
app.json
Normal file
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"name": "lnme",
|
||||
"description": "LnMe - your friendly ⚡ payment page",
|
||||
"repository": "https://github.com/bumi/lnme",
|
||||
"logo": "https://bitcoin.org/img/icons/logotop.svg",
|
||||
"keywords": ["payments", "lightning", "bitcoin"],
|
||||
"env": {
|
||||
"LNME_LND_ADDRESS": {
|
||||
"description": "LND Address (e.g. 42.42.21.21:10009)",
|
||||
"value": ""
|
||||
},
|
||||
"LNME_LND_MACAROON": {
|
||||
"description": "LND macaroon as HEX",
|
||||
"value": ""
|
||||
},
|
||||
"LNME_LND_CERT": {
|
||||
"description": "LND TLS cert as HEX",
|
||||
"value": ""
|
||||
},
|
||||
"LNME_TOR_EXE_PATH": {
|
||||
"description": "Path to the tor executable",
|
||||
"value": "/app/tor/bin/tor"
|
||||
}
|
||||
},
|
||||
"buildpacks": [
|
||||
{
|
||||
"url": "https://github.com/heroku/heroku-buildpack-go"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/iamashks/heroku-buildpack-tor-proxy.git"
|
||||
}
|
||||
]
|
||||
}
|
||||
11
config.toml.example
Normal file
@@ -0,0 +1,11 @@
|
||||
disable-website = false
|
||||
disable-cors = false
|
||||
request-limit = 5
|
||||
port = "1323"
|
||||
static-path = "" # Blank means disabled
|
||||
|
||||
lnd-address = "127.0.0.1:10009"
|
||||
lnd-cert = "TLS cert as hex"
|
||||
lnd-cert-path = "Alternative to cert. Path to TLS cert file"
|
||||
lnd-macaroon = "Macaroon as hex"
|
||||
lnd-macaroon-path = "Alternative to macaroon. Path to macaroon file"
|
||||
@@ -1,12 +1,16 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title></title>
|
||||
</head>
|
||||
<body>
|
||||
<script lntip-host="http://localhost:1323" src="http://localhost:1323/static/lntip.js" id="lntip-script"></script>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>LnMe</title>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<a href="#" onclick="javascript:new LnTip({ value: 1000, memo: 'thanks' }).request();return false;">Tip me</a>
|
||||
</body>
|
||||
<script data-lnme-base-url="https://lnme.michaelbumann.com" src="https://lnme.michaelbumann.com/lnme/lnme.js"></script>
|
||||
|
||||
<a href="#" onclick="javascript:new LnMe({ value: 100, memo: 'Thanks!' }).request();return false;">
|
||||
Send me some sats
|
||||
</a>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
||||
20
files/assets/favicon.svg
Normal file
@@ -0,0 +1,20 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="1024px" height="1024px" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<title>lnme-icon-no-padding</title>
|
||||
<defs>
|
||||
<linearGradient x1="50%" y1="0%" x2="50%" y2="100%" id="linearGradient-1">
|
||||
<stop stop-color="#FFA500" offset="0%"></stop>
|
||||
<stop stop-color="#FFE300" offset="100%"></stop>
|
||||
</linearGradient>
|
||||
<linearGradient x1="50%" y1="0%" x2="50%" y2="100%" id="linearGradient-2">
|
||||
<stop stop-color="#FFE300" offset="0%"></stop>
|
||||
<stop stop-color="#FFA500" offset="100%"></stop>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="lnme-icon-no-padding" transform="translate(0.000000, -0.460904)">
|
||||
<polygon id="Path1" fill="url(#linearGradient-1)" points="439.147203 0 192.83828 475.102188 823.013089 475.102188 909.205258 386.884293 491.070577 386.884293 737.132519 0.460904"></polygon>
|
||||
<polygon id="Path2" fill="url(#linearGradient-2)" points="115.14578 615.666174 528.131075 615.666174 327.461698 1024 793.918502 509.464888 174.115801 509.464888"></polygon>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
27
files/assets/icon.svg
Normal file
@@ -0,0 +1,27 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="1024px" height="1024px" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<title>lnme-icon-rect</title>
|
||||
<defs>
|
||||
<linearGradient x1="0%" y1="64.5790537%" x2="100%" y2="64.5790537%" id="linearGradient-1">
|
||||
<stop stop-color="#3D3393" offset="0%"></stop>
|
||||
<stop stop-color="#2B76B9" offset="24.6013508%"></stop>
|
||||
<stop stop-color="#2CACD1" offset="67.981896%"></stop>
|
||||
<stop stop-color="#35EB93" offset="100%"></stop>
|
||||
</linearGradient>
|
||||
<linearGradient x1="50%" y1="0%" x2="50%" y2="100%" id="linearGradient-2">
|
||||
<stop stop-color="#FFA500" offset="0%"></stop>
|
||||
<stop stop-color="#FFE300" offset="100%"></stop>
|
||||
</linearGradient>
|
||||
<linearGradient x1="50%" y1="0%" x2="50%" y2="100%" id="linearGradient-3">
|
||||
<stop stop-color="#FFE300" offset="0%"></stop>
|
||||
<stop stop-color="#FFA500" offset="100%"></stop>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="lnme-icon-rect">
|
||||
<rect id="Background" fill="url(#linearGradient-1)" x="0" y="0" width="1024" height="1024"></rect>
|
||||
<polygon id="Path1" fill="url(#linearGradient-2)" points="456 91.539096 254.655336 479.91029 769.790288 479.91029 840.247879 407.796774 498.444643 407.796774 698.541282 95.539096"></polygon>
|
||||
<polygon id="Path2" fill="url(#linearGradient-3)" points="191.14578 594.813998 528.739662 594.813998 364.70294 928.605547 746.006985 508 239.350688 508"></polygon>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.7 KiB |
BIN
files/assets/lnme-icon-bnw.png
Normal file
|
After Width: | Height: | Size: 397 KiB |
BIN
files/assets/lnme-icon-rect.png
Normal file
|
After Width: | Height: | Size: 760 KiB |
@@ -32,7 +32,7 @@ d=0;d<a;d+=1)for(var g=0;g<a;g+=1){for(var e=0,t=b.a(d,g),p=-1;1>=p;p+=1)if(!(0>
|
||||
typeof e)throw Error("bad rs block @ typeNumber:"+b+"/errorCorrectLevel:"+a);b=e.length/3;a=[];for(var d=0;d<b;d+=1)for(var g=e[3*d],h=e[3*d+1],t=e[3*d+2],p=0;p<g;p+=1){var q=t,f={};f.o=h;f.j=q;a.push(f)}return a}};return e}();return C}());
|
||||
|
||||
// no static class variables because those are not supported on mobile
|
||||
var paymentRequestTemplate = `<div id="lnme-wrapper" class="lnme-wrapper">
|
||||
var LnMePaymentRequestTemplate = `<div id="lnme-wrapper" class="lnme-wrapper">
|
||||
<h1 class="lnme-headline"><span class="lnme-memo"><span></h1>
|
||||
<h2 class="lnme-headline"><span class="lnme-value"></span> Sats</h2>
|
||||
<div class="lnme-qrcode"></div>
|
||||
@@ -43,13 +43,13 @@ var paymentRequestTemplate = `<div id="lnme-wrapper" class="lnme-wrapper">
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
var paymentConfirmationTemplate = `<h1 class="lnme-header lnme-confirmation">Payment sent!</h1>`;
|
||||
var LnMePaymentConfirmationTemplate = `<h1 class="lnme-header lnme-confirmation">Payment sent!</h1>`;
|
||||
|
||||
class LnMe {
|
||||
|
||||
constructor(options) {
|
||||
options = options || {};
|
||||
this.script = document.getElementById('lnme-script');
|
||||
this.script = document.querySelector("[src*='lnme.js']");
|
||||
if (options.baseURL) {
|
||||
this.baseURL = options.baseURL;
|
||||
} else if (this.script && this.script.dataset.lnmeBaseUrl) {
|
||||
@@ -57,12 +57,33 @@ class LnMe {
|
||||
} else {
|
||||
this.baseURL = `${document.location.protocol}//${document.location.host}`;
|
||||
}
|
||||
if (options.watchPaymentUrl) {
|
||||
this.watchPaymentUrl = options.watchPaymentUrl.bind(this);
|
||||
}
|
||||
if (options.createInvoiceUrl) {
|
||||
this.createInvoiceUrl = options.createInvoiceUrl.bind(this);
|
||||
}
|
||||
if (options.newAddressUrl) {
|
||||
this.newAddressUrl = options.newAddressUrl.bind(this);
|
||||
}
|
||||
this.value = parseInt(options.value || 0);
|
||||
this.memo = options.memo || '';
|
||||
this.target = options.target;
|
||||
this.loadStylesheet(); // load it early that styles are ready when the popup is opened
|
||||
}
|
||||
|
||||
watchPaymentUrl() {
|
||||
return `${this.baseURL}/v1/invoice/${this.invoice.payment_hash}`;
|
||||
}
|
||||
|
||||
createInvoiceUrl() {
|
||||
return `${this.baseURL}/v1/invoices`;
|
||||
}
|
||||
|
||||
newAddressUrl() {
|
||||
return `${this.baseURL}/v1/newaddress`;
|
||||
}
|
||||
|
||||
loadStylesheet() {
|
||||
if (document.getElementById('lnme-style')) { return; }
|
||||
// get the CSS file from the same source as the JS widget file
|
||||
@@ -81,7 +102,7 @@ class LnMe {
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
this.paymentWatcher = window.setInterval(() => {
|
||||
this._fetch(`${this.baseURL}/v1/invoice/${this.invoice.payment_hash}`)
|
||||
this._fetch(this.watchPaymentUrl())
|
||||
.then((invoice) => {
|
||||
if (invoice.settled) {
|
||||
this.invoice.settled = true;
|
||||
@@ -127,7 +148,7 @@ class LnMe {
|
||||
this.target.querySelectorAll('.lnme-copy').forEach(element => {
|
||||
element.addEventListener('click', (e) => {
|
||||
navigator.clipboard.writeText(this.invoice.payment_request).then(() => {
|
||||
alert('Copied to clipboad');
|
||||
alert('Copied to clipboard');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -147,7 +168,7 @@ class LnMe {
|
||||
body: JSON.stringify({ memo: this.memo, value: this.value })
|
||||
};
|
||||
return this._fetch(
|
||||
`${this.baseURL}/v1/invoices`,
|
||||
this.createInvoiceUrl(),
|
||||
args
|
||||
).then((invoice) => {
|
||||
this.invoice = invoice;
|
||||
@@ -161,7 +182,7 @@ class LnMe {
|
||||
mode: 'cors',
|
||||
header: { 'Content-Type': 'application/json' }
|
||||
};
|
||||
return this._fetch(`${this.baseURL}/v1/newaddress`, args)
|
||||
return this._fetch(this.newAddressUrl(), args)
|
||||
.then(address => {
|
||||
this.address = address;
|
||||
return address;
|
||||
@@ -210,6 +231,11 @@ class LnMe {
|
||||
shouldSetHash: false
|
||||
});
|
||||
this.target = document.querySelector('.jPopup .content').firstElementChild;
|
||||
|
||||
// When popup is closed, we stop watching the payment.
|
||||
document.querySelector('.jCloseBtn').addEventListener('click', (e) => {
|
||||
this.stopWatchingPayment();
|
||||
});
|
||||
}
|
||||
|
||||
thanks() {
|
||||
|
||||
@@ -2,8 +2,13 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>⚡ Send me some Sats</title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Source+Code+Pro:wght@400;700&display=swap" rel="stylesheet" />
|
||||
|
||||
<meta property="og:site_name" content="">
|
||||
<meta property="og:description" content="">
|
||||
|
||||
<meta property="og:image" content="/lnme/icon.svg">
|
||||
|
||||
<title>Send me some sats</title>
|
||||
<style>
|
||||
html {
|
||||
width: 100%;
|
||||
@@ -12,7 +17,7 @@
|
||||
}
|
||||
body {
|
||||
background-image: linear-gradient(60deg, #3d3393 0%, #2b76b9 37%, #2cacd1 65%, #35eb93 100%);
|
||||
font-family: 'Source Code Pro', monospace;
|
||||
font-family: "Lucida Sans Unicode", "Lucida Grande", sans-serif;
|
||||
}
|
||||
.wrapper {
|
||||
font-weight: 400;
|
||||
@@ -52,7 +57,7 @@
|
||||
}
|
||||
::placeholder {
|
||||
color: #fff;
|
||||
opacity: 0.3;
|
||||
opacity: 0.2;
|
||||
}
|
||||
input[type=number] {
|
||||
-moz-appearance: textfield;
|
||||
@@ -90,6 +95,10 @@
|
||||
width: 100%;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.lnme-qrcode > canvas {
|
||||
border: 5px solid #fff;
|
||||
background: #fff;
|
||||
}
|
||||
.lnme-details {
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
@@ -102,13 +111,14 @@
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
<link rel="icon" type="image/svg+xml" href="/lnme/favicon.svg" />
|
||||
</head>
|
||||
<body>
|
||||
<div class="wrapper">
|
||||
<div class="form" id="form">
|
||||
<p>
|
||||
Send me<br>
|
||||
<input type="number" placeholder="10000" class="amount" id="amount" autocomplete="off" min="100"> Sats
|
||||
<input type="number" placeholder="10000" class="amount" id="amount" autofocus="true" autocomplete="off" min="100"> sats
|
||||
<br>
|
||||
for
|
||||
<br>
|
||||
@@ -143,6 +153,14 @@
|
||||
|
||||
<script src="/lnme/lnme.js" id="lnme-script"></script>
|
||||
<script>
|
||||
var siteName = document.querySelector('head > meta[property="og:site_name"]');
|
||||
if (siteName && !siteName.content) {
|
||||
siteName.content = window.location.host;
|
||||
}
|
||||
var siteDescription = document.querySelector('head > meta[property="og:description"]');
|
||||
if (siteDescription && !siteDescription.content) {
|
||||
siteDescription.content = "sats for " + window.location.host;
|
||||
}
|
||||
document.getElementById("get-new-address").addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
var lnme = new LnMe({});
|
||||
@@ -160,10 +178,14 @@
|
||||
}
|
||||
document.getElementById('send-button').addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
document.getElementById('loader').style.display = 'block';
|
||||
|
||||
var amountElement = document.getElementById('amount');
|
||||
var memoElement = document.getElementById('memo');
|
||||
if (amountElement.value == "") {
|
||||
return;
|
||||
}
|
||||
|
||||
document.getElementById('loader').style.display = 'block';
|
||||
|
||||
|
||||
e.target.setAttribute('disabled', true);
|
||||
amountElement.setAttribute('disabled', true);
|
||||
|
||||
37
fly.toml
Normal file
@@ -0,0 +1,37 @@
|
||||
# fly.toml file generated
|
||||
|
||||
kill_signal = "SIGINT"
|
||||
kill_timeout = 5
|
||||
processes = []
|
||||
|
||||
[env]
|
||||
|
||||
[experimental]
|
||||
allowed_public_ports = []
|
||||
auto_rollback = true
|
||||
|
||||
[[services]]
|
||||
http_checks = []
|
||||
internal_port = 1323
|
||||
processes = ["app"]
|
||||
protocol = "tcp"
|
||||
script_checks = []
|
||||
[services.concurrency]
|
||||
hard_limit = 25
|
||||
soft_limit = 20
|
||||
type = "connections"
|
||||
|
||||
[[services.ports]]
|
||||
force_https = true
|
||||
handlers = ["http"]
|
||||
port = 80
|
||||
|
||||
[[services.ports]]
|
||||
handlers = ["tls", "http"]
|
||||
port = 443
|
||||
|
||||
[[services.tcp_checks]]
|
||||
grace_period = "1s"
|
||||
interval = "15s"
|
||||
restart_limit = 0
|
||||
timeout = "2s"
|
||||
146
go.mod
@@ -1,12 +1,146 @@
|
||||
// +heroku goVersion 1.17
|
||||
module github.com/bumi/lnme
|
||||
|
||||
go 1.15
|
||||
go 1.17
|
||||
|
||||
require (
|
||||
github.com/GeertJohan/go.rice v1.0.0
|
||||
github.com/didip/tollbooth/v6 v6.0.2
|
||||
github.com/labstack/echo/v4 v4.1.17
|
||||
github.com/lightningnetwork/lnd v0.11.1-beta
|
||||
google.golang.org/grpc v1.33.0
|
||||
github.com/cretz/bine v0.2.0
|
||||
github.com/didip/tollbooth/v6 v6.1.2
|
||||
github.com/knadh/koanf v1.4.1
|
||||
github.com/labstack/echo/v4 v4.9.0
|
||||
github.com/lightningnetwork/lnd v0.14.1-beta
|
||||
google.golang.org/grpc v1.45.0
|
||||
gopkg.in/macaroon.v2 v2.1.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da // indirect
|
||||
github.com/aead/siphash v1.0.1 // indirect
|
||||
github.com/andybalholm/brotli v1.0.3 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/btcsuite/btcd v0.22.0-beta.0.20211005184431-e3449998be39 // indirect
|
||||
github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f // indirect
|
||||
github.com/btcsuite/btcutil v1.0.3-0.20210527170813-e2ba6805a890 // indirect
|
||||
github.com/btcsuite/btcutil/psbt v1.0.3-0.20210527170813-e2ba6805a890 // indirect
|
||||
github.com/btcsuite/btcwallet v0.13.0 // indirect
|
||||
github.com/btcsuite/btcwallet/wallet/txauthor v1.1.0 // indirect
|
||||
github.com/btcsuite/btcwallet/wallet/txrules v1.1.0 // indirect
|
||||
github.com/btcsuite/btcwallet/wallet/txsizes v1.1.0 // indirect
|
||||
github.com/btcsuite/btcwallet/walletdb v1.3.6-0.20210803004036-eebed51155ec // indirect
|
||||
github.com/btcsuite/btcwallet/wtxmgr v1.3.1-0.20210822222949-9b5a201c344c // indirect
|
||||
github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd // indirect
|
||||
github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.1.1 // indirect
|
||||
github.com/coreos/go-semver v0.3.0 // indirect
|
||||
github.com/coreos/go-systemd/v22 v22.3.2 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/decred/dcrd/lru v1.0.0 // indirect
|
||||
github.com/dsnet/compress v0.0.1 // indirect
|
||||
github.com/dustin/go-humanize v1.0.0 // indirect
|
||||
github.com/fergusstrange/embedded-postgres v1.10.0 // indirect
|
||||
github.com/form3tech-oss/jwt-go v3.2.3+incompatible // indirect
|
||||
github.com/fsnotify/fsnotify v1.4.9 // indirect
|
||||
github.com/go-errors/errors v1.0.1 // indirect
|
||||
github.com/go-pkgz/expirable-cache v0.0.3 // indirect
|
||||
github.com/gogo/protobuf v1.3.2 // indirect
|
||||
github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
|
||||
github.com/golang/protobuf v1.5.2 // indirect
|
||||
github.com/golang/snappy v0.0.4 // indirect
|
||||
github.com/google/btree v1.0.1 // indirect
|
||||
github.com/gorilla/websocket v1.4.2 // indirect
|
||||
github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 // indirect
|
||||
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 // indirect
|
||||
github.com/grpc-ecosystem/grpc-gateway v1.16.0 // indirect
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.5.0 // indirect
|
||||
github.com/jackc/chunkreader/v2 v2.0.1 // indirect
|
||||
github.com/jackc/pgconn v1.10.0 // indirect
|
||||
github.com/jackc/pgio v1.0.0 // indirect
|
||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||
github.com/jackc/pgproto3/v2 v2.1.1 // indirect
|
||||
github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b // indirect
|
||||
github.com/jackc/pgtype v1.8.1 // indirect
|
||||
github.com/jackc/pgx/v4 v4.13.0 // indirect
|
||||
github.com/jonboulle/clockwork v0.2.2 // indirect
|
||||
github.com/jrick/logrotate v1.0.0 // indirect
|
||||
github.com/json-iterator/go v1.1.11 // indirect
|
||||
github.com/juju/loggo v0.0.0-20190526231331-6e530bcce5d8 // indirect
|
||||
github.com/kkdai/bstream v1.0.0 // indirect
|
||||
github.com/klauspost/compress v1.13.6 // indirect
|
||||
github.com/klauspost/pgzip v1.2.5 // indirect
|
||||
github.com/labstack/gommon v0.3.1 // indirect
|
||||
github.com/lib/pq v1.10.3 // indirect
|
||||
github.com/lightninglabs/gozmq v0.0.0-20191113021534-d20a764486bf // indirect
|
||||
github.com/lightninglabs/neutrino v0.13.0 // indirect
|
||||
github.com/lightningnetwork/lightning-onion v1.0.2-0.20210520211913-522b799e65b1 // indirect
|
||||
github.com/lightningnetwork/lnd/clock v1.1.0 // indirect
|
||||
github.com/lightningnetwork/lnd/healthcheck v1.2.0 // indirect
|
||||
github.com/lightningnetwork/lnd/kvdb v1.2.1 // indirect
|
||||
github.com/lightningnetwork/lnd/queue v1.1.0 // indirect
|
||||
github.com/lightningnetwork/lnd/ticker v1.1.0 // indirect
|
||||
github.com/ltcsuite/ltcd v0.0.0-20190101042124-f37f8bf35796 // indirect
|
||||
github.com/mattn/go-colorable v0.1.11 // indirect
|
||||
github.com/mattn/go-isatty v0.0.14 // indirect
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect
|
||||
github.com/mholt/archiver/v3 v3.5.0 // indirect
|
||||
github.com/miekg/dns v1.1.43 // indirect
|
||||
github.com/mitchellh/copystructure v1.2.0 // indirect
|
||||
github.com/mitchellh/mapstructure v1.4.1 // indirect
|
||||
github.com/mitchellh/reflectwalk v1.0.2 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.1 // indirect
|
||||
github.com/nwaples/rardecode v1.1.2 // indirect
|
||||
github.com/pelletier/go-toml v1.7.0 // indirect
|
||||
github.com/pierrec/lz4/v4 v4.1.8 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/prometheus/client_golang v1.11.0 // indirect
|
||||
github.com/prometheus/client_model v0.2.0 // indirect
|
||||
github.com/prometheus/common v0.26.0 // indirect
|
||||
github.com/prometheus/procfs v0.6.0 // indirect
|
||||
github.com/rogpeppe/fastuuid v1.2.0 // indirect
|
||||
github.com/sirupsen/logrus v1.7.0 // indirect
|
||||
github.com/soheilhy/cmux v0.1.5 // indirect
|
||||
github.com/spf13/pflag v1.0.5 // indirect
|
||||
github.com/stretchr/testify v1.7.0 // indirect
|
||||
github.com/tmc/grpc-websocket-proxy v0.0.0-20201229170055-e5319fda7802 // indirect
|
||||
github.com/ulikunitz/xz v0.5.10 // indirect
|
||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||
github.com/valyala/fasttemplate v1.2.1 // indirect
|
||||
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect
|
||||
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2 // indirect
|
||||
go.etcd.io/bbolt v1.3.6 // indirect
|
||||
go.etcd.io/etcd/api/v3 v3.5.0 // indirect
|
||||
go.etcd.io/etcd/client/pkg/v3 v3.5.0 // indirect
|
||||
go.etcd.io/etcd/client/v2 v2.305.0 // indirect
|
||||
go.etcd.io/etcd/client/v3 v3.5.0 // indirect
|
||||
go.etcd.io/etcd/pkg/v3 v3.5.0 // indirect
|
||||
go.etcd.io/etcd/raft/v3 v3.5.0 // indirect
|
||||
go.etcd.io/etcd/server/v3 v3.5.0 // indirect
|
||||
go.opentelemetry.io/contrib v0.20.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.20.0 // indirect
|
||||
go.opentelemetry.io/otel v0.20.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp v0.20.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v0.20.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk v0.20.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk/export/metric v0.20.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk/metric v0.20.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v0.20.0 // indirect
|
||||
go.opentelemetry.io/proto/otlp v0.7.0 // indirect
|
||||
go.uber.org/atomic v1.7.0 // indirect
|
||||
go.uber.org/multierr v1.6.0 // indirect
|
||||
go.uber.org/zap v1.17.0 // indirect
|
||||
golang.org/x/crypto v0.1.0 // indirect
|
||||
golang.org/x/net v0.1.0 // indirect
|
||||
golang.org/x/sys v0.1.0 // indirect
|
||||
golang.org/x/term v0.1.0 // indirect
|
||||
golang.org/x/text v0.4.0 // indirect
|
||||
golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba // indirect
|
||||
google.golang.org/genproto v0.0.0-20210617175327-b9e0b3197ced // indirect
|
||||
google.golang.org/protobuf v1.26.0 // indirect
|
||||
gopkg.in/errgo.v1 v1.0.1 // indirect
|
||||
gopkg.in/macaroon-bakery.v2 v2.0.1 // indirect
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.0.0 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
|
||||
sigs.k8s.io/yaml v1.2.0 // indirect
|
||||
)
|
||||
|
||||
BIN
icon/lnme-icon-bnw.png
Normal file
|
After Width: | Height: | Size: 397 KiB |
27
icon/lnme-icon-bnw.svg
Normal file
@@ -0,0 +1,27 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="1024px" height="1024px" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<title>lnme-icon-bnw</title>
|
||||
<defs>
|
||||
<linearGradient x1="0%" y1="64.5790537%" x2="100%" y2="64.5790537%" id="linearGradient-1">
|
||||
<stop stop-color="#3A3A3A" offset="0%"></stop>
|
||||
<stop stop-color="#595959" offset="24.6013508%"></stop>
|
||||
<stop stop-color="#7B7B7B" offset="67.981896%"></stop>
|
||||
<stop stop-color="#8B8B8B" offset="100%"></stop>
|
||||
</linearGradient>
|
||||
<linearGradient x1="50%" y1="0%" x2="50%" y2="100%" id="linearGradient-2">
|
||||
<stop stop-color="#B4B4B4" offset="0%"></stop>
|
||||
<stop stop-color="#FFFFFF" offset="100%"></stop>
|
||||
</linearGradient>
|
||||
<linearGradient x1="50%" y1="0%" x2="50%" y2="100%" id="linearGradient-3">
|
||||
<stop stop-color="#FFFFFF" offset="0%"></stop>
|
||||
<stop stop-color="#AEAEAE" offset="100%"></stop>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="lnme-icon-bnw">
|
||||
<rect id="Background" fill="url(#linearGradient-1)" x="0" y="0" width="1024" height="1024" rx="128"></rect>
|
||||
<polygon id="Path1" fill="url(#linearGradient-2)" points="456 91.539096 254.655336 479.91029 769.790288 479.91029 840.247879 407.796774 498.444643 407.796774 698.541282 95.539096"></polygon>
|
||||
<polygon id="Path2" fill="url(#linearGradient-3)" points="191.15 594.813998 528.743882 594.813998 364.70716 928.605547 746.011205 508 239.354908 508"></polygon>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.7 KiB |
BIN
icon/lnme-icon-bnw@2x.png
Normal file
|
After Width: | Height: | Size: 1.3 MiB |
BIN
icon/lnme-icon-no-padding.png
Normal file
|
After Width: | Height: | Size: 115 KiB |
20
icon/lnme-icon-no-padding.svg
Normal file
@@ -0,0 +1,20 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="1024px" height="1024px" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<title>lnme-icon-no-padding</title>
|
||||
<defs>
|
||||
<linearGradient x1="50%" y1="0%" x2="50%" y2="100%" id="linearGradient-1">
|
||||
<stop stop-color="#FFA500" offset="0%"></stop>
|
||||
<stop stop-color="#FFE300" offset="100%"></stop>
|
||||
</linearGradient>
|
||||
<linearGradient x1="50%" y1="0%" x2="50%" y2="100%" id="linearGradient-2">
|
||||
<stop stop-color="#FFE300" offset="0%"></stop>
|
||||
<stop stop-color="#FFA500" offset="100%"></stop>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="lnme-icon-no-padding" transform="translate(0.000000, -0.460904)">
|
||||
<polygon id="Path1" fill="url(#linearGradient-1)" points="439.147203 0 192.83828 475.102188 823.013089 475.102188 909.205258 386.884293 491.070577 386.884293 737.132519 0.460904"></polygon>
|
||||
<polygon id="Path2" fill="url(#linearGradient-2)" points="115.14578 615.666174 528.131075 615.666174 327.461698 1024 793.918502 509.464888 174.115801 509.464888"></polygon>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
BIN
icon/lnme-icon-no-padding@2x.png
Normal file
|
After Width: | Height: | Size: 358 KiB |
BIN
icon/lnme-icon-rect.png
Normal file
|
After Width: | Height: | Size: 760 KiB |
27
icon/lnme-icon-rect.svg
Normal file
@@ -0,0 +1,27 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="1024px" height="1024px" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<title>lnme-icon-rect</title>
|
||||
<defs>
|
||||
<linearGradient x1="0%" y1="64.5790537%" x2="100%" y2="64.5790537%" id="linearGradient-1">
|
||||
<stop stop-color="#3D3393" offset="0%"></stop>
|
||||
<stop stop-color="#2B76B9" offset="24.6013508%"></stop>
|
||||
<stop stop-color="#2CACD1" offset="67.981896%"></stop>
|
||||
<stop stop-color="#35EB93" offset="100%"></stop>
|
||||
</linearGradient>
|
||||
<linearGradient x1="50%" y1="0%" x2="50%" y2="100%" id="linearGradient-2">
|
||||
<stop stop-color="#FFA500" offset="0%"></stop>
|
||||
<stop stop-color="#FFE300" offset="100%"></stop>
|
||||
</linearGradient>
|
||||
<linearGradient x1="50%" y1="0%" x2="50%" y2="100%" id="linearGradient-3">
|
||||
<stop stop-color="#FFE300" offset="0%"></stop>
|
||||
<stop stop-color="#FFA500" offset="100%"></stop>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="lnme-icon-rect">
|
||||
<rect id="Background" fill="url(#linearGradient-1)" x="0" y="0" width="1024" height="1024"></rect>
|
||||
<polygon id="Path1" fill="url(#linearGradient-2)" points="456 91.539096 254.655336 479.91029 769.790288 479.91029 840.247879 407.796774 498.444643 407.796774 698.541282 95.539096"></polygon>
|
||||
<polygon id="Path2" fill="url(#linearGradient-3)" points="191.14578 594.813998 528.739662 594.813998 364.70294 928.605547 746.006985 508 239.350688 508"></polygon>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.7 KiB |
BIN
icon/lnme-icon-rect@2x.png
Normal file
|
After Width: | Height: | Size: 3.0 MiB |
BIN
icon/lnme-icon-rrect.png
Normal file
|
After Width: | Height: | Size: 765 KiB |
27
icon/lnme-icon-rrect.svg
Normal file
@@ -0,0 +1,27 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="1024px" height="1024px" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<title>lnme-icon-rrect</title>
|
||||
<defs>
|
||||
<linearGradient x1="0%" y1="64.5790537%" x2="100%" y2="64.5790537%" id="linearGradient-1">
|
||||
<stop stop-color="#3D3393" offset="0%"></stop>
|
||||
<stop stop-color="#2B76B9" offset="24.6013508%"></stop>
|
||||
<stop stop-color="#2CACD1" offset="67.981896%"></stop>
|
||||
<stop stop-color="#35EB93" offset="100%"></stop>
|
||||
</linearGradient>
|
||||
<linearGradient x1="50%" y1="0%" x2="50%" y2="100%" id="linearGradient-2">
|
||||
<stop stop-color="#FFA500" offset="0%"></stop>
|
||||
<stop stop-color="#FFE300" offset="100%"></stop>
|
||||
</linearGradient>
|
||||
<linearGradient x1="50%" y1="0%" x2="50%" y2="100%" id="linearGradient-3">
|
||||
<stop stop-color="#FFE300" offset="0%"></stop>
|
||||
<stop stop-color="#FFA500" offset="100%"></stop>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="lnme-icon-rrect">
|
||||
<rect id="Background" fill="url(#linearGradient-1)" x="0" y="0" width="1024" height="1024" rx="128"></rect>
|
||||
<polygon id="Path1" fill="url(#linearGradient-2)" points="456 91.539096 254.655336 479.91029 769.790288 479.91029 840.247879 407.796774 498.444643 407.796774 698.541282 95.539096"></polygon>
|
||||
<polygon id="Path2" fill="url(#linearGradient-3)" points="191.14578 594.813998 528.739662 594.813998 364.70294 928.605547 746.006985 508 239.350688 508"></polygon>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.7 KiB |
BIN
icon/lnme-icon-rrect@2x.png
Normal file
|
After Width: | Height: | Size: 2.9 MiB |
BIN
icon/lnme-icon.png
Normal file
|
After Width: | Height: | Size: 92 KiB |
BIN
icon/lnme-icon.sketch
Normal file
20
icon/lnme-icon.svg
Normal file
@@ -0,0 +1,20 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="1024px" height="1024px" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<title>lnme-icon</title>
|
||||
<defs>
|
||||
<linearGradient x1="50%" y1="0%" x2="50%" y2="100%" id="linearGradient-1">
|
||||
<stop stop-color="#FFA500" offset="0%"></stop>
|
||||
<stop stop-color="#FFE300" offset="100%"></stop>
|
||||
</linearGradient>
|
||||
<linearGradient x1="50%" y1="0%" x2="50%" y2="100%" id="linearGradient-2">
|
||||
<stop stop-color="#FFE300" offset="0%"></stop>
|
||||
<stop stop-color="#FFA500" offset="100%"></stop>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="lnme-icon">
|
||||
<polygon id="Path1" fill="url(#linearGradient-1)" points="456 91.539096 254.655336 479.91029 769.790288 479.91029 840.247879 407.796774 498.444643 407.796774 698.541282 95.539096"></polygon>
|
||||
<polygon id="Path2" fill="url(#linearGradient-2)" points="191.14578 594.813998 528.739662 594.813998 364.70294 928.605547 746.006985 508 239.350688 508"></polygon>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
BIN
icon/lnme-icon@2x.png
Normal file
|
After Width: | Height: | Size: 283 KiB |
105
ln/lnd.go
@@ -2,15 +2,20 @@ package ln
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/x509"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/lightningnetwork/lnd/lnrpc"
|
||||
"github.com/lightningnetwork/lnd/macaroons"
|
||||
"gopkg.in/macaroon.v2"
|
||||
|
||||
"github.com/cretz/bine/tor"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/credentials"
|
||||
)
|
||||
@@ -27,9 +32,12 @@ type Invoice struct {
|
||||
|
||||
// LNDoptions are the options for the connection to the lnd node.
|
||||
type LNDoptions struct {
|
||||
Address string
|
||||
CertFile string
|
||||
Address string
|
||||
CertFile string
|
||||
CertHex string
|
||||
MacaroonFile string
|
||||
MacaroonHex string
|
||||
TorExePath string // used when connecting through tor to LND
|
||||
}
|
||||
|
||||
type LNDclient struct {
|
||||
@@ -39,13 +47,15 @@ type LNDclient struct {
|
||||
}
|
||||
|
||||
// AddInvoice generates an invoice with the given price and memo.
|
||||
func (c LNDclient) AddInvoice(value int64, memo string) (Invoice, error) {
|
||||
func (c LNDclient) AddInvoice(value int64, memo string, descriptionHash []byte, private bool) (Invoice, error) {
|
||||
result := Invoice{}
|
||||
|
||||
stdOutLogger.Printf("Adding invoice: memo=%s amount=%v ", memo, value)
|
||||
stdOutLogger.Printf("Adding invoice: memo=%s value=%v", memo, value)
|
||||
invoice := lnrpc.Invoice{
|
||||
Memo: memo,
|
||||
Value: value,
|
||||
Memo: memo,
|
||||
DescriptionHash: descriptionHash,
|
||||
Value: value,
|
||||
Private: private,
|
||||
}
|
||||
res, err := c.lndClient.AddInvoice(c.ctx, &invoice)
|
||||
if err != nil {
|
||||
@@ -60,14 +70,14 @@ func (c LNDclient) AddInvoice(value int64, memo string) (Invoice, error) {
|
||||
// NewAddress gets the next BTC onchain address.
|
||||
func (c LNDclient) NewAddress() (string, error) {
|
||||
stdOutLogger.Printf("Getting a new BTC address")
|
||||
request := lnrpc.NewAddressRequest{
|
||||
Type: lnrpc.AddressType_WITNESS_PUBKEY_HASH,
|
||||
}
|
||||
res, err := c.lndClient.NewAddress(c.ctx, &request)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return res.Address, nil
|
||||
request := lnrpc.NewAddressRequest{
|
||||
Type: lnrpc.AddressType_WITNESS_PUBKEY_HASH,
|
||||
}
|
||||
res, err := c.lndClient.NewAddress(c.ctx, &request)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return res.Address, nil
|
||||
}
|
||||
|
||||
// GetInvoice takes an invoice ID and returns the invoice details including settlement details
|
||||
@@ -103,24 +113,71 @@ func (c LNDclient) GetInvoice(paymentHashStr string) (Invoice, error) {
|
||||
func NewLNDclient(lndOptions LNDoptions) (LNDclient, error) {
|
||||
result := LNDclient{}
|
||||
|
||||
creds, err := credentials.NewClientTLSFromFile(lndOptions.CertFile, "")
|
||||
if err != nil {
|
||||
return result, err
|
||||
// Get credentials either from a hex string or a file
|
||||
var creds credentials.TransportCredentials
|
||||
// if a hex string is provided
|
||||
if lndOptions.CertHex != "" {
|
||||
cp := x509.NewCertPool()
|
||||
cert, err := hex.DecodeString(lndOptions.CertHex)
|
||||
if err != nil {
|
||||
return result, err
|
||||
}
|
||||
cp.AppendCertsFromPEM(cert)
|
||||
creds = credentials.NewClientTLSFromCert(cp, "")
|
||||
// if a path to a cert file is provided
|
||||
} else if lndOptions.CertFile != "" {
|
||||
credsFromFile, err := credentials.NewClientTLSFromFile(lndOptions.CertFile, "")
|
||||
if err != nil {
|
||||
return result, err
|
||||
}
|
||||
creds = credsFromFile // make it available outside of the else if block
|
||||
} else {
|
||||
return result, fmt.Errorf("LND credential is missing")
|
||||
}
|
||||
opts := []grpc.DialOption{
|
||||
grpc.WithTransportCredentials(creds),
|
||||
}
|
||||
|
||||
macaroonData, err := ioutil.ReadFile(lndOptions.MacaroonFile)
|
||||
if strings.Contains(lndOptions.Address, ".onion") {
|
||||
// Start Tor
|
||||
t, err := tor.Start(nil, &tor.StartConf{ExePath: lndOptions.TorExePath})
|
||||
if err != nil {
|
||||
return result, err
|
||||
}
|
||||
torDialer, err := t.Dialer(context.Background(), nil)
|
||||
if err != nil {
|
||||
return result, err
|
||||
}
|
||||
opts = append(opts, grpc.WithContextDialer(func(ctx context.Context, addr string) (net.Conn, error) {
|
||||
return torDialer.DialContext(ctx, "tcp", addr)
|
||||
}))
|
||||
}
|
||||
|
||||
var macaroonData []byte
|
||||
if lndOptions.MacaroonHex != "" {
|
||||
macBytes, err := hex.DecodeString(lndOptions.MacaroonHex)
|
||||
if err != nil {
|
||||
return result, err
|
||||
}
|
||||
macaroonData = macBytes
|
||||
} else if lndOptions.MacaroonFile != "" {
|
||||
macBytes, err := ioutil.ReadFile(lndOptions.MacaroonFile)
|
||||
if err != nil {
|
||||
return result, err
|
||||
}
|
||||
macaroonData = macBytes // make it available outside of the else if block
|
||||
} else {
|
||||
return result, fmt.Errorf("LND macaroon is missing")
|
||||
}
|
||||
|
||||
mac := &macaroon.Macaroon{}
|
||||
if err := mac.UnmarshalBinary(macaroonData); err != nil {
|
||||
return result, err
|
||||
}
|
||||
macCred, err := macaroons.NewMacaroonCredential(mac)
|
||||
if err != nil {
|
||||
return result, err
|
||||
}
|
||||
mac := &macaroon.Macaroon{}
|
||||
if err = mac.UnmarshalBinary(macaroonData); err != nil {
|
||||
return result, err
|
||||
}
|
||||
|
||||
macCred := macaroons.NewMacaroonCredential(mac)
|
||||
opts = append(opts, grpc.WithPerRPCCredentials(macCred))
|
||||
|
||||
conn, err := grpc.Dial(lndOptions.Address, opts...)
|
||||
|
||||
219
lnme.go
@@ -1,18 +1,32 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"embed"
|
||||
"flag"
|
||||
"github.com/GeertJohan/go.rice"
|
||||
"github.com/bumi/lnme/ln"
|
||||
"github.com/didip/tollbooth/v6"
|
||||
"github.com/didip/tollbooth/v6/limiter"
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/labstack/echo/v4/middleware"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/bumi/lnme/ln"
|
||||
"github.com/bumi/lnme/lnurl"
|
||||
"github.com/didip/tollbooth/v6"
|
||||
"github.com/didip/tollbooth/v6/limiter"
|
||||
"github.com/knadh/koanf"
|
||||
"github.com/knadh/koanf/parsers/toml"
|
||||
"github.com/knadh/koanf/providers/basicflag"
|
||||
"github.com/knadh/koanf/providers/env"
|
||||
"github.com/knadh/koanf/providers/file"
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/labstack/echo/v4/middleware"
|
||||
)
|
||||
|
||||
const DEFAULT_LISTEN = ":1323"
|
||||
|
||||
// Middleware for request limited to prevent too many requests
|
||||
// TODO: move to file
|
||||
func LimitMiddleware(lmt *limiter.Limiter) echo.MiddlewareFunc {
|
||||
@@ -34,42 +48,37 @@ type Invoice struct {
|
||||
Memo string `json:"memo"`
|
||||
}
|
||||
|
||||
func main() {
|
||||
address := flag.String("address", "localhost:10009", "The host and port of the lnd gRPC server.")
|
||||
certFile := flag.String("cert", "~/.lnd/tls.cert", "Path to the lnd tls.cert file.")
|
||||
macaroonFile := flag.String("macaroon", "~/.lnd/data/chain/bitcoin/mainnet/invoice.macaroon", "Path to the lnd macaroon file.")
|
||||
bind := flag.String("bind", ":1323", "Host and port to bind on.")
|
||||
staticPath := flag.String("static-path", "", "Path to a static assets directory. Leave blank to not serve any static files.")
|
||||
disableWebsite := flag.Bool("disable-website", false, "Disable default embedded website.")
|
||||
disableCors := flag.Bool("disable-cors", false, "Disable CORS headers.")
|
||||
requestLimit := flag.Float64("request-limit", 5, "Request limit per second.")
|
||||
//go:embed files/assets/*
|
||||
var embeddedAssets embed.FS
|
||||
|
||||
flag.Parse()
|
||||
//go:embed files/root/index.html
|
||||
var indexPage string
|
||||
|
||||
func main() {
|
||||
cfg := LoadConfig()
|
||||
|
||||
e := echo.New()
|
||||
|
||||
// Serve static files if configured
|
||||
if *staticPath != "" {
|
||||
e.Static("/", *staticPath)
|
||||
if cfg.String("static-path") != "" {
|
||||
e.Static("/", cfg.String("static-path"))
|
||||
// Serve default page
|
||||
} else if !*disableWebsite {
|
||||
rootBox := rice.MustFindBox("files/root")
|
||||
indexPage, err := rootBox.String("index.html")
|
||||
if err == nil {
|
||||
stdOutLogger.Print("Running embedded page")
|
||||
e.GET("/", func(c echo.Context) error {
|
||||
return c.HTML(200, indexPage)
|
||||
})
|
||||
} else {
|
||||
stdOutLogger.Printf("Failed to run embedded website: %s", err)
|
||||
}
|
||||
} else if !cfg.Bool("disable-website") {
|
||||
stdOutLogger.Print("Running embedded page")
|
||||
e.GET("/", func(c echo.Context) error {
|
||||
return c.HTML(200, indexPage)
|
||||
})
|
||||
}
|
||||
assetSubdir, err := fs.Sub(embeddedAssets, "files/assets")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
// Embed static files and serve those on /lnme (e.g. /lnme/lnme.js)
|
||||
assetHandler := http.FileServer(rice.MustFindBox("files/assets").HTTPBox())
|
||||
assetHandler := http.FileServer(http.FS(assetSubdir))
|
||||
e.GET("/lnme/*", echo.WrapHandler(http.StripPrefix("/lnme/", assetHandler)))
|
||||
|
||||
// CORS settings
|
||||
if !*disableCors {
|
||||
if !cfg.Bool("disable-cors") {
|
||||
e.Use(middleware.CORS())
|
||||
}
|
||||
|
||||
@@ -77,17 +86,20 @@ func main() {
|
||||
e.Use(middleware.Recover())
|
||||
|
||||
// Request limit per second. DoS protection
|
||||
if *requestLimit > 0 {
|
||||
limiter := tollbooth.NewLimiter(*requestLimit, nil)
|
||||
if cfg.Int("request-limit") > 0 {
|
||||
limiter := tollbooth.NewLimiter(cfg.Float64("request-limit"), nil)
|
||||
e.Use(LimitMiddleware(limiter))
|
||||
}
|
||||
|
||||
// Setup lightning client
|
||||
stdOutLogger.Printf("Connection to %s using macaroon %s and cert %s", *address, *macaroonFile, *certFile)
|
||||
stdOutLogger.Printf("Connecting to %s", cfg.String("lnd-address"))
|
||||
lndOptions := ln.LNDoptions{
|
||||
Address: *address,
|
||||
CertFile: *certFile,
|
||||
MacaroonFile: *macaroonFile,
|
||||
Address: cfg.String("lnd-address"),
|
||||
CertFile: cfg.String("lnd-cert-path"),
|
||||
CertHex: cfg.String("lnd-cert"),
|
||||
MacaroonFile: cfg.String("lnd-macaroon-path"),
|
||||
MacaroonHex: cfg.String("lnd-macaroon"),
|
||||
TorExePath: cfg.String("tor-exe-path"),
|
||||
}
|
||||
lnClient, err := ln.NewLNDclient(lndOptions)
|
||||
if err != nil {
|
||||
@@ -105,7 +117,7 @@ func main() {
|
||||
return c.JSON(http.StatusBadRequest, "Bad request")
|
||||
}
|
||||
|
||||
invoice, err := lnClient.AddInvoice(i.Value, i.Memo)
|
||||
invoice, err := lnClient.AddInvoice(i.Value, i.Memo, nil, cfg.Bool("enable-private-channels"))
|
||||
if err != nil {
|
||||
stdOutLogger.Printf("Error creating invoice: %s", err)
|
||||
return c.JSON(http.StatusInternalServerError, "Error adding invoice")
|
||||
@@ -125,9 +137,9 @@ func main() {
|
||||
})
|
||||
|
||||
// Check invoice status
|
||||
e.GET("/v1/invoice/:invoiceId", func(c echo.Context) error {
|
||||
invoiceId := c.Param("invoiceId")
|
||||
invoice, err := lnClient.GetInvoice(invoiceId)
|
||||
e.GET("/v1/invoice/:paymentHash", func(c echo.Context) error {
|
||||
paymentHash := c.Param("paymentHash")
|
||||
invoice, err := lnClient.GetInvoice(paymentHash)
|
||||
|
||||
if err != nil {
|
||||
stdOutLogger.Printf("Error looking up invoice: %s", err)
|
||||
@@ -137,10 +149,133 @@ func main() {
|
||||
return c.JSON(http.StatusOK, invoice)
|
||||
})
|
||||
|
||||
if !cfg.Bool("disable-ln-address") {
|
||||
lnurlHandler := func(c echo.Context) error {
|
||||
host := c.Request().Host
|
||||
proto := c.Scheme()
|
||||
// TODO: support RFC7239 Forwarded header
|
||||
if c.Request().Header.Get("X-Forwarded-Host") != "" {
|
||||
host = c.Request().Header.Get("X-Forwarded-Host")
|
||||
}
|
||||
if c.Request().Header.Get("X-Forwarded-Proto") != "" {
|
||||
proto = c.Request().Header.Get("X-Forwarded-Proto")
|
||||
}
|
||||
name := c.Param("name")
|
||||
lightningAddress := name + "@" + host
|
||||
lnurlMetadata := "[[\"text/identifier\", \"" + lightningAddress + "\"], [\"text/plain\", \"Sats for " + lightningAddress + "\"]]"
|
||||
lnurlpCommentAllowed := cfg.Int64("lnurlp-comment-allowed")
|
||||
|
||||
if amount := c.QueryParam("amount"); amount == "" {
|
||||
lnurlPayResponse1 := lnurl.LNURLPayResponse1{
|
||||
LNURLResponse: lnurl.LNURLResponse{Status: "OK"},
|
||||
Callback: fmt.Sprintf("%s://%s%s", proto, host, c.Request().URL.Path),
|
||||
MinSendable: 1000,
|
||||
MaxSendable: 100000000,
|
||||
EncodedMetadata: lnurlMetadata,
|
||||
CommentAllowed: lnurlpCommentAllowed,
|
||||
Tag: "payRequest",
|
||||
}
|
||||
return c.JSON(http.StatusOK, lnurlPayResponse1)
|
||||
} else {
|
||||
stdOutLogger.Printf("New LightningAddress request amount: %s", amount)
|
||||
msats, err := strconv.ParseInt(amount, 10, 64)
|
||||
if err != nil || msats < 1000 {
|
||||
stdOutLogger.Printf("Invalid amount: %s", amount)
|
||||
return c.JSON(http.StatusOK, lnurl.LNURLErrorResponse{Status: "ERROR", Reason: "Invalid Amount"})
|
||||
}
|
||||
sats := msats / 1000 // we need sats
|
||||
comment := c.QueryParam("comment")
|
||||
if commentLength := int64(len(comment)); commentLength > lnurlpCommentAllowed {
|
||||
stdOutLogger.Printf("Invalid comment length: %d", commentLength)
|
||||
return c.JSON(http.StatusOK, lnurl.LNURLErrorResponse{Status: "ERROR", Reason: "Invalid comment length"})
|
||||
}
|
||||
metadataHash := sha256.Sum256([]byte(lnurlMetadata))
|
||||
invoice, err := lnClient.AddInvoice(sats, comment, metadataHash[:], cfg.Bool("enable-private-channels"))
|
||||
if err != nil {
|
||||
stdOutLogger.Printf("Error creating invoice: %s", err)
|
||||
return c.JSON(http.StatusOK, lnurl.LNURLErrorResponse{Status: "ERROR", Reason: "Server Error"})
|
||||
}
|
||||
lnurlPayResponse2 := lnurl.LNURLPayResponse2{
|
||||
LNURLResponse: lnurl.LNURLResponse{Status: "OK"},
|
||||
PR: invoice.PaymentRequest,
|
||||
Routes: make([][]lnurl.RouteInfo, 0),
|
||||
Disposable: false,
|
||||
SuccessAction: &lnurl.SuccessAction{Tag: "message", Message: "Thanks, payment received!"},
|
||||
}
|
||||
return c.JSON(http.StatusOK, lnurlPayResponse2)
|
||||
}
|
||||
}
|
||||
e.GET("/.well-known/lnurlp/:name", lnurlHandler)
|
||||
e.GET("/lnurlp/:name", lnurlHandler)
|
||||
}
|
||||
|
||||
// Debug test endpoint
|
||||
e.GET("/ping", func(c echo.Context) error {
|
||||
return c.JSON(http.StatusOK, "pong")
|
||||
})
|
||||
|
||||
e.Logger.Fatal(e.Start(*bind))
|
||||
port := cfg.String("port")
|
||||
// Special case for PORT instead of LNME_PORT due to cloud-providers
|
||||
if os.Getenv("PORT") != "" {
|
||||
port = os.Getenv("PORT")
|
||||
}
|
||||
listen := cfg.String("listen")
|
||||
if listen != "" && port != "" {
|
||||
log.Fatalf("Port and listen options are mutually exclusive, please just use listen.")
|
||||
}
|
||||
if listen == "" {
|
||||
if port != "" {
|
||||
stdOutLogger.Printf("Please use listen instead of deprecated port setting.")
|
||||
listen = fmt.Sprintf(":%s", port)
|
||||
} else {
|
||||
listen = DEFAULT_LISTEN
|
||||
}
|
||||
}
|
||||
|
||||
e.Logger.Fatal(e.Start(listen))
|
||||
}
|
||||
|
||||
func LoadConfig() *koanf.Koanf {
|
||||
k := koanf.New(".")
|
||||
|
||||
f := flag.NewFlagSet("LnMe", flag.ExitOnError)
|
||||
f.String("lnd-address", "localhost:10009", "The host and port of the LND gRPC server.")
|
||||
f.String("lnd-macaroon-path", "~/.lnd/data/chain/bitcoin/mainnet/invoice.macaroon", "Path to the LND macaroon file.")
|
||||
f.String("lnd-macaroon", "", "HEX string of LND macaroon file.")
|
||||
f.String("lnd-cert-path", "~/.lnd/tls.cert", "Path to the LND tls.cert file.")
|
||||
f.String("lnd-cert", "", "HEX string of LND tls cert file.")
|
||||
f.Int64("lnurlp-comment-allowed", 210, "Allowed length of LNURL-pay comments.")
|
||||
f.Bool("disable-website", false, "Disable default embedded website.")
|
||||
f.Bool("disable-ln-address", false, "Disable Lightning Address handling")
|
||||
f.Bool("disable-cors", false, "Disable CORS headers.")
|
||||
f.Bool("enable-private-channels", false, "Adds private routing hints to invoices")
|
||||
f.Float64("request-limit", 5, "Request limit per second.")
|
||||
f.String("static-path", "", "Path to a static assets directory.")
|
||||
f.String("port", "", "Port to bind on (deprecated - use listen).")
|
||||
f.String("listen", "", fmt.Sprintf("Address to bind on. (default \"%s\")", DEFAULT_LISTEN))
|
||||
f.String("tor-exe-path", "tor", "Path to the Tor executable. Used when connecting through Tor. (default: tor)")
|
||||
var configPath string
|
||||
f.StringVar(&configPath, "config", "config.toml", "Path to a .toml config file.")
|
||||
f.Parse(os.Args[1:])
|
||||
|
||||
// Load config from flags, including defaults
|
||||
if err := k.Load(basicflag.Provider(f, "."), nil); err != nil {
|
||||
log.Fatalf("Error loading config: %v", err)
|
||||
}
|
||||
|
||||
// Load config from environment variables
|
||||
k.Load(env.Provider("LNME_", ".", func(s string) string {
|
||||
return strings.Replace(strings.ToLower(strings.TrimPrefix(s, "LNME_")), "_", "-", -1)
|
||||
}), nil)
|
||||
|
||||
// Load config from file if available
|
||||
if configPath != "" {
|
||||
if _, err := os.Stat(configPath); err == nil {
|
||||
if err := k.Load(file.Provider(configPath), toml.Parser()); err != nil {
|
||||
log.Fatalf("Error loading config file: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return k
|
||||
}
|
||||
|
||||
52
lnurl/types.go
Normal file
@@ -0,0 +1,52 @@
|
||||
// THANKS: https://github.com/fiatjaf/go-lnurl/blob/d50a8e916232580895822178fe36e0f5cf400554/base.go
|
||||
// only using the LNURL types here
|
||||
package lnurl
|
||||
|
||||
import "net/url"
|
||||
|
||||
type LNURLResponse struct {
|
||||
Status string `json:"status,omitempty"`
|
||||
Reason string `json:"reason,omitempty"`
|
||||
}
|
||||
|
||||
type LNURLPayResponse1 struct {
|
||||
LNURLResponse
|
||||
Callback string `json:"callback"`
|
||||
CallbackURL *url.URL `json:"-"`
|
||||
Tag string `json:"tag"`
|
||||
MaxSendable int64 `json:"maxSendable"`
|
||||
MinSendable int64 `json:"minSendable"`
|
||||
EncodedMetadata string `json:"metadata"`
|
||||
Metadata Metadata `json:"-"`
|
||||
CommentAllowed int64 `json:"commentAllowed"`
|
||||
}
|
||||
|
||||
type LNURLPayResponse2 struct {
|
||||
LNURLResponse
|
||||
SuccessAction *SuccessAction `json:"successAction"`
|
||||
Routes [][]RouteInfo `json:"routes"`
|
||||
PR string `json:"pr"`
|
||||
Disposable bool `json:"disposable,omitempty"`
|
||||
}
|
||||
|
||||
type RouteInfo struct {
|
||||
NodeId string `json:"nodeId"`
|
||||
ChannelUpdate string `json:"channelUpdate"`
|
||||
}
|
||||
|
||||
type SuccessAction struct {
|
||||
Tag string `json:"tag"`
|
||||
Description string `json:"description,omitempty"`
|
||||
URL string `json:"url,omitempty"`
|
||||
Message string `json:"message,omitempty"`
|
||||
Ciphertext string `json:"ciphertext,omitempty"`
|
||||
IV string `json:"iv,omitempty"`
|
||||
}
|
||||
|
||||
type LNURLErrorResponse struct {
|
||||
Status string `json:"status,omitempty"`
|
||||
Reason string `json:"reason,omitempty"`
|
||||
URL *url.URL `json:"-"`
|
||||
}
|
||||
|
||||
type Metadata [][]string
|
||||