initial commit

This commit is contained in:
Peter Rounce 2022-08-01 10:36:32 +00:00
commit 037788d6de
56 changed files with 1756 additions and 0 deletions

23
.gitignore vendored Normal file
View file

@ -0,0 +1,23 @@
# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib
lnurlw
# Test binary, built with `go test -c`
*.test
# Output of the go coverage tool, specifically when used with LiteIDE
*.out
# Dependency directories (remove the comment below to include it)
# vendor/
# secrets
tls.cert
*.macaroon*
# test data
# add_test_data.sql

2
Caddyfile Normal file
View file

@ -0,0 +1,2 @@
https://card.yourdomain.com
reverse_proxy 127.0.0.1:9000

21
LICENSE Normal file
View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2022 Peter Rounce
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.

25
README.md Normal file
View file

@ -0,0 +1,25 @@
# Bolt Card
## Overview
The bolt card enables a customer to make payment at a merchant point of sale over the bitcoin lightning network.
Each bolt card makes use of a service to receive the request from the merchant system, apply payment rules and make payment.
The 'bolt card service' software provided here can be used to host bolt cards for yourself and others.
The 'bolt card creation' instructions describe how to set up bolt cards for use with your bolt card service.
## Documents
| Document | Description |
| --- | --- |
| [Specification](docs/SPEC.md) | Bolt card specifications |
| [System](docs/SYSTEM.md) | Bolt card system overview |
| [Install](docs/INSTALL.md) | Bolt card service installation |
| [Card](docs/CARD.md) | Bolt card creation |
| [FAQ](docs/FAQ.md) | Frequently asked questions |
## Telegram group
Discussion and support is available at https://t.me/bolt_card .

22
add_card_data.sql Normal file
View file

@ -0,0 +1,22 @@
\c card_db
INSERT INTO cards (
aes_cmac,
uid,
last_counter_value,
lnurlw_request_timeout_sec,
enable_flag,
tx_limit_sats,
day_limit_sats,
card_description
)
VALUES (
'00000000000000000000000000000000',
'00000000000000',
0,
60,
'Y',
1000,
10000,
'bolt card'
);

50
boltcard.service Normal file
View file

@ -0,0 +1,50 @@
[Unit]
Description=bolt card service
After=network.target network-online.target
Requires=network-online.target
StartLimitIntervalSec=0
[Service]
Type=simple
Restart=always
RestartSec=10
User=ubuntu
# boltcard service settings
# LOG_LEVEL is DEBUG or PRODUCTION
Environment="LOG_LEVEL=DEBUG"
# AES_DECRYPT_KEY is the hex value of the server decrypt key for hosted bolt cards
Environment="AES_DECRYPT_KEY=00000000000000000000000000000000"
# DB_ values are for the postgres database connection
Environment="DB_HOST=localhost"
Environment="DB_PORT=5432"
Environment="DB_USER=cardapp"
Environment="DB_PASSWORD=database_password"
Environment="DB_NAME=card_db"
# LNURLW_CB_URL is the URL prefix for the lnurlw callback
Environment="LNURLW_CB_URL=https://card.yourdomain.com/cb"
# MIN_WITHDRAW_SATS & MAX_WITHDRAW_SATS set the values for the lnurlw response
Environment="MIN_WITHDRAW_SATS=1"
Environment="MAX_WITHDRAW_SATS=1000000"
# LN_ values are for the lightning server used for making payments
Environment="LN_HOST=ln.host.io"
Environment="LN_PORT=10009"
Environment="LN_TLS_FILE=/home/ubuntu/boltcard/tls.cert"
Environment="LN_MACAROON_FILE=/home/ubuntu/boltcard/SendPaymentV2.macaroon"
# FEE_LIMIT_SAT is the maximum lightning network fee to be paid
Environment="FEE_LIMIT_SAT=10"
# LN_TESTNODE may be used in testing and will then only pay to the defined test node pubkey
#Environment="LN_TESTNODE=000000000000000000000000000000000000000000000000000000000000000000"
ExecStart=/bin/bash /home/ubuntu/boltcard/s_launch
[Install]
WantedBy=multi-user.target

38
create_db.sql Normal file
View file

@ -0,0 +1,38 @@
DROP DATABASE IF EXISTS card_db;
CREATE DATABASE card_db;
--CREATE USER cardapp WITH PASSWORD '***';
\c card_db;
CREATE TABLE cards (
card_id INT GENERATED ALWAYS AS IDENTITY,
aes_cmac CHAR(32) NOT NULL,
uid CHAR(14) NOT NULL,
last_counter_value INTEGER NOT NULL,
lnurlw_request_timeout_sec INT NOT NULL,
enable_flag CHAR(1) NOT NULL DEFAULT 'N',
tx_limit_sats INT NOT NULL,
day_limit_sats INT NOT NULL,
card_description VARCHAR(100) NOT NULL DEFAULT '',
PRIMARY KEY(card_id)
);
CREATE TABLE card_payments (
card_payment_id INT GENERATED ALWAYS AS IDENTITY,
card_id INT NOT NULL,
k1 CHAR(32) UNIQUE NOT NULL,
lnurlw_request_time TIMESTAMPTZ NOT NULL,
ln_invoice VARCHAR(1024) NOT NULL DEFAULT '',
amount_msats BIGINT CHECK (amount_msats > 0),
paid_flag CHAR(1) NOT NULL,
payment_time TIMESTAMPTZ,
payment_status VARCHAR(100) NOT NULL DEFAULT '',
failure_reason VARCHAR(100) NOT NULL DEFAULT '',
payment_status_time TIMESTAMPTZ,
PRIMARY KEY(card_payment_id),
CONSTRAINT fk_card FOREIGN KEY(card_id) REFERENCES cards(card_id)
);
GRANT ALL PRIVILEGES ON TABLE cards TO cardapp;
GRANT ALL PRIVILEGES ON TABLE card_payments TO cardapp;

75
crypto.go Normal file
View file

@ -0,0 +1,75 @@
package main
import (
"bytes"
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"encoding/hex"
"github.com/aead/cmac"
)
func create_k1() (string, error) {
// 16 bytes = 128 bits
b := make([]byte, 16)
_, err := rand.Read(b)
if err != nil {
return "", err
}
str := hex.EncodeToString(b)
return str, nil
}
// decrypt p with aes_dec
func crypto_aes_decrypt(key_sdm_file_read []byte, ba_p []byte) ([]byte, error) {
dec_p := make([]byte, 16)
iv := make([]byte, 16)
c1, err := aes.NewCipher(key_sdm_file_read)
if err != nil {
return dec_p, err
}
mode := cipher.NewCBCDecrypter(c1, iv)
mode.CryptBlocks(dec_p, ba_p)
return dec_p, nil
}
func crypto_aes_cmac(key_sdm_file_read_mac []byte, sv2 []byte, ba_c []byte) (bool, error) {
c2, err := aes.NewCipher(key_sdm_file_read_mac)
if err != nil {
return false, err
}
ks, err := cmac.Sum(sv2, c2, 16)
if err != nil {
return false, err
}
c3, err := aes.NewCipher(ks)
if err != nil {
return false, err
}
cm, err := cmac.Sum([]byte{}, c3, 16)
if err != nil {
return false, err
}
ct := make([]byte, 8)
ct[0] = cm[1]
ct[1] = cm[3]
ct[2] = cm[5]
ct[3] = cm[7]
ct[4] = cm[9]
ct[5] = cm[11]
ct[6] = cm[13]
ct[7] = cm[15]
res_cmac := bytes.Compare(ct, ba_c)
if res_cmac != 0 {
return false, nil
}
return true, nil
}

312
database.go Normal file
View file

@ -0,0 +1,312 @@
package main
import (
"database/sql"
"errors"
"fmt"
_ "github.com/lib/pq"
"os"
)
type card struct {
card_id int
card_guid string
aes_dec string
aes_cmac string
db_uid string
last_counter_value uint32
lnurlw_request_timeout_sec int
enable_flag string
tx_limit_sats int
day_limit_sats int
}
type payment struct {
card_payment_id int
card_id int
k1 string
paid_flag string
}
func db_open() (*sql.DB, error) {
// get connection string from environment variables
conn := fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=%s sslmode=disable",
os.Getenv("DB_HOST"),
os.Getenv("DB_PORT"),
os.Getenv("DB_USER"),
os.Getenv("DB_PASSWORD"),
os.Getenv("DB_NAME"))
db, err := sql.Open("postgres", conn)
if err != nil {
return db, err
}
return db, nil
}
func db_get_card_from_uid(card_uid string) (*card, error) {
c := card{}
db, err := db_open()
if err != nil {
return &c, err
}
defer db.Close()
sqlStatement := `SELECT card_id, aes_cmac, uid,` +
` last_counter_value, lnurlw_request_timeout_sec,` +
` enable_flag, tx_limit_sats, day_limit_sats` +
` FROM cards WHERE uid=$1;`
row := db.QueryRow(sqlStatement, card_uid)
err = row.Scan(
&c.card_id,
&c.aes_cmac,
&c.db_uid,
&c.last_counter_value,
&c.lnurlw_request_timeout_sec,
&c.enable_flag,
&c.tx_limit_sats,
&c.day_limit_sats)
if err != nil {
return &c, err
}
return &c, nil
}
func db_get_card_from_card_id(card_id int) (*card, error) {
c := card{}
db, err := db_open()
if err != nil {
return &c, err
}
defer db.Close()
sqlStatement := `SELECT card_id, aes_cmac, uid,` +
` last_counter_value, lnurlw_request_timeout_sec,` +
` enable_flag, tx_limit_sats, day_limit_sats` +
` FROM cards WHERE card_id=$1;`
row := db.QueryRow(sqlStatement, card_id)
err = row.Scan(
&c.card_id,
&c.aes_cmac,
&c.db_uid,
&c.last_counter_value,
&c.lnurlw_request_timeout_sec,
&c.enable_flag,
&c.tx_limit_sats,
&c.day_limit_sats)
if err != nil {
return &c, err
}
return &c, nil
}
func db_check_lnurlw_timeout(card_payment_id int) (bool, error) {
db, err := db_open()
if err != nil {
return true, err
}
defer db.Close()
lnurlw_timeout := true
sqlStatement := `SELECT NOW() > cp.lnurlw_request_time + c.lnurlw_request_timeout_sec * INTERVAL '1 SECOND'` +
` FROM card_payments AS cp INNER JOIN cards AS c ON c.card_id = cp.card_id` +
` WHERE cp.card_payment_id=$1;`
row := db.QueryRow(sqlStatement, card_payment_id)
err = row.Scan(&lnurlw_timeout)
if err != nil {
return true, err
}
return lnurlw_timeout, nil
}
func db_check_and_update_counter(card_id int, new_counter_value uint32) (bool, error) {
db, err := db_open()
if err != nil {
return false, err
}
defer db.Close()
sqlStatement := `UPDATE cards SET last_counter_value = $2 WHERE card_id = $1` +
` AND last_counter_value < $2;`
res, err := db.Exec(sqlStatement, card_id, new_counter_value)
if err != nil {
return false, err
}
count, err := res.RowsAffected()
if err != nil {
return false, err
}
if count != 1 {
return false, nil
}
return true, nil
}
func db_insert_payment(card_id int, k1 string) error {
db, err := db_open()
if err != nil {
return err
}
defer db.Close()
// insert a new record into card_payments with card_id & k1 set
sqlStatement := `INSERT INTO card_payments` +
` (card_id, k1, paid_flag, lnurlw_request_time)` +
` VALUES ($1, $2, 'N', NOW());`
res, err := db.Exec(sqlStatement, card_id, k1)
if err != nil {
return err
}
count, err := res.RowsAffected()
if err != nil {
return err
}
if count != 1 {
return errors.New("not one card_payments record inserted")
}
return nil
}
func db_get_payment_k1(k1 string) (*payment, error) {
p := payment{}
db, err := db_open()
if err != nil {
return &p, err
}
defer db.Close()
sqlStatement := `SELECT card_payment_id, card_id, paid_flag` +
` FROM card_payments WHERE k1=$1;`
row := db.QueryRow(sqlStatement, k1)
err = row.Scan(
&p.card_payment_id,
&p.card_id,
&p.paid_flag)
if err != nil {
return &p, err
}
return &p, nil
}
func db_update_payment_invoice(card_payment_id int, ln_invoice string, amount_msats int64) error {
db, err := db_open()
if err != nil {
return err
}
defer db.Close()
sqlStatement := `UPDATE card_payments SET ln_invoice = $2, amount_msats = $3 WHERE card_payment_id = $1;`
res, err := db.Exec(sqlStatement, card_payment_id, ln_invoice, amount_msats)
if err != nil {
return err
}
count, err := res.RowsAffected()
if err != nil {
return err
}
if count != 1 {
return errors.New("not one card_payment record updated")
}
return nil
}
func db_update_payment_paid(card_payment_id int) error {
db, err := db_open()
if err != nil {
return err
}
defer db.Close()
sqlStatement := `UPDATE card_payments SET paid_flag = 'Y', payment_time = NOW() WHERE card_payment_id = $1;`
res, err := db.Exec(sqlStatement, card_payment_id)
if err != nil {
return err
}
count, err := res.RowsAffected()
if err != nil {
return err
}
if count != 1 {
return errors.New("not one card_payment record updated")
}
return nil
}
func db_update_payment_status(card_payment_id int, payment_status string, failure_reason string) error {
db, err := db_open()
if err != nil {
return err
}
defer db.Close()
sqlStatement := `UPDATE card_payments SET payment_status = $2, failure_reason = $3, ` +
`payment_status_time = NOW() WHERE card_payment_id = $1;`
res, err := db.Exec(sqlStatement, card_payment_id, payment_status, failure_reason)
if err != nil {
return err
}
count, err := res.RowsAffected()
if err != nil {
return err
}
if count != 1 {
return errors.New("not one card_payment record updated")
}
return nil
}
func db_get_card_totals(card_id int) (int, error) {
db, err := db_open()
if err != nil {
return 0, err
}
defer db.Close()
day_total_msats := 0
sqlStatement := `SELECT COALESCE(SUM(amount_msats),0) FROM card_payments ` +
`WHERE card_id=$1 AND paid_flag='Y' ` +
`AND payment_time > NOW() - INTERVAL '1 DAY';`
row := db.QueryRow(sqlStatement, card_id)
err = row.Scan(&day_total_msats)
if err != nil {
return 0, err
}
day_total_sats := day_total_msats / 1000
return day_total_sats, nil
}

151
docs/CARD.md Normal file
View file

@ -0,0 +1,151 @@
# Steps for making a bolt card
## Introduction
Here we describe how to create your own bolt card.
## Resources
- some cards
- NXP DNA 424 NTAG cards
- a good NFC reader/writer
- Identiv uTrust 3700 F
- software
- [NXP TagXplorer software](https://www.nxp.com/products/rfid-nfc/mifare-hf/mifare-desfire/tagxplorer-pc-based-nfc-tag-reader-writer-tool:TAGXPLORER)
- [Java Run Time environment](https://java.com/de/download/)
## Steps
### Connect to the card
- start the NFC TagXplorer software
- `Connect` to the NFC card reader
- place a card on the reader and click `Connect Tag`
- verify the card description
![tag connected](images/con.webp)
### Read the card
- select `NDEF Operations` and `Read NDEF`
- if you get this error, click `Format NDEF` and try again
![bytes to read should be greater than zero](images/btr.webp)
- verify that the read completes without erroring
### Start to set up the URI template
- select `NTAG Operations`, `Mirroring Features` and `NTAG 424 DNA`
- set `Protocol` to `https://`
- set `URI Data` to
```
lnurlw://card.yourdomain.com
```
- select `Add PICCDATA` and `Enable SUN Message`
- adjust the `URI Data` to
```
lnurlw://card.yourdomain.com/ln?p=00000000000000000000000000000000&c=0000000000000000
```
- click after `p=` and note the p_position (38 in this case)
- click after `c=` and note the c_position (73 in this case)
- select `Write To Tag`
![NDEF message written successfully](images/nfwc.webp)
- now go back to `NDEF Operations` and `Read NDEF`
- verify that the `NDEF Payload Info` is as expected
![read payload as text](images/rd-txt.webp)
### Finish setting up the URI template
- notice that the URI shows as `https://lnurlw://card ...` but we want `lnurlw://card ...`
- go to `NTAG Operations` and `NTAG 424 DNA`
- select `Read/Write data`
- select `File No` as `NDEF File - 02`
- click `Read`
![read NDEF data](images/rdh.webp)
- the NDEF file is `0057D1015355046C6E75726C ...`.
- look for the bytes `5504` (6 bytes from the start)
- `04` is the code for `https://` URI prepending
- change the `04` to `00` to indicate no prepending for the URI
![write NDEF data](images/wrh.webp)
- click `Write`
![written successfully](images/ws.webp)
- now go back to `NDEF Operations` and `Read NDEF`
- verify that the `NDEF Payload(HEX) Info` is similar to that shown
![read payload as hex](images/nrd.webp)
- copy the hex data and convert to text, without the `0x00` prefix
- verify you have your expected `URI data` value
[Online hex to text tool](http://www.unit-conversion.info/texttools/hexadecimal/)
![hex to text online tool](images/hex.webp)
### Set up the SUN authentication message
- go to `NTAG Operations` and `NTAG 424 DNA`
- select `Security Management` and click `Authentiate First`
![success dialog](images/avs.webp)
- select `Get/Change File Settings`
![success dialog](images/gfs.webp)
- set up the values in the order shown
![file and SDM options with field entry order](images/fs-add.webp)
- select `Change File Settings`
![success message](images/cfs.webp)
- now go back to `NDEF Operations` and `Read NDEF`
- convert the hex data to text again
- verify that the `p` and `c` values are non zero
- select `Read NDEF` again
- convert the hex data to text again
- verify that the `p` and `c` values are in the right place
- verify that the `p` and `c` values change on each read
### Change the application keys
- go to `NTAG Operations` and `NTAG 424 DNA`
- select `Security Management`
- select `Authenticate`
- leave the `Card Key No` set to `00`
- leave the `Key` value set to `00000000000000000000000000000000` if not changed yet
- click `Authenticate First`
![success message](images/avs.webp)
- select `Change Key`
- select the `Card Key No` to change the key value for `00` to `04`
- leave the `Old Key` value set to `00000000000000000000000000000000` if not changed yet
- enter a `New Key` value as required
- enter a `New Key Version` value of `00` or as required to keep track of your keys
- click `Change Key`
![success message](images/ccs.webp)
- repeat this to change all 5 application keys to your own values
### Lock the card
- go to `NTAG Operations` and `NTAG 424 DNA`
- select `Security Management` and click `Authentiate First`
- select `Get/Change File Settings`
- adjust the `Access Rights` settings as shown
![success message](images/lock.webp)
## Testing
- set up a [bolt card service](INSTALL.md)
- add a record in the database for the new card
- use a merchant point of sale to scan your bolt card, e.g. [Breez wallet](https://breez.technology/)
- watch the bolt card service logs and verify that the requests are received and processed

16
docs/FAQ.md Normal file
View file

@ -0,0 +1,16 @@
# FAQ
> Why do I get a payment failure with NO_ROUTE ?
This is due to your payment lightning node not finding a route to the merchant lightning node.
It may help to open well funded channels to other well connected nodes.
It may also help to increase your maximum network fee in your service variables, **FEE_LIMIT_SAT** .
It can be useful to test paying invoices directly from your lightning node.
> Why do my payments take so long ?
This is due to the time taken for your payment lightning node to find a route.
It can be improved by opening channels using clearnet rather than on the tor network.
It may also help to improve your lightning node hardware or software setup.
It can be useful to test paying invoices directly from your lightning node.

82
docs/INSTALL.md Normal file
View file

@ -0,0 +1,82 @@
# Bolt card service installation
## hardware & o/s
1 GHz processor, 2 GB RAM, 10GB storage minimum
Ubuntu 20.04 LTS server
### install Go
[Go download & install](https://go.dev/doc/install)
`$ go version` >= 1.18.3
### install Postgres
[Postgres download & install](https://www.postgresql.org/download/linux/ubuntu/)
`$ psql --version` >= 12.11
### install Caddy
[Caddy download & install](https://caddyserver.com/docs/install)
`$ caddy version` >= 2.5.2
### download the boltcard repository
`$ git clone https://github.com/boltcard/boltcard`
### get a macaroon and tls.cert from the lightning node
create a macaroon with limited permissions to the lightning node
```
$ lncli \
--rpcserver=lightning-node.io:10009 \
--macaroonpath=admin.macaroon \
--tlscertpath="tls.cert" \
bakemacaroon uri:/routerrpc.Router/SendPaymentV2 > SendPaymentV2.macaroon.hex
$ xxd -r -p SendPaymentV2.macaroon.hex SendPaymentV2.macaroon
```
### setup the boltcard server
edit `boltcard.service` in the section named `boltcard service settings`
edit `Caddyfile` to set the boltcard domain name
edit `add_card_data.sql` to set up the individual bolt card records
### database creation
`$ ./s_create_db`
### boltcard service install
`$ sudo cp boltcard.service /etc/systemd/system/boltcard.service`
`$ ./s_build`
`$ systemctl status boltcard`
### https setup
set up the domain A record to point to the server
set up the server hosting firewall to allow open access to https (port 443) only
### caddy setup for https
`$ sudo cp Caddyfile /etc/caddy`
`$ sudo systemctl stop caddy`
`$ sudo systemctl start caddy`
`$ sudo systemctl status caddy`
you should see 'certificate obtained successfully' in the service log
### service bring-up and testing
#### service log
the service log should be monitored on a separate console while tests are run
`$ journalctl -u boltcard.service -f`
#### local http
`$ curl http://127.0.0.1:9000/ln?1`
this should respond with 'bad request' and show up in the service log
#### remote https
navigate to the service URL from a browser, for example `https://card.yourdomain.com/ln?2`
this should respond with 'bad request' and show up in the service log
#### bolt card
[create a bolt card](docs/CARD.md) with the URI pointing to this server
use a PoS setup to read the bolt card, e.g. [Breez wallet](https://breez.technology/)
monitor the service log to ensure decryption, authentication, payment rules and lightning payment work as expected
# Further information and support
[bolt card FAQ](docs/FAQ.md)
[bolt card telegram group](https://t.me/bolt_card)

11
docs/SECURITY.md Normal file
View file

@ -0,0 +1,11 @@
# Security
## secrets
- card AES decrypt key to the environment variable `AES_DECRYPT_KEY`
- card AES cmac keys into the database table `cards`
- `tls.cert` and `SendPaymentV2.macaroon` for the lightning node
- password for the application database user `cardapp`
- database script in `create_db.sql`
- application environment variable in `lnurlw.service`

34
docs/SPEC.md Normal file
View file

@ -0,0 +1,34 @@
# Bolt card specification
The bolt card system is built on the open standards listed below.
- [LUD-03: withdrawRequest base spec.](https://github.com/fiatjaf/lnurl-rfc/blob/luds/03.md)
- [LUD-17: Protocol schemes and raw (non bech32-encoded) URLs.](https://github.com/fiatjaf/lnurl-rfc/blob/luds/17.md)
## Bolt card interaction
- the point-of-sale (POS) will read an NDEF message from the card, for example
```
lnurlw://card.yourdomain.com?p=A2EF40F6D46F1BB36E6EBF0114D4A464&c=F509EEA788E37E32
```
- the POS will call your server here
```
https://card.yourdomain.com?p=A2EF40F6D46F1BB36E6EBF0114D4A464&c=F509EEA788E37E32
```
- your server should verify the payment request and issue an LNURLw response
### Server side verification
- for the `p` value and the `SDM Meta Read Access Key` value, decrypt the UID and counter
- for the `c` value and the `SDM File Read Access Key` value, check with AES-CMAC
![decrypt and cmac steps](images/ac.webp)
- the authenticated UID and counter values can be used on your server to verify the request
- your server should only accept an increasing counter value
- additional validation rules can be added at your server, for example
- an enable flag
- payment limits
- a list of allowed merchants
- a verification of your location from your phone
- your server can then make payment from your lightning node

41
docs/SYSTEM.md Normal file
View file

@ -0,0 +1,41 @@
## System
The customer and the merchant must have a supporting infrastructure to make and accept payments using a bolt card.
### Interaction
```mermaid
flowchart TB
BoltCard(bolt card)-. NFC .-PointOfSale(point of sale)
MerchantServer(merchant server)-. LNURLw .-BoltCardServer(bolt card server)
LightningNodeA(lightning node)-. lightning network .-LightningNodeB(lightning node)
subgraph merchant
PointOfSale<-->MerchantServer
LightningNodeB-->MerchantServer
end
subgraph customer
BoltCardServer-->LightningNodeA
end
```
### Sequencing
```mermaid
sequenceDiagram
participant p1 as customer bolt card
participant p2 as merchant point of sale
participant p3 as merchant server
participant p4 as customer bolt card server
participant p5 as customer lightining node
participant p6 as merchant lightning node
p1->>p2: NFC read
p2->>p3: API call
p3->>p4: LNURLw request
p4->>p3: LNURLw response
p3->>p4: LNURLw callback
p4->>p3: LNURLw response
p4->>p5: API call
p5-->>p6: lightning network payment
p6->>p3: payment notification
p3->>p2: user notification
```

BIN
docs/images/ac.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

BIN
docs/images/aes-dec.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

BIN
docs/images/aesd.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

BIN
docs/images/avs.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5 KiB

BIN
docs/images/breez.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

BIN
docs/images/btr.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

BIN
docs/images/card.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

BIN
docs/images/ccs.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

BIN
docs/images/cfs.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

BIN
docs/images/cmac-aes.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

BIN
docs/images/cmac.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

BIN
docs/images/con.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

BIN
docs/images/fs-add.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

BIN
docs/images/fs.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

BIN
docs/images/gfs.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

BIN
docs/images/hex.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

BIN
docs/images/lock.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

BIN
docs/images/nfc.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

BIN
docs/images/nfwc.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

BIN
docs/images/nrd.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

BIN
docs/images/picc.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

BIN
docs/images/rd-txt.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

BIN
docs/images/rd.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

BIN
docs/images/rdh.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

BIN
docs/images/ref.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

BIN
docs/images/su.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

BIN
docs/images/sun.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

BIN
docs/images/wr.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

BIN
docs/images/wrh.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

BIN
docs/images/ws.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3 KiB

45
go.mod Normal file
View file

@ -0,0 +1,45 @@
module github.com/boltcard/boltcard
go 1.18
require (
github.com/aead/cmac v0.0.0-20160719120800-7af84192f0b1 // indirect
github.com/aead/siphash v1.0.1 // indirect
github.com/btcsuite/btcd v0.20.1-beta.0.20200515232429-9f0179fd2c46 // indirect
github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f // indirect
github.com/btcsuite/btcutil v1.0.2 // indirect
github.com/btcsuite/btcwallet v0.11.1-0.20200515224913-e0e62245ecbe // indirect
github.com/btcsuite/btcwallet/wallet/txauthor v1.0.0 // indirect
github.com/btcsuite/btcwallet/wallet/txrules v1.0.0 // indirect
github.com/btcsuite/btcwallet/wallet/txsizes v1.0.0 // indirect
github.com/btcsuite/btcwallet/walletdb v1.3.1 // indirect
github.com/btcsuite/btcwallet/wtxmgr v1.1.1-0.20200515224913-e0e62245ecbe // indirect
github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd // indirect
github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/fiatjaf/ln-decodepay v1.4.0 // indirect
github.com/go-errors/errors v1.0.1 // indirect
github.com/golang/protobuf v1.3.3 // indirect
github.com/grpc-ecosystem/grpc-gateway v1.8.6 // indirect
github.com/juju/loggo v0.0.0-20190526231331-6e530bcce5d8 // indirect
github.com/kkdai/bstream v0.0.0-20181106074824-b3251f7901ec // indirect
github.com/lib/pq v1.10.6 // indirect
github.com/lightninglabs/gozmq v0.0.0-20191113021534-d20a764486bf // indirect
github.com/lightninglabs/neutrino v0.11.1-0.20200316235139-bffc52e8f200 // indirect
github.com/lightningnetwork/lnd v0.10.1-beta // indirect
github.com/lightningnetwork/lnd/queue v1.0.3 // indirect
github.com/lightningnetwork/lnd/ticker v1.0.0 // indirect
github.com/lncm/lnd-rpc v1.0.2 // indirect
github.com/miekg/dns v0.0.0-20171125082028-79bfde677fa8 // indirect
github.com/rogpeppe/fastuuid v1.2.0 // indirect
github.com/sirupsen/logrus v1.8.1 // indirect
golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37 // indirect
golang.org/x/net v0.0.0-20200202094626-16171245cfb2 // indirect
golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4 // indirect
golang.org/x/text v0.3.2 // indirect
google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce // indirect
google.golang.org/grpc v1.27.1 // indirect
gopkg.in/errgo.v1 v1.0.1 // indirect
gopkg.in/macaroon-bakery.v2 v2.0.1 // indirect
gopkg.in/macaroon.v2 v2.1.0 // indirect
)

275
go.sum Normal file
View file

@ -0,0 +1,275 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
git.schwanenlied.me/yawning/bsaes.git v0.0.0-20180720073208-c0276d75487e/go.mod h1:BWqTsj8PgcPriQJGl7el20J/7TuT1d/hSyFDXMEpoEo=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/NebulousLabs/fastrand v0.0.0-20181203155948-6fb6489aac4e/go.mod h1:Bdzq+51GR4/0DIhaICZEOm+OHvXGwwB2trKZ8B4Y6eQ=
github.com/NebulousLabs/go-upnp v0.0.0-20180202185039-29b680b06c82/go.mod h1:GbuBk21JqF+driLX3XtJYNZjGa45YDoa9IqCTzNSfEc=
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
github.com/Yawning/aez v0.0.0-20180114000226-4dad034d9db2/go.mod h1:9pIqrY6SXNL8vjRQE5Hd/OL5GyK/9MrGUWs87z/eFfk=
github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da/go.mod h1:eHEWzANqSiWQsof+nXEI9bUVUyV6F53Fp89EuCh2EAA=
github.com/aead/cmac v0.0.0-20160719120800-7af84192f0b1 h1:+JkXLHME8vLJafGhOH4aoV2Iu8bR55nU6iKMVfYVLjY=
github.com/aead/cmac v0.0.0-20160719120800-7af84192f0b1/go.mod h1:nuudZmJhzWtx2212z+pkuy7B6nkBqa+xwNXZHL1j8cg=
github.com/aead/siphash v1.0.1 h1:FwHfE/T45KPKYuuSAKyyvE+oPWcaQ+CUmFW0bPlM+kg=
github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
github.com/btcsuite/btcd v0.0.0-20190629003639-c26ffa870fd8/go.mod h1:3J08xEfcugPacsc34/LKRU2yO7YmuT8yt28J8k2+rrI=
github.com/btcsuite/btcd v0.0.0-20190824003749-130ea5bddde3/go.mod h1:3J08xEfcugPacsc34/LKRU2yO7YmuT8yt28J8k2+rrI=
github.com/btcsuite/btcd v0.20.1-beta/go.mod h1:wVuoA8VJLEcwgqHBwHmzLRazpKxTv13Px/pDuV7OomQ=
github.com/btcsuite/btcd v0.20.1-beta.0.20200513120220-b470eee47728/go.mod h1:wVuoA8VJLEcwgqHBwHmzLRazpKxTv13Px/pDuV7OomQ=
github.com/btcsuite/btcd v0.20.1-beta.0.20200515232429-9f0179fd2c46 h1:QyTpiR5nQe94vza2qkvf7Ns8XX2Rjh/vdIhO3RzGj4o=
github.com/btcsuite/btcd v0.20.1-beta.0.20200515232429-9f0179fd2c46/go.mod h1:Yktc19YNjh/Iz2//CX0vfRTS4IJKM/RKO5YZ9Fn+Pgo=
github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f h1:bAs4lUbRJpnnkd9VhRV3jjAVU7DJVjMaK+IsvSeZvFo=
github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f/go.mod h1:TdznJufoqS23FtqVCzL0ZqgP5MqXbb4fg/WgDys70nA=
github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d/go.mod h1:+5NJ2+qvTyV9exUAL/rxXi3DcLg2Ts+ymUAY5y4NvMg=
github.com/btcsuite/btcutil v1.0.2 h1:9iZ1Terx9fMIOtq1VrwdqfsATL9MC2l8ZrUY6YZ2uts=
github.com/btcsuite/btcutil v1.0.2/go.mod h1:j9HUFwoQRsZL3V4n+qG+CUnEGHOarIxfC3Le2Yhbcts=
github.com/btcsuite/btcutil/psbt v1.0.2/go.mod h1:LVveMu4VaNSkIRTZu2+ut0HDBRuYjqGocxDMNS1KuGQ=
github.com/btcsuite/btcwallet v0.11.1-0.20200515224913-e0e62245ecbe h1:0m9uXDcnUc3Fv72635O/MfLbhbW+0hfSVgRiWezpkHU=
github.com/btcsuite/btcwallet v0.11.1-0.20200515224913-e0e62245ecbe/go.mod h1:9+AH3V5mcTtNXTKe+fe63fDLKGOwQbZqmvOVUef+JFE=
github.com/btcsuite/btcwallet/wallet/txauthor v1.0.0 h1:KGHMW5sd7yDdDMkCZ/JpP0KltolFsQcB973brBnfj4c=
github.com/btcsuite/btcwallet/wallet/txauthor v1.0.0/go.mod h1:VufDts7bd/zs3GV13f/lXc/0lXrPnvxD/NvmpG/FEKU=
github.com/btcsuite/btcwallet/wallet/txrules v1.0.0 h1:2VsfS0sBedcM5KmDzRMT3+b6xobqWveZGvjb+jFez5w=
github.com/btcsuite/btcwallet/wallet/txrules v1.0.0/go.mod h1:UwQE78yCerZ313EXZwEiu3jNAtfXj2n2+c8RWiE/WNA=
github.com/btcsuite/btcwallet/wallet/txsizes v1.0.0 h1:6DxkcoMnCPY4E9cUDPB5tbuuf40SmmMkSQkoE8vCT+s=
github.com/btcsuite/btcwallet/wallet/txsizes v1.0.0/go.mod h1:pauEU8UuMFiThe5PB3EO+gO5kx87Me5NvdQDsTuq6cs=
github.com/btcsuite/btcwallet/walletdb v1.0.0/go.mod h1:bZTy9RyYZh9fLnSua+/CD48TJtYJSHjjYcSaszuxCCk=
github.com/btcsuite/btcwallet/walletdb v1.2.0/go.mod h1:9cwc1Yyg4uvd4ZdfdoMnALji+V9gfWSMfxEdLdR5Vwc=
github.com/btcsuite/btcwallet/walletdb v1.3.1 h1:lW1Ac3F1jJY4K11P+YQtRNcP5jFk27ASfrV7C6mvRU0=
github.com/btcsuite/btcwallet/walletdb v1.3.1/go.mod h1:9cwc1Yyg4uvd4ZdfdoMnALji+V9gfWSMfxEdLdR5Vwc=
github.com/btcsuite/btcwallet/wtxmgr v1.0.0/go.mod h1:vc4gBprll6BP0UJ+AIGDaySoc7MdAmZf8kelfNb8CFY=
github.com/btcsuite/btcwallet/wtxmgr v1.1.1-0.20200515224913-e0e62245ecbe h1:yQbJVYfsKbdqDQNLxd4hhiLSiMkIygefW5mSHMsdKpc=
github.com/btcsuite/btcwallet/wtxmgr v1.1.1-0.20200515224913-e0e62245ecbe/go.mod h1:OwC0W0HhUszbWdvJvH6xvgabKSJ0lXl11YbmmqF9YXQ=
github.com/btcsuite/fastsha256 v0.0.0-20160815193821-637e65642941/go.mod h1:QcFA8DZHtuIAdYKCq/BzELOaznRsCvwf4zTPmaYwaig=
github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd h1:R/opQEbFEy9JGkIguV40SvRY1uliPX8ifOvi6ICsFCw=
github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd/go.mod h1:HHNXQzUsZCxOoE+CPiyCTO6x34Zs86zZUiwtpXoGdtg=
github.com/btcsuite/golangcrypto v0.0.0-20150304025918-53f62d9b43e8/go.mod h1:tYvUd8KLhm/oXvUeSEs2VlLghFjQt9+ZaF9ghH0JNjc=
github.com/btcsuite/goleveldb v0.0.0-20160330041536-7834afc9e8cd/go.mod h1:F+uVaaLLH7j4eDXPRvw78tMflu7Ie2bzYOH4Y8rRKBY=
github.com/btcsuite/goleveldb v1.0.0/go.mod h1:QiK9vBlgftBg6rWQIj6wFzbPfRjiykIEhBH4obrXJ/I=
github.com/btcsuite/snappy-go v0.0.0-20151229074030-0bdef8d06723/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc=
github.com/btcsuite/snappy-go v1.0.0/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc=
github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792 h1:R8vQdOQdZ9Y3SkEwmHoWBmX1DNXhXZqlTpq6s4tyJGc=
github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtEyQwv5/p4Mg4C0fgbePVuGr935/5ddU9Z3TmDRY=
github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46fmI40EZs=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/coreos/bbolt v1.3.3/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
github.com/davecgh/go-spew v0.0.0-20171005155431-ecdeabc65495/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/fiatjaf/ln-decodepay v1.4.0 h1:WDedFrzitQPQHfo7pktYXoZ1Rmy28RJnA/4w3cpzt40=
github.com/fiatjaf/ln-decodepay v1.4.0/go.mod h1:zLf4G9EsRhryXtFO63072BZ0JQLWbr7uRm3wZFJafdo=
github.com/frankban/quicktest v1.0.0/go.mod h1:R98jIehRai+d1/3Hv2//jOVCTJhW1VBavT6B6CuGq2k=
github.com/frankban/quicktest v1.2.2/go.mod h1:Qh/WofXFeiAFII1aEBu529AtJo6Zg2VHscnEsbBnJ20=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/go-errors/errors v1.0.1 h1:LUHzmkK3GUKUrL/1gfBUxAHzcev3apQlezX/+O7ma6w=
github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
github.com/go-openapi/errors v0.19.2/go.mod h1:qX0BLWsyaKfvhluLejVpVNwNRdXZhEbTA4kxxpKBC94=
github.com/go-openapi/strfmt v0.19.5/go.mod h1:eftuHTlB/dI8Uq8JJOyRlieZf+WkkxUuk0dgdHXr2Qk=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.3 h1:gyjaxf+svBWX08ZjK86iN9geUJF0H6gp2IRKX6Nf6/I=
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.2.1-0.20190312032427-6f77996f0c42/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
github.com/grpc-ecosystem/grpc-gateway v1.8.6 h1:XvND7+MPP7Jp+JpqSZ7naSl5nVZf6k0LbL1V3EKh0zc=
github.com/grpc-ecosystem/grpc-gateway v1.8.6/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/jackpal/gateway v1.0.5/go.mod h1:lTpwd4ACLXmpyiCTRtfiNyVnUmqT9RivzCDQetPfnjA=
github.com/jackpal/go-nat-pmp v0.0.0-20170405195558-28a68d0c24ad/go.mod h1:QPH045xvCAeXUZOxsnwmrtiCoxIr9eob+4orBN1SBKc=
github.com/jedib0t/go-pretty v4.3.0+incompatible/go.mod h1:XemHduiw8R651AF9Pt4FwCTKeG3oo7hrHJAoznj9nag=
github.com/jessevdk/go-flags v0.0.0-20141203071132-1679536dcc89/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlTRt3OuAQ=
github.com/juju/clock v0.0.0-20190205081909-9c5c9712527c/go.mod h1:nD0vlnrUjcjJhqN5WuCWZyzfd5AHZAC9/ajvbSx69xA=
github.com/juju/errors v0.0.0-20190806202954-0232dcc7464d/go.mod h1:W54LbzXuIE0boCoNJfwqpmkKJ1O4TCTZMetAt6jGk7Q=
github.com/juju/loggo v0.0.0-20190526231331-6e530bcce5d8 h1:UUHMLvzt/31azWTN/ifGWef4WUqvXk0iRqdhdy/2uzI=
github.com/juju/loggo v0.0.0-20190526231331-6e530bcce5d8/go.mod h1:vgyd7OREkbtVEN/8IXZe5Ooef3LQePvuBm9UWj6ZL8U=
github.com/juju/retry v0.0.0-20180821225755-9058e192b216/go.mod h1:OohPQGsr4pnxwD5YljhQ+TZnuVRYpa5irjugL1Yuif4=
github.com/juju/testing v0.0.0-20190723135506-ce30eb24acd2/go.mod h1:63prj8cnj0tU0S9OHjGJn+b1h0ZghCndfnbQolrYTwA=
github.com/juju/utils v0.0.0-20180820210520-bf9cc5bdd62d/go.mod h1:6/KLg8Wz/y2KVGWEpkK9vMNGkOnu4k/cqs8Z1fKjTOk=
github.com/juju/version v0.0.0-20180108022336-b64dbd566305/go.mod h1:kE8gK5X0CImdr7qpSKl3xB2PmpySSmfj7zVbkZFs81U=
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4=
github.com/kkdai/bstream v0.0.0-20181106074824-b3251f7901ec h1:n1NeQ3SgUHyISrjFFoO5dR748Is8dBL9qpaTNfphQrs=
github.com/kkdai/bstream v0.0.0-20181106074824-b3251f7901ec/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/lib/pq v1.10.6 h1:jbk+ZieJ0D7EVGJYpL9QTz7/YW6UHbmdnZWYyK5cdBs=
github.com/lib/pq v1.10.6/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/lightninglabs/gozmq v0.0.0-20191113021534-d20a764486bf h1:HZKvJUHlcXI/f/O0Avg7t8sqkPo78HFzjmeYFl6DPnc=
github.com/lightninglabs/gozmq v0.0.0-20191113021534-d20a764486bf/go.mod h1:vxmQPeIQxPf6Jf9rM8R+B4rKBqLA2AjttNxkFBL2Plk=
github.com/lightninglabs/neutrino v0.11.0/go.mod h1:CuhF0iuzg9Sp2HO6ZgXgayviFTn1QHdSTJlMncK80wg=
github.com/lightninglabs/neutrino v0.11.1-0.20200316235139-bffc52e8f200 h1:j4iZ1XlUAPQmW6oSzMcJGILYsRHNs+4O3Gk+2Ms5Dww=
github.com/lightninglabs/neutrino v0.11.1-0.20200316235139-bffc52e8f200/go.mod h1:MlZmoKa7CJP3eR1s5yB7Rm5aSyadpKkxqAwLQmog7N0=
github.com/lightninglabs/protobuf-hex-display v1.3.3-0.20191212020323-b444784ce75d/go.mod h1:KDb67YMzoh4eudnzClmvs2FbiLG9vxISmLApUkCa4uI=
github.com/lightningnetwork/lightning-onion v1.0.1/go.mod h1:rigfi6Af/KqsF7Za0hOgcyq2PNH4AN70AaMRxcJkff4=
github.com/lightningnetwork/lnd v0.10.1-beta h1:zA/rQoxC5FNHtayVuA2wRtSOEDnJbuzAzHKAf2PWj1Q=
github.com/lightningnetwork/lnd v0.10.1-beta/go.mod h1:F9er1DrpOHdQVQBqYqyBqIFyl6q16xgBM8yTioHj2Cg=
github.com/lightningnetwork/lnd/cert v1.0.2/go.mod h1:fmtemlSMf5t4hsQmcprSoOykypAPp+9c+0d0iqTScMo=
github.com/lightningnetwork/lnd/queue v1.0.1/go.mod h1:vaQwexir73flPW43Mrm7JOgJHmcEFBWWSl9HlyASoms=
github.com/lightningnetwork/lnd/queue v1.0.3 h1:5ufYVE7lh9GJnL1wOoeO3bZ3aAHWNnkNFHP7W1+NiJ8=
github.com/lightningnetwork/lnd/queue v1.0.3/go.mod h1:YTkTVZCxz8tAYreH27EO3s8572ODumWrNdYW2E/YKxg=
github.com/lightningnetwork/lnd/ticker v1.0.0 h1:S1b60TEGoTtCe2A0yeB+ecoj/kkS4qpwh6l+AkQEZwU=
github.com/lightningnetwork/lnd/ticker v1.0.0/go.mod h1:iaLXJiVgI1sPANIF2qYYUJXjoksPNvGNYowB8aRbpX0=
github.com/lncm/lnd-rpc v1.0.2 h1:34F1+aNT5bbBrfA8aWg3m40yaEByJjQwCqVAyaV2Fog=
github.com/lncm/lnd-rpc v1.0.2/go.mod h1:zw6oVgCpAYE+KYDfCa52xZag4m0+fOcuXxj6D+4NDtY=
github.com/ltcsuite/ltcd v0.0.0-20190101042124-f37f8bf35796/go.mod h1:3p7ZTf9V1sNPI5H8P3NkTFF4LuwMdPl2DodF60qAKqY=
github.com/ltcsuite/ltcutil v0.0.0-20181217130922-17f3b04680b6/go.mod h1:8Vg/LTOO0KYa/vlHWJ6XZAevPQThGH5sufO0Hrou/lA=
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/miekg/dns v0.0.0-20171125082028-79bfde677fa8 h1:PRMAcldsl4mXKJeRNB/KVNz6TlbS6hk2Rs42PqgU3Ws=
github.com/miekg/dns v0.0.0-20171125082028-79bfde677fa8/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/gomega v1.4.1/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA=
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
github.com/rogpeppe/fastuuid v1.2.0 h1:Ppwyp6VYCF1nvBTXL3trRso7mXMlRrw9ooo375wvi2s=
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE=
github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
github.com/tv42/zbase32 v0.0.0-20160707012821-501572607d02/go.mod h1:tHlrkM198S068ZqfrO6S8HsoJq2bF3ETfTL+kt4tInY=
github.com/urfave/cli v1.18.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
go.mongodb.org/mongo-driver v1.0.3/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qLUO4lqsUM=
golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20180723164146-c126467f60eb/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190211182817-74369b46fc67/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20200115085410-6d4e4cb37c7d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37 h1:cg5LA/zNPRzIXIWSCxQW10Rvpy94aQh3LT/ShoCpkHw=
golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/net v0.0.0-20180719180050-a680a1efc54d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181106065722-10aee1819953/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190206173232-65e2d4e15006/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3 h1:0GoQqolDA55aaLxZyTzK/Y2ePZzZTUrRacwib7cNsYQ=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20200202094626-16171245cfb2 h1:CCH4IOTTfewWjGOlSp+zGcjutRKlBEZQ6wTn8ozI/nI=
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190209173611-3b5209105503/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42 h1:vEOn+mP2zCOVzKckCZy6YsCtDblrpj/w7B9nxGNELpg=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4 h1:sfkvUWPNGwSV+8/fNqctR5lS2AqCSqYwXdrjCxp/dXo=
golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190201180003-4b09977fb922/go.mod h1:L3J43x8/uS+qIUoksaLKe6OS3nUKxOKuIFz1sl2/jx4=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce h1:1mbrb1tUU+Zmt5C94IGKADBTJZjZXAd+BubWi7r9EiI=
google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio=
google.golang.org/grpc v1.18.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.27.1 h1:zvIju4sqAGvwKspUQOhwnpcqSbzi7/H6QomNNjTL4sk=
google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/errgo.v1 v1.0.1 h1:oQFRXzZ7CkBGdm1XZm/EbQYaYNNEElNBOd09M6cqNso=
gopkg.in/errgo.v1 v1.0.1/go.mod h1:3NjfXwocQRYAPTq4/fzX+CwUhPRcR/azYRhj8G+LqMo=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/macaroon-bakery.v2 v2.0.1 h1:0N1TlEdfLP4HXNCg7MQUMp5XwvOoxk+oe9Owr2cpvsc=
gopkg.in/macaroon-bakery.v2 v2.0.1/go.mod h1:B4/T17l+ZWGwxFSZQmlBwp25x+og7OkhETfr3S9MbIA=
gopkg.in/macaroon.v2 v2.0.0/go.mod h1:+I6LnTMkm/uV5ew/0nsulNjL16SK4+C8yDmRUzHR17I=
gopkg.in/macaroon.v2 v2.1.0 h1:HZcsjBCzq9t0eBPMKqTN/uSN6JOm78ZJ2INbqcBQOUI=
gopkg.in/macaroon.v2 v2.1.0/go.mod h1:OUb+TQP/OP0WOerC2Jp/3CwhIKyIa9kQjuc7H24e6/o=
gopkg.in/mgo.v2 v2.0.0-20190816093944-a6b53ec6cb22/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA=
gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=

131
lightning.go Normal file
View file

@ -0,0 +1,131 @@
package main
import (
"context"
"encoding/hex"
"fmt"
"io"
"io/ioutil"
"log"
"os"
"strconv"
"time"
lnrpc "github.com/lncm/lnd-rpc/v0.10.0/lnrpc"
routerrpc "github.com/lncm/lnd-rpc/v0.10.0/routerrpc"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
"gopkg.in/macaroon.v2"
)
type rpcCreds map[string]string
func (m rpcCreds) RequireTransportSecurity() bool { return true }
func (m rpcCreds) GetRequestMetadata(_ context.Context, _ ...string) (map[string]string, error) {
return m, nil
}
func newCreds(bytes []byte) rpcCreds {
creds := make(map[string]string)
creds["macaroon"] = hex.EncodeToString(bytes)
return creds
}
func getRouterClient(hostname string, port int, tlsFile, macaroonFile string) routerrpc.RouterClient {
macaroonBytes, err := ioutil.ReadFile(macaroonFile)
if err != nil {
log.Println("Cannot read macaroon file .. ", err)
panic(err)
}
mac := &macaroon.Macaroon{}
if err = mac.UnmarshalBinary(macaroonBytes); err != nil {
log.Println("Cannot unmarshal macaroon .. ", err)
panic(err)
}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
transportCredentials, err := credentials.NewClientTLSFromFile(tlsFile, hostname)
if err != nil {
panic(err)
}
fullHostname := fmt.Sprintf("%s:%d", hostname, port)
connection, err := grpc.DialContext(ctx, fullHostname, []grpc.DialOption{
grpc.WithBlock(),
grpc.WithTransportCredentials(transportCredentials),
grpc.WithPerRPCCredentials(newCreds(macaroonBytes)),
}...)
if err != nil {
log.Printf("unable to connect to %s: %w", fullHostname, err)
panic(err)
}
return routerrpc.NewRouterClient(connection)
}
func pay_invoice(invoice string) (payment_status string, failure_reason string, return_err error) {
payment_status = ""
failure_reason = ""
return_err = nil
// SendPaymentV2
ctx2, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
// get node parameters from environment variables
ln_port, err := strconv.Atoi(os.Getenv("LN_PORT"))
if err != nil {
return_err = err
return
}
r_client := getRouterClient(
os.Getenv("LN_HOST"),
ln_port,
os.Getenv("LN_TLS_FILE"),
os.Getenv("LN_MACAROON_FILE"))
fee_limit_sat_str := os.Getenv("FEE_LIMIT_SAT")
fee_limit_sat, err := strconv.ParseInt(fee_limit_sat_str, 10, 64)
if err != nil {
return_err = err
return
}
stream, err := r_client.SendPaymentV2(ctx2, &routerrpc.SendPaymentRequest{
PaymentRequest: invoice,
NoInflightUpdates: true,
TimeoutSeconds: 30,
FeeLimitSat: fee_limit_sat})
if err != nil {
return_err = err
return
}
for {
update, err := stream.Recv()
if err == io.EOF {
break
}
if err != nil {
return_err = err
return
}
payment_status = lnrpc.Payment_PaymentStatus_name[int32(update.Status)]
failure_reason = lnrpc.PaymentFailureReason_name[int32(update.FailureReason)]
}
return
}

142
lnurlw_callback.go Normal file
View file

@ -0,0 +1,142 @@
package main
import (
decodepay "github.com/fiatjaf/ln-decodepay"
log "github.com/sirupsen/logrus"
"net/http"
"os"
)
func lnurlw_callback(w http.ResponseWriter, req *http.Request) {
url := req.URL.RequestURI()
log.WithFields(log.Fields{"url": url}).Debug("cb request")
// check k1 value
params_k1, ok := req.URL.Query()["k1"]
if !ok || len(params_k1[0]) < 1 {
log.WithFields(log.Fields{"url": url}).Debug("k1 not found")
return
}
param_k1 := params_k1[0]
p, err := db_get_payment_k1(param_k1)
if err != nil {
log.WithFields(log.Fields{"url": url, "k1": param_k1}).Warn(err)
return
}
// check that payment has not been made
if p.paid_flag != "N" {
log.WithFields(log.Fields{"card_payment_id": p.card_payment_id}).Info("payment already made")
return
}
// check if lnurlw_request has timed out
lnurlw_timeout, err := db_check_lnurlw_timeout(p.card_payment_id)
if err != nil {
log.WithFields(log.Fields{"card_payment_id": p.card_payment_id}).Warn(err)
return
}
if lnurlw_timeout == true {
log.WithFields(log.Fields{"card_payment_id": p.card_payment_id}).Info("lnurlw request has timed out")
return
}
params_pr, ok := req.URL.Query()["pr"]
if !ok || len(params_pr[0]) < 1 {
log.WithFields(log.Fields{"card_payment_id": p.card_payment_id}).Warn("pr field not found")
return
}
param_pr := params_pr[0]
bolt11, _ := decodepay.Decodepay(param_pr)
// record the lightning invoice
err = db_update_payment_invoice(p.card_payment_id, param_pr, bolt11.MSatoshi)
if err != nil {
log.WithFields(log.Fields{"card_payment_id": p.card_payment_id}).Warn(err)
return
}
log.WithFields(log.Fields{"card_payment_id": p.card_payment_id}).Debug("checking payment rules")
// check if we are only sending funds to a defined test node
testnode := os.Getenv("LN_TESTNODE")
if testnode != "" && bolt11.Payee != testnode {
log.WithFields(log.Fields{"card_payment_id": p.card_payment_id}).Info("rejected as not the defined test node")
return
}
// check amount limits
invoice_sats := int(bolt11.MSatoshi / 1000)
day_total_sats, err := db_get_card_totals(p.card_id)
if err != nil {
log.WithFields(log.Fields{"card_payment_id": p.card_payment_id}).Warn(err)
return
}
c, err := db_get_card_from_card_id(p.card_id)
if err != nil {
log.WithFields(log.Fields{"card_payment_id": p.card_payment_id}).Warn(err)
return
}
if invoice_sats > c.tx_limit_sats {
log.WithFields(log.Fields{"card_payment_id": p.card_payment_id}).Info("invoice_sats: ", invoice_sats)
log.WithFields(log.Fields{"card_payment_id": p.card_payment_id}).Info("tx_limit_sats: ", c.tx_limit_sats)
log.WithFields(log.Fields{"card_payment_id": p.card_payment_id}).Info("over tx_limit_sats!")
return
}
if day_total_sats+invoice_sats > c.day_limit_sats {
log.WithFields(log.Fields{"card_payment_id": p.card_payment_id}).Info("invoice_sats: ", invoice_sats)
log.WithFields(log.Fields{"card_payment_id": p.card_payment_id}).Info("day_total_sats: ", day_total_sats)
log.WithFields(log.Fields{"card_payment_id": p.card_payment_id}).Info("day_limit_sats: ", c.day_limit_sats)
log.WithFields(log.Fields{"card_payment_id": p.card_payment_id}).Info("over day_limit_sats!")
return
}
log.WithFields(log.Fields{"card_payment_id": p.card_payment_id}).Info("paying invoice")
// update paid_flag so we only attempt payment once
err = db_update_payment_paid(p.card_payment_id)
if err != nil {
log.WithFields(log.Fields{"card_payment_id": p.card_payment_id}).Warn(err)
return
}
// https://github.com/fiatjaf/lnurl-rfc/blob/luds/03.md
//
// LN SERVICE sends a {"status": "OK"} or
// {"status": "ERROR", "reason": "error details..."}
// JSON response and then attempts to pay the invoices asynchronously.
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
jsonData := []byte(`{"status":"OK"}`)
w.Write(jsonData)
payment_status, failure_reason, err := pay_invoice(param_pr)
if err != nil {
log.WithFields(log.Fields{"card_payment_id": p.card_payment_id}).Warn(err)
return
}
if failure_reason != "FAILURE_REASON_NONE" {
log.WithFields(log.Fields{"card_payment_id": p.card_payment_id}).Info("payment failure reason : ", failure_reason)
}
log.WithFields(log.Fields{"card_payment_id": p.card_payment_id}).Info("payment status : ", payment_status)
// store result in database
err = db_update_payment_status(p.card_payment_id, payment_status, failure_reason)
if err != nil {
log.WithFields(log.Fields{"card_payment_id": p.card_payment_id}).Warn(err)
return
}
}

215
lnurlw_request.go Normal file
View file

@ -0,0 +1,215 @@
package main
import (
"encoding/hex"
"encoding/json"
"errors"
log "github.com/sirupsen/logrus"
"net/http"
"os"
"strconv"
)
type Response struct {
Tag string `json:"tag"`
Callback string `json:"callback"`
K1 string `json:"k1"`
DefaultDescription string `json:"defaultDescription"`
MinWithdrawable int `json:"minWithdrawable"`
MaxWithdrawable int `json:"maxWithdrawable"`
}
func get_p_c(req *http.Request, p_name string, c_name string) (p string, c string) {
params_p, ok := req.URL.Query()[p_name]
if !ok || len(params_p[0]) < 1 {
return "", ""
}
params_c, ok := req.URL.Query()[c_name]
if !ok || len(params_c[0]) < 1 {
return "", ""
}
p = params_p[0]
c = params_c[0]
return
}
func parse_request(req *http.Request) (int, error) {
url := req.URL.RequestURI()
log.Debug("ln url: ", url)
param_p, param_c := get_p_c(req, "p", "c")
ba_p, err := hex.DecodeString(param_p)
if err != nil {
return 0, errors.New("p parameter not valid hex")
}
ba_c, err := hex.DecodeString(param_c)
if err != nil {
return 0, errors.New("c parameter not valid hex")
}
if len(ba_p) != 16 {
return 0, errors.New("p parameter length not valid")
}
if len(ba_c) != 8 {
return 0, errors.New("c parameter length not valid")
}
// decrypt p with aes_decrypt_key
aes_decrypt_key := os.Getenv("AES_DECRYPT_KEY")
key_sdm_file_read, err := hex.DecodeString(aes_decrypt_key)
if err != nil {
return 0, err
}
dec_p, err := crypto_aes_decrypt(key_sdm_file_read, ba_p)
if err != nil {
return 0, err
}
if dec_p[0] != 0xC7 {
return 0, errors.New("decrypted data not starting with 0xC7")
}
uid := dec_p[1:8]
ctr := dec_p[8:11]
ctr_int := uint32(ctr[2])<<16 | uint32(ctr[1])<<8 | uint32(ctr[0])
// get card record from database for UID
uid_str := hex.EncodeToString(uid)
log.Debug("card UID: ", uid_str)
c, err := db_get_card_from_uid(uid_str)
if err != nil {
return 0, errors.New("card not found for UID")
}
// check if card is enabled
if c.enable_flag != "Y" {
return 0, errors.New("card enable is not set to Y")
}
// check cmac
sv2 := make([]byte, 16)
sv2[0] = 0x3c
sv2[1] = 0xc3
sv2[2] = 0x00
sv2[3] = 0x01
sv2[4] = 0x00
sv2[5] = 0x80
sv2[6] = uid[0]
sv2[7] = uid[1]
sv2[8] = uid[2]
sv2[9] = uid[3]
sv2[10] = uid[4]
sv2[11] = uid[5]
sv2[12] = uid[6]
sv2[13] = ctr[0]
sv2[14] = ctr[1]
sv2[15] = ctr[2]
key_sdm_file_read_mac, err := hex.DecodeString(c.aes_cmac)
if err != nil {
return 0, err
}
cmac_verified, err := crypto_aes_cmac(key_sdm_file_read_mac, sv2, ba_c)
if err != nil {
return 0, err
}
if cmac_verified == false {
return 0, errors.New("CMAC incorrect")
}
// check and update last_counter_value
counter_ok, err := db_check_and_update_counter(c.card_id, ctr_int)
if err != nil {
return 0, err
}
if counter_ok == false {
return 0, errors.New("counter not increasing")
}
log.WithFields(log.Fields{"card_id": c.card_id, "counter": ctr_int}).Info("validated")
return c.card_id, nil
}
func lnurlw_response(w http.ResponseWriter, req *http.Request) {
card_id, err := parse_request(req)
if err != nil {
log.Debug(err.Error())
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
jsonData := []byte(`{"status":"ERROR","reason":"bad request"}`)
w.Write(jsonData)
return
}
k1, err := create_k1()
if err != nil {
log.Warn(err.Error())
return
}
// store k1 in database and include in response
err = db_insert_payment(card_id, k1)
if err != nil {
log.Warn(err.Error())
return
}
lnurlw_cb_url := os.Getenv("LNURLW_CB_URL")
min_withdraw_sats_str := os.Getenv("MIN_WITHDRAW_SATS")
min_withdraw_sats, err := strconv.Atoi(min_withdraw_sats_str)
if err != nil {
log.Warn(err.Error())
return
}
max_withdraw_sats_str := os.Getenv("MAX_WITHDRAW_SATS")
max_withdraw_sats, err := strconv.Atoi(max_withdraw_sats_str)
if err != nil {
log.Warn(err.Error())
return
}
response := Response{}
response.Tag = "withdrawRequest"
response.Callback = lnurlw_cb_url
response.K1 = k1
response.DefaultDescription = "WWT withdrawal"
response.MinWithdrawable = min_withdraw_sats * 1000 // milliSats
response.MaxWithdrawable = max_withdraw_sats * 1000 // milliSats
jsonData, err := json.Marshal(response)
if err != nil {
log.Warn(err)
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
w.Write(jsonData)
}

27
main.go Normal file
View file

@ -0,0 +1,27 @@
package main
import (
log "github.com/sirupsen/logrus"
"net/http"
"os"
)
func main() {
log_level := os.Getenv("LOG_LEVEL")
if log_level == "DEBUG" {
log.SetLevel(log.DebugLevel)
}
log.SetFormatter(&log.JSONFormatter{
DisableHTMLEscape: true,
})
mux := http.NewServeMux()
mux.HandleFunc("/ln", lnurlw_response)
mux.HandleFunc("/cb", lnurlw_callback)
err := http.ListenAndServe(":9000", mux)
log.Fatal(err)
}

5
s_build Executable file
View file

@ -0,0 +1,5 @@
go build
sudo systemctl daemon-reload
sudo systemctl stop boltcard
sudo systemctl start boltcard

6
s_create_db Executable file
View file

@ -0,0 +1,6 @@
# to close any database connections
sudo systemctl stop postgresql
sudo systemctl start postgresql
psql postgres -f create_db.sql
psql postgres -f add_card_data.sql

4
s_launch Executable file
View file

@ -0,0 +1,4 @@
export PATH=$PATH:/usr/local/go/bin
cd /home/ubuntu/boltcard
./boltcard

3
s_restart Executable file
View file

@ -0,0 +1,3 @@
sudo systemctl daemon-reload
sudo systemctl stop boltcard
sudo systemctl start boltcard