commit 037788d6de10f62583dcff48e24dd71ac5f96ca6 Author: Peter Rounce Date: Mon Aug 1 10:36:32 2022 +0000 initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ed1074e --- /dev/null +++ b/.gitignore @@ -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 diff --git a/Caddyfile b/Caddyfile new file mode 100644 index 0000000..740c8bf --- /dev/null +++ b/Caddyfile @@ -0,0 +1,2 @@ +https://card.yourdomain.com +reverse_proxy 127.0.0.1:9000 diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..fb50f35 --- /dev/null +++ b/LICENSE @@ -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. diff --git a/README.md b/README.md new file mode 100644 index 0000000..c6a5cb8 --- /dev/null +++ b/README.md @@ -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 . diff --git a/add_card_data.sql b/add_card_data.sql new file mode 100644 index 0000000..6850956 --- /dev/null +++ b/add_card_data.sql @@ -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' + ); diff --git a/boltcard.service b/boltcard.service new file mode 100644 index 0000000..b6a9149 --- /dev/null +++ b/boltcard.service @@ -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 diff --git a/create_db.sql b/create_db.sql new file mode 100644 index 0000000..553e343 --- /dev/null +++ b/create_db.sql @@ -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; diff --git a/crypto.go b/crypto.go new file mode 100644 index 0000000..23ed884 --- /dev/null +++ b/crypto.go @@ -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 +} diff --git a/database.go b/database.go new file mode 100644 index 0000000..b007843 --- /dev/null +++ b/database.go @@ -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 +} diff --git a/docs/CARD.md b/docs/CARD.md new file mode 100644 index 0000000..b14316f --- /dev/null +++ b/docs/CARD.md @@ -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 diff --git a/docs/FAQ.md b/docs/FAQ.md new file mode 100644 index 0000000..748c95f --- /dev/null +++ b/docs/FAQ.md @@ -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. diff --git a/docs/INSTALL.md b/docs/INSTALL.md new file mode 100644 index 0000000..2ef4c3a --- /dev/null +++ b/docs/INSTALL.md @@ -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) diff --git a/docs/SECURITY.md b/docs/SECURITY.md new file mode 100644 index 0000000..22fd371 --- /dev/null +++ b/docs/SECURITY.md @@ -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` diff --git a/docs/SPEC.md b/docs/SPEC.md new file mode 100644 index 0000000..1531002 --- /dev/null +++ b/docs/SPEC.md @@ -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 + diff --git a/docs/SYSTEM.md b/docs/SYSTEM.md new file mode 100644 index 0000000..158b8ad --- /dev/null +++ b/docs/SYSTEM.md @@ -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 +``` diff --git a/docs/images/ac.webp b/docs/images/ac.webp new file mode 100644 index 0000000..aca1719 Binary files /dev/null and b/docs/images/ac.webp differ diff --git a/docs/images/aes-dec.webp b/docs/images/aes-dec.webp new file mode 100644 index 0000000..3b38425 Binary files /dev/null and b/docs/images/aes-dec.webp differ diff --git a/docs/images/aesd.webp b/docs/images/aesd.webp new file mode 100644 index 0000000..31ba163 Binary files /dev/null and b/docs/images/aesd.webp differ diff --git a/docs/images/avs.webp b/docs/images/avs.webp new file mode 100644 index 0000000..6061d40 Binary files /dev/null and b/docs/images/avs.webp differ diff --git a/docs/images/breez.webp b/docs/images/breez.webp new file mode 100644 index 0000000..dbdc811 Binary files /dev/null and b/docs/images/breez.webp differ diff --git a/docs/images/btcpayserver.webp b/docs/images/btcpayserver.webp new file mode 100644 index 0000000..a883153 Binary files /dev/null and b/docs/images/btcpayserver.webp differ diff --git a/docs/images/btr.webp b/docs/images/btr.webp new file mode 100644 index 0000000..c4731ad Binary files /dev/null and b/docs/images/btr.webp differ diff --git a/docs/images/card.webp b/docs/images/card.webp new file mode 100644 index 0000000..63a636b Binary files /dev/null and b/docs/images/card.webp differ diff --git a/docs/images/ccs.webp b/docs/images/ccs.webp new file mode 100644 index 0000000..a7f5377 Binary files /dev/null and b/docs/images/ccs.webp differ diff --git a/docs/images/cfs.webp b/docs/images/cfs.webp new file mode 100644 index 0000000..36ba6ed Binary files /dev/null and b/docs/images/cfs.webp differ diff --git a/docs/images/cmac-aes.webp b/docs/images/cmac-aes.webp new file mode 100644 index 0000000..11a6444 Binary files /dev/null and b/docs/images/cmac-aes.webp differ diff --git a/docs/images/cmac.webp b/docs/images/cmac.webp new file mode 100644 index 0000000..83dd86f Binary files /dev/null and b/docs/images/cmac.webp differ diff --git a/docs/images/con.webp b/docs/images/con.webp new file mode 100644 index 0000000..efbbe36 Binary files /dev/null and b/docs/images/con.webp differ diff --git a/docs/images/fs-add.webp b/docs/images/fs-add.webp new file mode 100644 index 0000000..6d15cec Binary files /dev/null and b/docs/images/fs-add.webp differ diff --git a/docs/images/fs.webp b/docs/images/fs.webp new file mode 100644 index 0000000..1eb80d9 Binary files /dev/null and b/docs/images/fs.webp differ diff --git a/docs/images/gfs.webp b/docs/images/gfs.webp new file mode 100644 index 0000000..8eab31b Binary files /dev/null and b/docs/images/gfs.webp differ diff --git a/docs/images/hex.webp b/docs/images/hex.webp new file mode 100644 index 0000000..0d5b201 Binary files /dev/null and b/docs/images/hex.webp differ diff --git a/docs/images/lock.webp b/docs/images/lock.webp new file mode 100644 index 0000000..a7c5144 Binary files /dev/null and b/docs/images/lock.webp differ diff --git a/docs/images/nfc.webp b/docs/images/nfc.webp new file mode 100644 index 0000000..2dfcf8e Binary files /dev/null and b/docs/images/nfc.webp differ diff --git a/docs/images/nfwc.webp b/docs/images/nfwc.webp new file mode 100644 index 0000000..33492b2 Binary files /dev/null and b/docs/images/nfwc.webp differ diff --git a/docs/images/nrd.webp b/docs/images/nrd.webp new file mode 100644 index 0000000..e95848a Binary files /dev/null and b/docs/images/nrd.webp differ diff --git a/docs/images/picc.webp b/docs/images/picc.webp new file mode 100644 index 0000000..04f2fff Binary files /dev/null and b/docs/images/picc.webp differ diff --git a/docs/images/rd-txt.webp b/docs/images/rd-txt.webp new file mode 100644 index 0000000..943307c Binary files /dev/null and b/docs/images/rd-txt.webp differ diff --git a/docs/images/rd.webp b/docs/images/rd.webp new file mode 100644 index 0000000..bd7726e Binary files /dev/null and b/docs/images/rd.webp differ diff --git a/docs/images/rdh.webp b/docs/images/rdh.webp new file mode 100644 index 0000000..bd7fa42 Binary files /dev/null and b/docs/images/rdh.webp differ diff --git a/docs/images/ref.webp b/docs/images/ref.webp new file mode 100644 index 0000000..6b73eed Binary files /dev/null and b/docs/images/ref.webp differ diff --git a/docs/images/su.webp b/docs/images/su.webp new file mode 100644 index 0000000..9878891 Binary files /dev/null and b/docs/images/su.webp differ diff --git a/docs/images/sun.webp b/docs/images/sun.webp new file mode 100644 index 0000000..a4235d1 Binary files /dev/null and b/docs/images/sun.webp differ diff --git a/docs/images/wr.webp b/docs/images/wr.webp new file mode 100644 index 0000000..0ccdc9e Binary files /dev/null and b/docs/images/wr.webp differ diff --git a/docs/images/wrh.webp b/docs/images/wrh.webp new file mode 100644 index 0000000..4fc9fb2 Binary files /dev/null and b/docs/images/wrh.webp differ diff --git a/docs/images/ws.webp b/docs/images/ws.webp new file mode 100644 index 0000000..72a778f Binary files /dev/null and b/docs/images/ws.webp differ diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..2dcfdd7 --- /dev/null +++ b/go.mod @@ -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 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..4a9d6ed --- /dev/null +++ b/go.sum @@ -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= diff --git a/lightning.go b/lightning.go new file mode 100644 index 0000000..ade078e --- /dev/null +++ b/lightning.go @@ -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 +} diff --git a/lnurlw_callback.go b/lnurlw_callback.go new file mode 100644 index 0000000..0fc43d2 --- /dev/null +++ b/lnurlw_callback.go @@ -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 + } +} diff --git a/lnurlw_request.go b/lnurlw_request.go new file mode 100644 index 0000000..dd6faf5 --- /dev/null +++ b/lnurlw_request.go @@ -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) +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..5ec3aa1 --- /dev/null +++ b/main.go @@ -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) +} diff --git a/s_build b/s_build new file mode 100755 index 0000000..e1722d5 --- /dev/null +++ b/s_build @@ -0,0 +1,5 @@ +go build +sudo systemctl daemon-reload +sudo systemctl stop boltcard +sudo systemctl start boltcard + diff --git a/s_create_db b/s_create_db new file mode 100755 index 0000000..fadfa94 --- /dev/null +++ b/s_create_db @@ -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 diff --git a/s_launch b/s_launch new file mode 100755 index 0000000..5aee1ca --- /dev/null +++ b/s_launch @@ -0,0 +1,4 @@ +export PATH=$PATH:/usr/local/go/bin + +cd /home/ubuntu/boltcard +./boltcard diff --git a/s_restart b/s_restart new file mode 100755 index 0000000..bae13dc --- /dev/null +++ b/s_restart @@ -0,0 +1,3 @@ +sudo systemctl daemon-reload +sudo systemctl stop boltcard +sudo systemctl start boltcard