From ab1b7c464f61d549b5d074ce0f25ee86a69102a0 Mon Sep 17 00:00:00 2001 From: Peter Rounce Date: Sun, 25 Sep 2022 06:47:26 +0000 Subject: [PATCH 1/3] add balance to update email --- database.go | 64 ++++++++++++++++++++++++++++++++++++++++++---------- email.go | 14 ++++++++++++ lightning.go | 4 ++-- 3 files changed, 68 insertions(+), 14 deletions(-) diff --git a/database.go b/database.go index 4be4cd5..25abe4e 100644 --- a/database.go +++ b/database.go @@ -8,7 +8,7 @@ import ( "os" ) -type Card struct { +type card struct { card_id int card_guid string k0_auth_key string @@ -36,6 +36,14 @@ type payment struct { paid_flag string } +type transaction struct { + card_id int + tx_id int + tx_type string + tx_amount_msats int + tx_time string +} + func db_open() (*sql.DB, error) { // get connection string from environment variables @@ -55,9 +63,9 @@ func db_open() (*sql.DB, error) { return db, nil } -func db_get_new_card(one_time_code string) (*Card, error) { +func db_get_new_card(one_time_code string) (*card, error) { - c := Card{} + c :=card{} db, err := db_open() if err != nil { @@ -191,7 +199,7 @@ func db_get_card_id_for_r_hash(r_hash string) (int, error) { return card_id, nil } -func db_get_cards_blank_uid() ([]Card, error) { +func db_get_cards_blank_uid() ([]card, error) { // open the database @@ -217,17 +225,17 @@ func db_get_cards_blank_uid() ([]Card, error) { // prepare the results - var cards []Card + var cards []card // Loop through rows, using Scan to assign column data to struct fields. for rows.Next() { - var card Card - err := rows.Scan(&card.card_id, &card.k2_cmac_key) + var c card + err := rows.Scan(&c.card_id, &c.k2_cmac_key) if err != nil { return cards, err } - cards = append(cards, card) + cards = append(cards, c) } err = rows.Err() @@ -262,9 +270,9 @@ func db_update_card_uid_ctr(card_id int, uid string, ctr uint32) error { return nil } -func db_get_card_from_uid(card_uid string) (*Card, error) { +func db_get_card_from_uid(card_uid string) (*card, error) { - c := Card{} + c := card{} db, err := db_open() if err != nil { @@ -293,9 +301,9 @@ func db_get_card_from_uid(card_uid string) (*Card, error) { return &c, nil } -func db_get_card_from_card_id(card_id int) (*Card, error) { +func db_get_card_from_card_id(card_id int) (*card, error) { - c := Card{} + c := card{} db, err := db_open() if err != nil { @@ -584,3 +592,35 @@ func db_get_card_totals(card_id int) (int, error) { return day_total_sats, nil } + +//TODO: +//func db_get_card_txs(card_id int) ([]transaction, error) { + +func db_get_card_total(card_id int) (int, error) { + + db, err := db_open() + if err != nil { + return 0, err + } + + card_total_msats := 0 + + sqlStatement := `SELECT SUM(tx_amount_msats) FROM (SELECT card_id, ` + + `card_payments.card_payment_id AS tx_id, 'payment' AS tx_type, ` + + `-amount_msats as tx_amount_msats, payment_status_time AS tx_time ` + + `FROM card_payments WHERE card_id = $1 AND payment_status != 'FAILED' ` + + `AND amount_msats != 0 UNION SELECT card_id, card_receipts.card_receipt_id AS tx_id, ` + + `'receipt' AS tx_type, amount_msats as tx_amount_msats, ` + + `receipt_status_time AS tx_time FROM card_receipts WHERE card_id = $1 ` + + `AND receipt_status = 'SETTLED' ORDER BY tx_time) AS transactions;` + + row := db.QueryRow(sqlStatement, card_id) + err = row.Scan(&card_total_msats) + if err != nil { + return 0, err + } + + card_total_sats := card_total_msats / 1000 + + return card_total_sats, nil +} diff --git a/email.go b/email.go index ba2d6cc..2d141e6 100644 --- a/email.go +++ b/email.go @@ -8,8 +8,22 @@ import ( "github.com/aws/aws-sdk-go/service/ses" log "github.com/sirupsen/logrus" "os" + "strconv" ) +func send_balance_email(recipient_email string, card_id int) { + card_total_sats, err := db_get_card_total(card_id) + if err != nil { + log.Warn(err.Error()) + return + } + + send_email(recipient_email, + "bolt card balance: " + strconv.Itoa(card_total_sats) + " sats", + "html body", + "text body") +} + // 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) { diff --git a/lightning.go b/lightning.go index 28460ae..f4274f4 100644 --- a/lightning.go +++ b/lightning.go @@ -182,7 +182,7 @@ func monitor_invoice_state(r_hash []byte) () { return } - go send_email(c.email_address, "bolt card receipt", "html body", "text body") + go send_balance_email(c.email_address, card_id) return } @@ -278,7 +278,7 @@ func pay_invoice(card_payment_id int, invoice string) { return } - go send_email(c.email_address, "bolt card payment", "html body", "text body") + go send_balance_email(c.email_address, card_id) return } From 94267c47cfafb29d1f35271683f14a1eedf249dd Mon Sep 17 00:00:00 2001 From: Peter Rounce Date: Sun, 25 Sep 2022 10:33:24 +0000 Subject: [PATCH 2/3] add transactions to email body --- database.go | 70 +++++++++++++++++++++++++++++++++++++++++++++++------ email.go | 52 ++++++++++++++++++++++++++++++++++++--- 2 files changed, 112 insertions(+), 10 deletions(-) diff --git a/database.go b/database.go index 25abe4e..42453f0 100644 --- a/database.go +++ b/database.go @@ -311,10 +311,11 @@ func db_get_card_from_card_id(card_id int) (*card, error) { } defer db.Close() - sqlStatement := `SELECT card_id, k2_cmac_key, uid,` + - ` last_counter_value, lnurlw_request_timeout_sec,` + - ` lnurlw_enable, tx_limit_sats, day_limit_sats, email_enable, email_address` + - ` FROM cards WHERE card_id=$1;` + sqlStatement := `SELECT card_id, k2_cmac_key, uid, ` + + `last_counter_value, lnurlw_request_timeout_sec, ` + + `lnurlw_enable, tx_limit_sats, day_limit_sats, ` + + `email_enable, email_address, card_name ` + + `FROM cards WHERE card_id=$1;` row := db.QueryRow(sqlStatement, card_id) err = row.Scan( &c.card_id, @@ -326,7 +327,8 @@ func db_get_card_from_card_id(card_id int) (*card, error) { &c.tx_limit_sats, &c.day_limit_sats, &c.email_enable, - &c.email_address) + &c.email_address, + &c.card_name) if err != nil { return &c, err } @@ -593,8 +595,62 @@ func db_get_card_totals(card_id int) (int, error) { return day_total_sats, nil } -//TODO: -//func db_get_card_txs(card_id int) ([]transaction, error) { +func db_get_card_txs(card_id int) ([]transaction, error) { + // open the database + + db, err := db_open() + + if err != nil { + return nil, err + } + + defer db.Close() + + // query the database + +//TODO: LIMIT 10 + COUNT + sqlStatement := `SELECT card_id, ` + + `card_payments.card_payment_id AS tx_id, 'payment' AS tx_type, ` + + `amount_msats as tx_amount_msats, ` + + `TO_CHAR(payment_status_time, 'DD/MM/YYYY HH:MI:SS') AS tx_time ` + + `FROM card_payments WHERE card_id = $1 AND payment_status != 'FAILED' ` + + `AND amount_msats != 0 UNION SELECT card_id, card_receipts.card_receipt_id AS tx_id, ` + + `'receipt' AS tx_type, amount_msats as tx_amount_msats, ` + + `TO_CHAR(receipt_status_time, 'DD/MM/YYYY HH:MI:SS') AS tx_time ` + + `FROM card_receipts WHERE card_id = $1 ` + + `AND receipt_status = 'SETTLED' ORDER BY tx_time DESC` + + rows, err := db.Query(sqlStatement, card_id) + + if err != nil { + return nil, err + } + + defer rows.Close() + + // prepare the results + + var transactions []transaction + + // Loop through rows, using Scan to assign column data to struct fields. + for rows.Next() { + var t transaction + err := rows.Scan(&t.card_id, &t.tx_id, &t.tx_type, &t.tx_amount_msats, &t.tx_time) + + if err != nil { + return transactions, err + } + transactions = append(transactions, t) + } + + err = rows.Err() + + if err != nil { + return transactions, err + } + + return transactions, nil +} func db_get_card_total(card_id int) (int, error) { diff --git a/email.go b/email.go index 2d141e6..203ecc2 100644 --- a/email.go +++ b/email.go @@ -9,19 +9,65 @@ import ( log "github.com/sirupsen/logrus" "os" "strconv" + "strings" ) func send_balance_email(recipient_email string, card_id int) { + + c, err := db_get_card_from_card_id(card_id) + if err != nil { + log.Warn(err.Error()) + return + } + card_total_sats, err := db_get_card_total(card_id) if err != nil { log.Warn(err.Error()) return } + txs, err := db_get_card_txs(card_id) + if err != nil { + log.Warn(err.Error()) + return + } + + subject := c.card_name + " balance = " + strconv.Itoa(card_total_sats) + " sats" + + // add transactions to the email body + + var html_body_sb strings.Builder + var text_body_sb strings.Builder + + html_body_sb.WriteString("") + + html_body_sb.WriteString("

transactions

") + text_body_sb.WriteString("transactions\n\n") + + for _, tx := range txs { + + html_body_sb.WriteString( + "" + + "" + + "" + + "" + + "") + + text_body_sb.WriteString(tx.tx_type + + " " + strconv.Itoa(tx.tx_amount_msats / 1000)) + } + + html_body_sb.WriteString("
dateactionamount
" + tx.tx_time + "" + tx.tx_type + "" + strconv.Itoa(tx.tx_amount_msats / 1000) + "
") + + html_body := html_body_sb.String() + text_body := text_body_sb.String() + send_email(recipient_email, - "bolt card balance: " + strconv.Itoa(card_total_sats) + " sats", - "html body", - "text body") + subject, + html_body, + text_body) } // https://docs.aws.amazon.com/sdk-for-go/v1/developer-guide/ses-example-send-email.html From 9c5149e373afb91af105c6ead9da7dfb1235c50e Mon Sep 17 00:00:00 2001 From: Peter Rounce Date: Sun, 25 Sep 2022 19:01:52 +0000 Subject: [PATCH 3/3] add max txs setting for email, add setting to stop negative card balance --- create_db.sql | 1 + database.go | 87 ++++++++++++++++++++++++---------------------- email.go | 41 +++++++++++++++------- lnurlw_callback.go | 32 +++++++++++++---- 4 files changed, 99 insertions(+), 62 deletions(-) diff --git a/create_db.sql b/create_db.sql index 83abc02..45bafe3 100644 --- a/create_db.sql +++ b/create_db.sql @@ -25,6 +25,7 @@ CREATE TABLE cards ( one_time_code CHAR(32) NOT NULL DEFAULT '', one_time_code_expiry TIMESTAMPTZ DEFAULT NOW() + INTERVAL '1 DAY', one_time_code_used CHAR(1) NOT NULL DEFAULT 'Y', + allow_negative_balance CHAR(1) NOT NULL DEFAULT 'N', PRIMARY KEY(card_id) ); diff --git a/database.go b/database.go index 42453f0..297a44d 100644 --- a/database.go +++ b/database.go @@ -22,11 +22,12 @@ type card struct { lnurlw_enable string tx_limit_sats int day_limit_sats int - lnurlp_enable string - email_address string - email_enable string + lnurlp_enable string + email_address string + email_enable string one_time_code string card_name string + allow_negative_balance string } type payment struct { @@ -37,11 +38,11 @@ type payment struct { } type transaction struct { - card_id int - tx_id int - tx_type string + card_id int + tx_id int + tx_type string tx_amount_msats int - tx_time string + tx_time string } func db_open() (*sql.DB, error) { @@ -65,7 +66,7 @@ func db_open() (*sql.DB, error) { func db_get_new_card(one_time_code string) (*card, error) { - c :=card{} + c := card{} db, err := db_open() if err != nil { @@ -314,8 +315,8 @@ func db_get_card_from_card_id(card_id int) (*card, error) { sqlStatement := `SELECT card_id, k2_cmac_key, uid, ` + `last_counter_value, lnurlw_request_timeout_sec, ` + `lnurlw_enable, tx_limit_sats, day_limit_sats, ` + - `email_enable, email_address, card_name ` + - `FROM cards WHERE card_id=$1;` + `email_enable, email_address, card_name, ` + + `allow_negative_balance FROM cards WHERE card_id=$1;` row := db.QueryRow(sqlStatement, card_id) err = row.Scan( &c.card_id, @@ -328,7 +329,8 @@ func db_get_card_from_card_id(card_id int) (*card, error) { &c.day_limit_sats, &c.email_enable, &c.email_address, - &c.card_name) + &c.card_name, + &c.allow_negative_balance) if err != nil { return &c, err } @@ -595,64 +597,64 @@ func db_get_card_totals(card_id int) (int, error) { return day_total_sats, nil } -func db_get_card_txs(card_id int) ([]transaction, error) { +func db_get_card_txs(card_id int, max_txs int) ([]transaction, error) { // open the database - db, err := db_open() + db, err := db_open() - if err != nil { - return nil, err - } + if err != nil { + return nil, err + } - defer db.Close() + defer db.Close() - // query the database + // query the database -//TODO: LIMIT 10 + COUNT - sqlStatement := `SELECT card_id, ` + + sqlStatement := `SELECT card_id, ` + `card_payments.card_payment_id AS tx_id, 'payment' AS tx_type, ` + `amount_msats as tx_amount_msats, ` + `TO_CHAR(payment_status_time, 'DD/MM/YYYY HH:MI:SS') AS tx_time ` + `FROM card_payments WHERE card_id = $1 AND payment_status != 'FAILED' ` + + `AND payment_status != '' ` + `AND amount_msats != 0 UNION SELECT card_id, card_receipts.card_receipt_id AS tx_id, ` + `'receipt' AS tx_type, amount_msats as tx_amount_msats, ` + `TO_CHAR(receipt_status_time, 'DD/MM/YYYY HH:MI:SS') AS tx_time ` + `FROM card_receipts WHERE card_id = $1 ` + - `AND receipt_status = 'SETTLED' ORDER BY tx_time DESC` + `AND receipt_status = 'SETTLED' ORDER BY tx_time DESC LIMIT $2` - rows, err := db.Query(sqlStatement, card_id) + rows, err := db.Query(sqlStatement, card_id, max_txs) - if err != nil { - return nil, err - } + if err != nil { + return nil, err + } - defer rows.Close() + defer rows.Close() // prepare the results - var transactions []transaction + var transactions []transaction - // Loop through rows, using Scan to assign column data to struct fields. - for rows.Next() { - var t transaction - err := rows.Scan(&t.card_id, &t.tx_id, &t.tx_type, &t.tx_amount_msats, &t.tx_time) + // Loop through rows, using Scan to assign column data to struct fields. + for rows.Next() { + var t transaction + err := rows.Scan(&t.card_id, &t.tx_id, &t.tx_type, &t.tx_amount_msats, &t.tx_time) - if err != nil { - return transactions, err - } - transactions = append(transactions, t) - } + if err != nil { + return transactions, err + } + transactions = append(transactions, t) + } - err = rows.Err() + err = rows.Err() - if err != nil { - return transactions, err - } + if err != nil { + return transactions, err + } - return transactions, nil + return transactions, nil } -func db_get_card_total(card_id int) (int, error) { +func db_get_card_total_sats(card_id int) (int, error) { db, err := db_open() if err != nil { @@ -665,6 +667,7 @@ func db_get_card_total(card_id int) (int, error) { `card_payments.card_payment_id AS tx_id, 'payment' AS tx_type, ` + `-amount_msats as tx_amount_msats, payment_status_time AS tx_time ` + `FROM card_payments WHERE card_id = $1 AND payment_status != 'FAILED' ` + + `AND payment_status != '' ` + `AND amount_msats != 0 UNION SELECT card_id, card_receipts.card_receipt_id AS tx_id, ` + `'receipt' AS tx_type, amount_msats as tx_amount_msats, ` + `receipt_status_time AS tx_time FROM card_receipts WHERE card_id = $1 ` + diff --git a/email.go b/email.go index 203ecc2..100643b 100644 --- a/email.go +++ b/email.go @@ -20,13 +20,19 @@ func send_balance_email(recipient_email string, card_id int) { return } - card_total_sats, err := db_get_card_total(card_id) + card_total_sats, err := db_get_card_total_sats(card_id) if err != nil { log.Warn(err.Error()) return } - txs, err := db_get_card_txs(card_id) + email_max_txs, err := strconv.Atoi(os.Getenv("EMAIL_MAX_TXS")) + if err != nil { + log.Warn(err.Error()) + return + } + + txs, err := db_get_card_txs(card_id, email_max_txs+1) if err != nil { log.Warn(err.Error()) return @@ -46,17 +52,26 @@ func send_balance_email(recipient_email string, card_id int) { html_body_sb.WriteString("

transactions

") text_body_sb.WriteString("transactions\n\n") - for _, tx := range txs { + for i, tx := range txs { - html_body_sb.WriteString( - "" + - "" + - "" + - "" + - "") + if i < email_max_txs { + html_body_sb.WriteString( + "" + + "" + + "" + + "" + + "") + } else { + html_body_sb.WriteString( + "" + + "" + + "" + + "" + + "") + } text_body_sb.WriteString(tx.tx_type + - " " + strconv.Itoa(tx.tx_amount_msats / 1000)) + " " + strconv.Itoa(tx.tx_amount_msats/1000)) } html_body_sb.WriteString("
dateactionamount
" + tx.tx_time + "" + tx.tx_type + "" + strconv.Itoa(tx.tx_amount_msats / 1000) + "
" + tx.tx_time + "" + tx.tx_type + "" + strconv.Itoa(tx.tx_amount_msats/1000) + "
... ... ...
") @@ -64,10 +79,10 @@ func send_balance_email(recipient_email string, card_id int) { html_body := html_body_sb.String() text_body := text_body_sb.String() - send_email(recipient_email, + send_email(recipient_email, subject, - html_body, - text_body) + html_body, + text_body) } // https://docs.aws.amazon.com/sdk-for-go/v1/developer-guide/ses-example-send-email.html diff --git a/lnurlw_callback.go b/lnurlw_callback.go index 5137969..cfd864b 100644 --- a/lnurlw_callback.go +++ b/lnurlw_callback.go @@ -9,12 +9,12 @@ import ( 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 - } + 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() log.WithFields(log.Fields{"url": url}).Debug("cb request") @@ -120,6 +120,24 @@ func lnurlw_callback(w http.ResponseWriter, req *http.Request) { return } + // check the card balance if marked as 'must stay above zero' (default) + // i.e. cards.allow_negative_balance == 'N' + + if c.allow_negative_balance != "Y" { + card_total, err := db_get_card_total_sats(p.card_id) + if err != nil { + log.WithFields(log.Fields{"card_payment_id": p.card_payment_id}).Warn(err) + write_error(w) + return + } + + if card_total-invoice_sats < 0 { + log.WithFields(log.Fields{"card_payment_id": p.card_payment_id}).Warn("not enough balance") + write_error(w) + return + } + } + log.WithFields(log.Fields{"card_payment_id": p.card_payment_id}).Info("paying invoice") // update paid_flag so we only attempt payment once @@ -138,7 +156,7 @@ func lnurlw_callback(w http.ResponseWriter, req *http.Request) { go pay_invoice(p.card_payment_id, param_pr) - log.Debug("sending 'status OK' response"); + log.Debug("sending 'status OK' response") w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK)