From cb4bd7d7c10cadcb43f82c09b13ffed744e541f7 Mon Sep 17 00:00:00 2001 From: Your Name Date: Sun, 17 May 2026 04:37:15 +0530 Subject: Phase 5: Lightning auto-payout with LNURL-pay and NUT-05 melt - New lnurl_pay.c/h: LNURL-pay protocol (GET .well-known/lnurlp + callback) - New lightning_payout.c/h: threshold-based auto-payout with multi-recipient split - Extended nucula_wallet bridge with nucula_wallet_melt() (NUT-05) - Config: payout section with multi-mint, multi-recipient, fee_tolerance - Default: enabled, TollGate@coinos.io, min_payout=128, min_balance=64 - 18 new unit tests (all passing), 134 total --- main/lightning_payout.c | 93 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 93 insertions(+) create mode 100644 main/lightning_payout.c (limited to 'main/lightning_payout.c') diff --git a/main/lightning_payout.c b/main/lightning_payout.c new file mode 100644 index 0000000..42593a9 --- /dev/null +++ b/main/lightning_payout.c @@ -0,0 +1,93 @@ +#include "lightning_payout.h" +#include "lnurl_pay.h" +#include "nucula_wallet.h" +#include "esp_log.h" +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include +#include + +static const char *TAG = "ln_payout"; + +static payout_config_t s_config; +static int64_t s_last_check_ms = 0; + +static int64_t get_time_ms(void) { + return (int64_t)(xTaskGetTickCount() * portTICK_PERIOD_MS); +} + +esp_err_t lightning_payout_init(const payout_config_t *config) +{ + if (!config) return ESP_FAIL; + memcpy(&s_config, config, sizeof(s_config)); + s_last_check_ms = get_time_ms(); + ESP_LOGI(TAG, "Payout initialized: %d mints, %d recipients, interval=%ds", + s_config.mint_count, s_config.recipient_count, s_config.check_interval_s); + return ESP_OK; +} + +static esp_err_t payout_one(const char *lightning_address, uint64_t amount_sats, uint64_t fee_tolerance_pct) +{ + if (amount_sats == 0) return ESP_OK; + + char bolt11[LNURL_MAX_BOLT11_LEN]; + esp_err_t err = lnurl_get_invoice(lightning_address, amount_sats, bolt11, sizeof(bolt11)); + if (err != ESP_OK) { + ESP_LOGE(TAG, "Failed to get invoice for %s (%llu sats)", lightning_address, (unsigned long long)amount_sats); + return err; + } + + uint64_t max_cost = amount_sats + (amount_sats * fee_tolerance_pct / 100); + err = nucula_wallet_melt(bolt11, max_cost); + if (err != ESP_OK) { + ESP_LOGE(TAG, "Melt failed for %s", lightning_address); + return err; + } + + ESP_LOGI(TAG, "Payout: %llu sats -> %s", (unsigned long long)amount_sats, lightning_address); + return ESP_OK; +} + +void lightning_payout_tick(void) +{ + if (!s_config.enabled) return; + if (s_config.recipient_count == 0) return; + + int64_t now = get_time_ms(); + int64_t elapsed = now - s_last_check_ms; + int64_t interval_ms = (int64_t)s_config.check_interval_s * 1000; + if (elapsed < interval_ms) return; + + s_last_check_ms = now; + + uint64_t balance = nucula_wallet_balance(); + + for (int m = 0; m < s_config.mint_count; m++) { + const payout_mint_config_t *mc = &s_config.mints[m]; + + if (balance < mc->min_payout_amount) { + ESP_LOGI(TAG, "Balance %llu < min_payout %llu for %s, skipping", + (unsigned long long)balance, (unsigned long long)mc->min_payout_amount, mc->url); + continue; + } + + uint64_t payout_pool = balance - mc->min_balance; + if (payout_pool == 0) continue; + + ESP_LOGI(TAG, "Payout pool: %llu sats (balance=%llu - reserve=%llu)", + (unsigned long long)payout_pool, (unsigned long long)balance, + (unsigned long long)mc->min_balance); + + for (int r = 0; r < s_config.recipient_count; r++) { + const payout_recipient_t *recip = &s_config.recipients[r]; + if (recip->factor <= 0.0 || recip->lightning_address[0] == '\0') continue; + + uint64_t share = (uint64_t)round((double)payout_pool * recip->factor); + if (share == 0) continue; + + payout_one(recip->lightning_address, share, s_config.fee_tolerance_pct); + } + + balance = nucula_wallet_balance(); + } +} -- cgit v1.2.3