Merge pull request #14 from boltcard/ln-addr

adds support for lightning address receipts & email notifications
This commit is contained in:
Peter Rounce 2022-09-20 04:03:54 +01:00 committed by GitHub
commit 5d3072fa6d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 977 additions and 120 deletions

View file

@ -15,10 +15,13 @@ CREATE TABLE cards (
uid CHAR(14) NOT NULL, uid CHAR(14) NOT NULL,
last_counter_value INTEGER NOT NULL, last_counter_value INTEGER NOT NULL,
lnurlw_request_timeout_sec INT NOT NULL, lnurlw_request_timeout_sec INT NOT NULL,
enable_flag CHAR(1) NOT NULL DEFAULT 'N', lnurlw_enable CHAR(1) NOT NULL DEFAULT 'N',
tx_limit_sats INT NOT NULL, tx_limit_sats INT NOT NULL,
day_limit_sats INT NOT NULL, day_limit_sats INT NOT NULL,
card_name VARCHAR(100) NOT NULL DEFAULT '', lnurlp_enable CHAR(1) NOT NULL DEFAULT 'N',
card_name VARCHAR(100) UNIQUE NOT NULL DEFAULT '',
email_address VARCHAR(100) DEFAULT '',
email_enable CHAR(1) NOT NULL DEFAULT 'N',
one_time_code CHAR(32) NOT NULL DEFAULT '', one_time_code CHAR(32) NOT NULL DEFAULT '',
one_time_code_expiry TIMESTAMPTZ DEFAULT NOW() + INTERVAL '1 DAY', one_time_code_expiry TIMESTAMPTZ DEFAULT NOW() + INTERVAL '1 DAY',
one_time_code_used CHAR(1) NOT NULL DEFAULT 'Y', one_time_code_used CHAR(1) NOT NULL DEFAULT 'Y',
@ -41,5 +44,19 @@ CREATE TABLE card_payments (
CONSTRAINT fk_card FOREIGN KEY(card_id) REFERENCES cards(card_id) CONSTRAINT fk_card FOREIGN KEY(card_id) REFERENCES cards(card_id)
); );
CREATE TABLE card_receipts (
card_receipt_id INT GENERATED ALWAYS AS IDENTITY,
card_id INT NOT NULL,
ln_invoice VARCHAR(1024) NOT NULL DEFAULT '',
r_hash_hex CHAR(64) UNIQUE NOT NULL DEFAULT '',
amount_msats BIGINT CHECK (amount_msats > 0),
receipt_status VARCHAR(100) NOT NULL DEFAULT '',
receipt_status_time TIMESTAMPTZ,
CONSTRAINT fk_card FOREIGN KEY(card_id) REFERENCES cards(card_id)
);
GRANT ALL PRIVILEGES ON TABLE cards TO cardapp; GRANT ALL PRIVILEGES ON TABLE cards TO cardapp;
GRANT ALL PRIVILEGES ON TABLE card_payments TO cardapp; GRANT ALL PRIVILEGES ON TABLE card_payments TO cardapp;
GRANT ALL PRIVILEGES ON TABLE card_receipts TO cardapp;

View file

@ -19,9 +19,12 @@ type Card struct {
db_uid string db_uid string
last_counter_value uint32 last_counter_value uint32
lnurlw_request_timeout_sec int lnurlw_request_timeout_sec int
enable_flag string lnurlw_enable string
tx_limit_sats int tx_limit_sats int
day_limit_sats int day_limit_sats int
lnurlp_enable string
email_address string
email_enable string
one_time_code string one_time_code string
card_name string card_name string
} }
@ -106,6 +109,88 @@ func db_get_card_count_for_uid(uid string) (int, error) {
return card_count, nil return card_count, nil
} }
func db_get_card_count_for_name_lnurlp(name string) (int, error) {
card_count := 0
db, err := db_open()
if err != nil {
return 0, err
}
defer db.Close()
sqlStatement := `select count(card_id) from cards where card_name=$1 and lnurlp_enable='Y';`
row := db.QueryRow(sqlStatement, name)
err = row.Scan(&card_count)
if err != nil {
return 0, err
}
return card_count, nil
}
func db_get_card_id_for_name(name string) (int, error) {
card_id := 0
db, err := db_open()
if err != nil {
return 0, err
}
defer db.Close()
sqlStatement := `select card_id from cards where card_name=$1;`
row := db.QueryRow(sqlStatement, name)
err = row.Scan(&card_id)
if err != nil {
return 0, err
}
return card_id, nil
}
func db_get_card_id_for_card_payment_id(card_payment_id int) (int, error) {
card_id := 0
db, err := db_open()
if err != nil {
return 0, err
}
defer db.Close()
sqlStatement := `SELECT card_id FROM card_payments WHERE card_payment_id=$1;`
row := db.QueryRow(sqlStatement, card_payment_id)
err = row.Scan(&card_id)
if err != nil {
return 0, err
}
return card_id, nil
}
func db_get_card_id_for_r_hash(r_hash string) (int, error) {
card_id := 0
db, err := db_open()
if err != nil {
return 0, err
}
defer db.Close()
sqlStatement := `SELECT card_id FROM card_receipts WHERE r_hash_hex=$1;`
row := db.QueryRow(sqlStatement, r_hash)
err = row.Scan(&card_id)
if err != nil {
return 0, err
}
return card_id, nil
}
func db_get_cards_blank_uid() ([]Card, error) { func db_get_cards_blank_uid() ([]Card, error) {
// open the database // open the database
@ -189,7 +274,7 @@ func db_get_card_from_uid(card_uid string) (*Card, error) {
sqlStatement := `SELECT card_id, k2_cmac_key, uid,` + sqlStatement := `SELECT card_id, k2_cmac_key, uid,` +
` last_counter_value, lnurlw_request_timeout_sec,` + ` last_counter_value, lnurlw_request_timeout_sec,` +
` enable_flag, tx_limit_sats, day_limit_sats` + ` lnurlw_enable, tx_limit_sats, day_limit_sats` +
` FROM cards WHERE uid=$1;` ` FROM cards WHERE uid=$1;`
row := db.QueryRow(sqlStatement, card_uid) row := db.QueryRow(sqlStatement, card_uid)
err = row.Scan( err = row.Scan(
@ -198,7 +283,7 @@ func db_get_card_from_uid(card_uid string) (*Card, error) {
&c.db_uid, &c.db_uid,
&c.last_counter_value, &c.last_counter_value,
&c.lnurlw_request_timeout_sec, &c.lnurlw_request_timeout_sec,
&c.enable_flag, &c.lnurlw_enable,
&c.tx_limit_sats, &c.tx_limit_sats,
&c.day_limit_sats) &c.day_limit_sats)
if err != nil { if err != nil {
@ -220,7 +305,7 @@ func db_get_card_from_card_id(card_id int) (*Card, error) {
sqlStatement := `SELECT card_id, k2_cmac_key, uid,` + sqlStatement := `SELECT card_id, k2_cmac_key, uid,` +
` last_counter_value, lnurlw_request_timeout_sec,` + ` last_counter_value, lnurlw_request_timeout_sec,` +
` enable_flag, tx_limit_sats, day_limit_sats` + ` lnurlw_enable, tx_limit_sats, day_limit_sats, email_enable, email_address` +
` FROM cards WHERE card_id=$1;` ` FROM cards WHERE card_id=$1;`
row := db.QueryRow(sqlStatement, card_id) row := db.QueryRow(sqlStatement, card_id)
err = row.Scan( err = row.Scan(
@ -229,9 +314,11 @@ func db_get_card_from_card_id(card_id int) (*Card, error) {
&c.db_uid, &c.db_uid,
&c.last_counter_value, &c.last_counter_value,
&c.lnurlw_request_timeout_sec, &c.lnurlw_request_timeout_sec,
&c.enable_flag, &c.lnurlw_enable,
&c.tx_limit_sats, &c.tx_limit_sats,
&c.day_limit_sats) &c.day_limit_sats,
&c.email_enable,
&c.email_address)
if err != nil { if err != nil {
return &c, err return &c, err
} }
@ -297,8 +384,8 @@ func db_insert_payment(card_id int, lnurlw_k1 string) error {
// insert a new record into card_payments with card_id & lnurlw_k1 set // insert a new record into card_payments with card_id & lnurlw_k1 set
sqlStatement := `INSERT INTO card_payments` + sqlStatement := `INSERT INTO card_payments` +
` (card_id, lnurlw_k1, paid_flag, lnurlw_request_time)` + ` (card_id, lnurlw_k1, paid_flag, lnurlw_request_time, payment_status_time)` +
` VALUES ($1, $2, 'N', NOW());` ` VALUES ($1, $2, 'N', NOW(), NOW());`
res, err := db.Exec(sqlStatement, card_id, lnurlw_k1) res, err := db.Exec(sqlStatement, card_id, lnurlw_k1)
if err != nil { if err != nil {
return err return err
@ -314,6 +401,63 @@ func db_insert_payment(card_id int, lnurlw_k1 string) error {
return nil return nil
} }
func db_insert_receipt(
card_id int,
ln_invoice string,
r_hash_hex string,
amount_msat int64) error {
db, err := db_open()
if err != nil {
return err
}
defer db.Close()
// insert a new record into card_receipts
sqlStatement := `INSERT INTO card_receipts` +
` (card_id, ln_invoice, r_hash_hex, amount_msats, receipt_status_time)` +
` VALUES ($1, $2, $3, $4, NOW());`
res, err := db.Exec(sqlStatement, card_id, ln_invoice, r_hash_hex, amount_msat)
if err != nil {
return err
}
count, err := res.RowsAffected()
if err != nil {
return err
}
if count != 1 {
return errors.New("not one card_receipts record inserted")
}
return nil
}
func db_update_receipt_state(r_hash_hex string, invoice_state string) error {
db, err := db_open()
if err != nil {
return err
}
defer db.Close()
sqlStatement := `UPDATE card_receipts ` +
`SET receipt_status = $2, receipt_status_time = NOW() ` +
`WHERE r_hash_hex = $1;`
res, err := db.Exec(sqlStatement, r_hash_hex, invoice_state)
if err != nil {
return err
}
count, err := res.RowsAffected()
if err != nil {
return err
}
if count != 1 {
return errors.New("not one card_receipts record updated")
}
return nil
}
func db_get_payment_k1(lnurlw_k1 string) (*payment, error) { func db_get_payment_k1(lnurlw_k1 string) (*payment, error) {
p := payment{} p := payment{}
@ -355,7 +499,7 @@ func db_update_payment_invoice(card_payment_id int, ln_invoice string, amount_ms
return err return err
} }
if count != 1 { if count != 1 {
return errors.New("not one card_payment record updated") return errors.New("not one card_payments record updated")
} }
return nil return nil

View file

@ -52,7 +52,6 @@ edit `create_db.sql` to set the cardapp password
`$ ./s_create_db` `$ ./s_create_db`
### boltcard service install ### boltcard service install
`$ sudo cp boltcard.service /etc/systemd/system/boltcard.service`
`$ ./s_build` `$ ./s_build`
`$ sudo systemctl enable boltcard` `$ sudo systemctl enable boltcard`
`$ sudo systemctl status boltcard` `$ sudo systemctl status boltcard`

79
email.go Normal file
View file

@ -0,0 +1,79 @@
package main
import (
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/awserr"
"github.com/aws/aws-sdk-go/aws/credentials"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/ses"
log "github.com/sirupsen/logrus"
"os"
)
// https://docs.aws.amazon.com/sdk-for-go/v1/developer-guide/ses-example-send-email.html
func send_email(recipient string, subject string, htmlBody string, textBody string) {
aws_ses_id := os.Getenv("AWS_SES_ID")
aws_ses_secret := os.Getenv("AWS_SES_SECRET")
sender := os.Getenv("AWS_SES_EMAIL_FROM")
sess, err := session.NewSession(&aws.Config{
Region: aws.String("us-east-1"),
Credentials: credentials.NewStaticCredentials(aws_ses_id, aws_ses_secret, ""),
})
svc := ses.New(sess)
charSet := "UTF-8"
input := &ses.SendEmailInput{
Destination: &ses.Destination{
CcAddresses: []*string{},
ToAddresses: []*string{
aws.String(recipient),
},
},
Message: &ses.Message{
Body: &ses.Body{
Html: &ses.Content{
Charset: aws.String(charSet),
Data: aws.String(htmlBody),
},
Text: &ses.Content{
Charset: aws.String(charSet),
Data: aws.String(textBody),
},
},
Subject: &ses.Content{
Charset: aws.String(charSet),
Data: aws.String(subject),
},
},
Source: aws.String(sender),
//ConfigurationSetName: aws.String(ConfigurationSet),
}
result, err := svc.SendEmail(input)
if err != nil {
if aerr, ok := err.(awserr.Error); ok {
switch aerr.Code() {
case ses.ErrCodeMessageRejected:
log.Warn(ses.ErrCodeMessageRejected, aerr.Error())
case ses.ErrCodeMailFromDomainNotVerifiedException:
log.Warn(ses.ErrCodeMailFromDomainNotVerifiedException, aerr.Error())
case ses.ErrCodeConfigurationSetDoesNotExistException:
log.Warn(ses.ErrCodeConfigurationSetDoesNotExistException, aerr.Error())
default:
log.Warn(aerr.Error())
}
} else {
log.Warn(err.Error())
}
return
}
log.WithFields(log.Fields{"result": result}).Info("email sent")
}

22
go.mod
View file

@ -2,11 +2,23 @@ module github.com/boltcard/boltcard
go 1.18 go 1.18
require (
github.com/aead/cmac v0.0.0-20160719120800-7af84192f0b1
github.com/fiatjaf/ln-decodepay v1.4.0
github.com/gorilla/mux v1.8.0
github.com/lib/pq v1.10.6
github.com/lightningnetwork/lnd v0.15.1-beta.rc2
github.com/sirupsen/logrus v1.9.0
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
google.golang.org/grpc v1.49.0
gopkg.in/macaroon.v2 v2.1.0
)
require ( require (
github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da // indirect github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da // indirect
github.com/aead/cmac v0.0.0-20160719120800-7af84192f0b1 // indirect
github.com/aead/siphash v1.0.1 // indirect github.com/aead/siphash v1.0.1 // indirect
github.com/andybalholm/brotli v1.0.4 // indirect github.com/andybalholm/brotli v1.0.4 // indirect
github.com/aws/aws-sdk-go v1.44.101 // indirect
github.com/beorn7/perks v1.0.1 // indirect github.com/beorn7/perks v1.0.1 // indirect
github.com/btcsuite/btcd v0.23.1 // indirect github.com/btcsuite/btcd v0.23.1 // indirect
github.com/btcsuite/btcd/btcec/v2 v2.2.1 // indirect github.com/btcsuite/btcd/btcec/v2 v2.2.1 // indirect
@ -35,7 +47,6 @@ require (
github.com/dustin/go-humanize v1.0.0 // indirect github.com/dustin/go-humanize v1.0.0 // indirect
github.com/dvyukov/go-fuzz v0.0.0-20220726122315-1d375ef9f9f6 // indirect github.com/dvyukov/go-fuzz v0.0.0-20220726122315-1d375ef9f9f6 // indirect
github.com/fergusstrange/embedded-postgres v1.17.0 // indirect github.com/fergusstrange/embedded-postgres v1.17.0 // indirect
github.com/fiatjaf/ln-decodepay v1.4.0 // indirect
github.com/form3tech-oss/jwt-go v3.2.5+incompatible // indirect github.com/form3tech-oss/jwt-go v3.2.5+incompatible // indirect
github.com/go-errors/errors v1.4.2 // indirect github.com/go-errors/errors v1.4.2 // indirect
github.com/go-logr/logr v1.2.3 // indirect github.com/go-logr/logr v1.2.3 // indirect
@ -59,6 +70,7 @@ require (
github.com/jackc/pgtype v1.12.0 // indirect github.com/jackc/pgtype v1.12.0 // indirect
github.com/jackc/pgx/v4 v4.17.0 // indirect github.com/jackc/pgx/v4 v4.17.0 // indirect
github.com/jessevdk/go-flags v1.5.0 // indirect github.com/jessevdk/go-flags v1.5.0 // indirect
github.com/jmespath/go-jmespath v0.4.0 // indirect
github.com/jonboulle/clockwork v0.3.0 // indirect github.com/jonboulle/clockwork v0.3.0 // indirect
github.com/jrick/logrotate v1.0.0 // indirect github.com/jrick/logrotate v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect github.com/json-iterator/go v1.1.12 // indirect
@ -66,11 +78,9 @@ require (
github.com/kkdai/bstream v1.0.0 // indirect github.com/kkdai/bstream v1.0.0 // indirect
github.com/klauspost/compress v1.15.9 // indirect github.com/klauspost/compress v1.15.9 // indirect
github.com/klauspost/pgzip v1.2.5 // indirect github.com/klauspost/pgzip v1.2.5 // indirect
github.com/lib/pq v1.10.6 // indirect
github.com/lightninglabs/gozmq v0.0.0-20191113021534-d20a764486bf // indirect github.com/lightninglabs/gozmq v0.0.0-20191113021534-d20a764486bf // indirect
github.com/lightninglabs/neutrino v0.14.2 // indirect github.com/lightninglabs/neutrino v0.14.2 // indirect
github.com/lightningnetwork/lightning-onion v1.2.0 // indirect github.com/lightningnetwork/lightning-onion v1.2.0 // indirect
github.com/lightningnetwork/lnd v0.15.1-beta.rc2 // indirect
github.com/lightningnetwork/lnd/clock v1.1.0 // indirect github.com/lightningnetwork/lnd/clock v1.1.0 // indirect
github.com/lightningnetwork/lnd/healthcheck v1.2.2 // indirect github.com/lightningnetwork/lnd/healthcheck v1.2.2 // indirect
github.com/lightningnetwork/lnd/kvdb v1.3.1 // indirect github.com/lightningnetwork/lnd/kvdb v1.3.1 // indirect
@ -93,8 +103,6 @@ require (
github.com/prometheus/common v0.37.0 // indirect github.com/prometheus/common v0.37.0 // indirect
github.com/prometheus/procfs v0.8.0 // indirect github.com/prometheus/procfs v0.8.0 // indirect
github.com/rogpeppe/fastuuid v1.2.0 // indirect github.com/rogpeppe/fastuuid v1.2.0 // indirect
github.com/sirupsen/logrus v1.9.0 // indirect
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e // indirect
github.com/soheilhy/cmux v0.1.5 // indirect github.com/soheilhy/cmux v0.1.5 // indirect
github.com/spf13/pflag v1.0.5 // indirect github.com/spf13/pflag v1.0.5 // indirect
github.com/stretchr/objx v0.4.0 // indirect github.com/stretchr/objx v0.4.0 // indirect
@ -136,11 +144,9 @@ require (
golang.org/x/tools v0.1.12 // indirect golang.org/x/tools v0.1.12 // indirect
golang.org/x/xerrors v0.0.0-20220609144429-65e65417b02f // indirect golang.org/x/xerrors v0.0.0-20220609144429-65e65417b02f // indirect
google.golang.org/genproto v0.0.0-20220822174746-9e6da59bd2fc // indirect google.golang.org/genproto v0.0.0-20220822174746-9e6da59bd2fc // indirect
google.golang.org/grpc v1.49.0 // indirect
google.golang.org/protobuf v1.28.1 // indirect google.golang.org/protobuf v1.28.1 // indirect
gopkg.in/errgo.v1 v1.0.1 // indirect gopkg.in/errgo.v1 v1.0.1 // indirect
gopkg.in/macaroon-bakery.v2 v2.3.0 // indirect gopkg.in/macaroon-bakery.v2 v2.3.0 // indirect
gopkg.in/macaroon.v2 v2.1.0 // indirect
gopkg.in/natefinch/lumberjack.v2 v2.0.0 // indirect gopkg.in/natefinch/lumberjack.v2 v2.0.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect

381
go.sum

File diff suppressed because it is too large Load diff

View file

@ -6,12 +6,14 @@ import (
"fmt" "fmt"
"io" "io"
"io/ioutil" "io/ioutil"
"log" log "github.com/sirupsen/logrus"
"os" "os"
"strconv" "strconv"
"time" "time"
"crypto/sha256"
lnrpc "github.com/lightningnetwork/lnd/lnrpc" lnrpc "github.com/lightningnetwork/lnd/lnrpc"
invoicesrpc "github.com/lightningnetwork/lnd/lnrpc/invoicesrpc"
routerrpc "github.com/lightningnetwork/lnd/lnrpc/routerrpc" routerrpc "github.com/lightningnetwork/lnd/lnrpc/routerrpc"
"google.golang.org/grpc" "google.golang.org/grpc"
@ -32,7 +34,7 @@ func newCreds(bytes []byte) rpcCreds {
return creds return creds
} }
func getRouterClient(hostname string, port int, tlsFile, macaroonFile string) routerrpc.RouterClient { func getGrpcConn(hostname string, port int, tlsFile, macaroonFile string) *grpc.ClientConn {
macaroonBytes, err := ioutil.ReadFile(macaroonFile) macaroonBytes, err := ioutil.ReadFile(macaroonFile)
if err != nil { if err != nil {
log.Println("Cannot read macaroon file .. ", err) log.Println("Cannot read macaroon file .. ", err)
@ -65,49 +67,72 @@ func getRouterClient(hostname string, port int, tlsFile, macaroonFile string) ro
panic(err) panic(err)
} }
return routerrpc.NewRouterClient(connection) return connection
} }
func pay_invoice(invoice string) (payment_status string, failure_reason string, return_err error) { // https://api.lightning.community/?shell#addinvoice
payment_status = "" func add_invoice(amount_sat int64, metadata string) (payment_request string, r_hash []byte, return_err error) {
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")) ln_port, err := strconv.Atoi(os.Getenv("LN_PORT"))
if err != nil { if err != nil {
return_err = err return "", nil, err
return
} }
r_client := getRouterClient( dh := sha256.Sum256([]byte(metadata))
connection := getGrpcConn(
os.Getenv("LN_HOST"), os.Getenv("LN_HOST"),
ln_port, ln_port,
os.Getenv("LN_TLS_FILE"), os.Getenv("LN_TLS_FILE"),
os.Getenv("LN_MACAROON_FILE")) os.Getenv("LN_MACAROON_FILE"))
fee_limit_sat_str := os.Getenv("FEE_LIMIT_SAT") l_client := lnrpc.NewLightningClient(connection)
fee_limit_sat, err := strconv.ParseInt(fee_limit_sat_str, 10, 64)
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
result, err := l_client.AddInvoice(ctx, &lnrpc.Invoice {
Value: amount_sat,
DescriptionHash: dh[:],
})
if err != nil { if err != nil {
return_err = err return "", nil, err
}
return result.PaymentRequest, result.RHash, nil
}
// https://api.lightning.community/?shell#subscribesingleinvoice
func monitor_invoice_state(r_hash []byte) () {
// SubscribeSingleInvoice
// get node parameters from environment variables
ln_port, err := strconv.Atoi(os.Getenv("LN_PORT"))
if err != nil {
log.Warn(err)
return return
} }
stream, err := r_client.SendPaymentV2(ctx2, &routerrpc.SendPaymentRequest{ connection := getGrpcConn(
PaymentRequest: invoice, os.Getenv("LN_HOST"),
NoInflightUpdates: true, ln_port,
TimeoutSeconds: 30, os.Getenv("LN_TLS_FILE"),
FeeLimitSat: fee_limit_sat}) os.Getenv("LN_MACAROON_FILE"))
i_client := invoicesrpc.NewInvoicesClient(connection)
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
stream, err := i_client.SubscribeSingleInvoice(ctx, &invoicesrpc.SubscribeSingleInvoiceRequest{
RHash: r_hash})
if err != nil { if err != nil {
return_err = err log.WithFields(log.Fields{"r_hash": hex.EncodeToString(r_hash)}).Warn(err)
return return
} }
@ -119,13 +144,141 @@ func pay_invoice(invoice string) (payment_status string, failure_reason string,
} }
if err != nil { if err != nil {
return_err = err log.WithFields(log.Fields{"r_hash": hex.EncodeToString(r_hash)}).Warn(err)
return return
} }
payment_status = lnrpc.Payment_PaymentStatus_name[int32(update.Status)] invoice_state := lnrpc.Invoice_InvoiceState_name[int32(update.State)]
failure_reason = lnrpc.PaymentFailureReason_name[int32(update.FailureReason)]
log.WithFields(
log.Fields{
"r_hash": hex.EncodeToString(r_hash),
"invoice_state": invoice_state,
},).Info("invoice state updated")
db_update_receipt_state(hex.EncodeToString(r_hash), invoice_state)
} }
connection.Close()
// send email
card_id, err := db_get_card_id_for_r_hash(hex.EncodeToString(r_hash))
if err != nil {
log.WithFields(log.Fields{"r_hash": hex.EncodeToString(r_hash)}).Warn(err)
return
}
log.WithFields(log.Fields{"r_hash": hex.EncodeToString(r_hash), "card_id": card_id}).Debug("card found")
c, err := db_get_card_from_card_id(card_id)
if err != nil {
log.WithFields(log.Fields{"r_hash": hex.EncodeToString(r_hash)}).Warn(err)
return
}
if c.email_enable != "Y" {
log.Debug("email is not enabled for the card")
return
}
go send_email(c.email_address, "bolt card receipt", "html body", "text body")
return
}
// https://api.lightning.community/?shell#sendpaymentv2
func pay_invoice(card_payment_id int, invoice string) {
// SendPaymentV2
// get node parameters from environment variables
ln_port, err := strconv.Atoi(os.Getenv("LN_PORT"))
if err != nil {
log.WithFields(log.Fields{"card_payment_id": card_payment_id}).Warn(err)
return
}
connection := getGrpcConn(
os.Getenv("LN_HOST"),
ln_port,
os.Getenv("LN_TLS_FILE"),
os.Getenv("LN_MACAROON_FILE"))
r_client := routerrpc.NewRouterClient(connection)
fee_limit_sat_str := os.Getenv("FEE_LIMIT_SAT")
fee_limit_sat, err := strconv.ParseInt(fee_limit_sat_str, 10, 64)
if err != nil {
log.WithFields(log.Fields{"card_payment_id": card_payment_id}).Warn(err)
return
}
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
stream, err := r_client.SendPaymentV2(ctx, &routerrpc.SendPaymentRequest{
PaymentRequest: invoice,
NoInflightUpdates: true,
TimeoutSeconds: 30,
FeeLimitSat: fee_limit_sat})
if err != nil {
log.WithFields(log.Fields{"card_payment_id": card_payment_id}).Warn(err)
return
}
for {
update, err := stream.Recv()
if err == io.EOF {
break
}
if err != nil {
log.WithFields(log.Fields{"card_payment_id": card_payment_id}).Warn(err)
return
}
payment_status := lnrpc.Payment_PaymentStatus_name[int32(update.Status)]
failure_reason := lnrpc.PaymentFailureReason_name[int32(update.FailureReason)]
log.WithFields(log.Fields{"card_payment_id": card_payment_id}).Info("payment failure reason : ", failure_reason)
log.WithFields(log.Fields{"card_payment_id": card_payment_id}).Info("payment status : ", payment_status)
err = db_update_payment_status(card_payment_id, payment_status, failure_reason)
if err != nil {
log.WithFields(log.Fields{"card_payment_id": card_payment_id}).Warn(err)
return
}
}
connection.Close()
// send email
card_id, err := db_get_card_id_for_card_payment_id(card_payment_id)
if err != nil {
log.WithFields(log.Fields{"card_payment_id": card_payment_id}).Warn(err)
return
}
log.WithFields(log.Fields{"card_payment_id": card_payment_id, "card_id": card_id}).Debug("card found")
c, err := db_get_card_from_card_id(card_id)
if err != nil {
log.WithFields(log.Fields{"card_payment_id": card_payment_id}).Warn(err)
return
}
if c.email_enable != "Y" {
log.Debug("email is not enabled for the card")
return
}
go send_email(c.email_address, "bolt card payment", "html body", "text body")
return return
} }

81
lnurlp_callback.go Normal file
View file

@ -0,0 +1,81 @@
package main
import (
"os"
log "github.com/sirupsen/logrus"
"github.com/gorilla/mux"
"net/http"
"strconv"
"encoding/hex"
)
func lnurlp_callback(w http.ResponseWriter, r *http.Request) {
if os.Getenv("FUNCTION_LNURLP") != "ENABLE" {
log.Debug("LNURLp function is not enabled")
return
}
name := mux.Vars(r)["name"]
amount := r.URL.Query().Get("amount")
card_id, err := db_get_card_id_for_name(name)
if err != nil {
log.Info("card name not found")
write_error(w)
return
}
log.WithFields(
log.Fields{
"url_path": r.URL.Path,
"name": name,
"card_id": card_id,
"amount": amount,
"req.Host": r.Host,
},).Info("lnurlp_callback")
domain := os.Getenv("HOST_DOMAIN")
if r.Host != domain {
log.Warn("wrong host domain")
write_error(w)
return
}
amount_msat, err := strconv.ParseInt(amount, 10, 64)
if err != nil {
log.Warn("amount is not a valid integer")
write_error(w)
return
}
amount_sat := amount_msat / 1000;
metadata := "[[\"text/identifier\",\"" + name + "@" + domain + "\"],[\"text/plain\",\"bolt card deposit\"]]"
pr, r_hash, err := add_invoice(amount_sat, metadata)
if err != nil {
log.Warn("could not add_invoice")
write_error(w)
return
}
err = db_insert_receipt(card_id, pr, hex.EncodeToString(r_hash), amount_msat)
if err != nil {
log.Warn(err)
write_error(w)
return
}
go monitor_invoice_state(r_hash)
log.Debug("sending 'status OK' response");
jsonData := []byte(`{` +
`"status":"OK",` +
`"routes":[],` +
`"pr":"` + pr + `"` +
`}`)
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
w.Write(jsonData)
}

63
lnurlp_request.go Normal file
View file

@ -0,0 +1,63 @@
package main
import (
"os"
log "github.com/sirupsen/logrus"
"github.com/gorilla/mux"
"net/http"
)
func lnurlp_response(w http.ResponseWriter, r *http.Request) {
if os.Getenv("FUNCTION_LNURLP") != "ENABLE" {
log.Debug("LNURLp function is not enabled")
return
}
name := mux.Vars(r)["name"]
log.WithFields(
log.Fields{
"url_path": r.URL.Path,
"name": name,
"r.Host": r.Host,
},).Info("lnurlp_response")
// look up domain in env vars (HOST_DOMAIN)
domain := os.Getenv("HOST_DOMAIN")
if r.Host != domain {
log.Warn("wrong host domain")
write_error(w)
return
}
// look up name in database (table cards, field card_name)
card_count, err := db_get_card_count_for_name_lnurlp(name)
if err != nil {
log.Warn("could not get card count for name")
write_error(w)
return
}
if card_count != 1 {
log.Info("not one enabled card with that name")
write_error(w)
return
}
metadata := "[[\\\"text/identifier\\\",\\\"" + name + "@" + domain + "\\\"],[\\\"text/plain\\\",\\\"bolt card deposit\\\"]]"
jsonData := []byte(`{"status":"OK",` +
`"callback":"https://` + domain + `/lnurlp/` + name + `",` +
`"tag":"payRequest",` +
`"maxSendable":1000000000,` +
`"minSendable":1000,` +
`"metadata":"` + metadata + `",` +
`"commentAllowed":0` +
`}`)
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
w.Write(jsonData)
}

View file

@ -9,6 +9,13 @@ import (
func lnurlw_callback(w http.ResponseWriter, req *http.Request) { func lnurlw_callback(w http.ResponseWriter, req *http.Request) {
env_host_domain := os.Getenv("HOST_DOMAIN")
if req.Host != env_host_domain {
log.Warn("wrong host domain")
write_error(w)
return
}
url := req.URL.RequestURI() url := req.URL.RequestURI()
log.WithFields(log.Fields{"url": url}).Debug("cb request") log.WithFields(log.Fields{"url": url}).Debug("cb request")
@ -123,34 +130,16 @@ func lnurlw_callback(w http.ResponseWriter, req *http.Request) {
return return
} }
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)
write_error(w)
return
}
if failure_reason != "FAILURE_REASON_NONE" {
log.WithFields(log.Fields{"card_payment_id": p.card_payment_id}).Info("payment failure reason : ", failure_reason)
write_error(w)
}
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)
write_error(w)
return
}
// https://github.com/fiatjaf/lnurl-rfc/blob/luds/03.md // https://github.com/fiatjaf/lnurl-rfc/blob/luds/03.md
// //
// LN SERVICE sends a {"status": "OK"} or // LN SERVICE sends a {"status": "OK"} or
// {"status": "ERROR", "reason": "error details..."} // {"status": "ERROR", "reason": "error details..."}
// JSON response and then attempts to pay the invoices asynchronously. // JSON response and then attempts to pay the invoices asynchronously.
go pay_invoice(p.card_payment_id, param_pr)
log.Debug("sending 'status OK' response");
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK) w.WriteHeader(http.StatusOK)
jsonData := []byte(`{"status":"OK"}`) jsonData := []byte(`{"status":"OK"}`)

View file

@ -182,7 +182,7 @@ func parse_request(req *http.Request) (int, error) {
card_count, err := db_get_card_count_for_uid(uid_str) card_count, err := db_get_card_count_for_uid(uid_str)
if err != nil { if err != nil {
return 0, errors.New("could not get card records count") return 0, errors.New("could not get card count for uid")
} }
if card_count == 0 { if card_count == 0 {
@ -205,8 +205,8 @@ func parse_request(req *http.Request) (int, error) {
// check if card is enabled // check if card is enabled
if c.enable_flag != "Y" { if c.lnurlw_enable != "Y" {
return 0, errors.New("card enable is not set to Y") return 0, errors.New("card lnurlw enable is not set to Y")
} }
// check cmac // check cmac
@ -246,6 +246,13 @@ func parse_request(req *http.Request) (int, error) {
func lnurlw_response(w http.ResponseWriter, req *http.Request) { func lnurlw_response(w http.ResponseWriter, req *http.Request) {
env_host_domain := os.Getenv("HOST_DOMAIN")
if req.Host != env_host_domain {
log.Warn("wrong host domain")
write_error(w)
return
}
card_id, err := parse_request(req) card_id, err := parse_request(req)
if err != nil { if err != nil {

29
main.go
View file

@ -2,10 +2,14 @@ package main
import ( import (
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"github.com/gorilla/mux"
"net/http" "net/http"
"time"
"os" "os"
) )
var router = mux.NewRouter()
func write_error(w http.ResponseWriter) { func write_error(w http.ResponseWriter) {
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK) w.WriteHeader(http.StatusOK)
@ -34,17 +38,26 @@ func main() {
DisableHTMLEscape: true, DisableHTMLEscape: true,
}) })
mux := http.NewServeMux() // createboltcard
router.Path("/new").Methods("GET").HandlerFunc(new_card_request)
mux.HandleFunc("/new", new_card_request) // lnurlw for pos
mux.HandleFunc("/ln", lnurlw_response) router.Path("/ln").Methods("GET").HandlerFunc(lnurlw_response)
mux.HandleFunc("/cb", lnurlw_callback) router.Path("/cb").Methods("GET").HandlerFunc(lnurlw_callback)
// lnurlp for lightning address lnurlp
router.Path("/.well-known/lnurlp/{name}").Methods("GET").HandlerFunc(lnurlp_response)
router.Path("/lnurlp/{name}").Methods("GET").HandlerFunc(lnurlp_callback)
port := os.Getenv("HOST_PORT") port := os.Getenv("HOST_PORT")
if len(port) == 0 { if len(port) == 0 {
port = "9000" port = "9000"
} }
err := http.ListenAndServe(":" + port, mux) srv := &http.Server {
log.Fatal(err) Handler: router,
Addr: ":" + port, // consider adding host
WriteTimeout: 30 * time.Second,
ReadTimeout: 30 * time.Second,
}
srv.ListenAndServe()
} }

View file

@ -1,4 +1,5 @@
go build go build
sudo cp boltcard.service /etc/systemd/system/boltcard.service
sudo systemctl daemon-reload sudo systemctl daemon-reload
sudo systemctl stop boltcard sudo systemctl stop boltcard
sudo systemctl start boltcard sudo systemctl start boltcard