From 4c47ae188b288e7d24bd9566ab3e6a6805d9484f Mon Sep 17 00:00:00 2001 From: Your Name Date: Sat, 16 May 2026 23:55:05 +0530 Subject: Phase 3: Nostr identity derivation + wifistr service discovery MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add identity.c/h: HMAC-SHA512 derivation from nsec → npub, STA/AP MAC, SSID, AP IP - Add nostr_event.c/h: NIP-01 event serialization + Schnorr signing (BIP-340) - Add geohash.c/h: lat/lon to geohash encoding - Add wifistr.c/h: kind 38787 event builder + WebSocket publish to Nostr relays - Update config.c/h: nsec-based identity, Nostr relay/geo config, remove static SSID/IP - Replace custom mbedTLS wallet with nucula library (libsecp256k1) - Remove wallet.c/h, wallet_persist.c/h (replaced by nucula_lib component) - Verified on Board A: derived SSID, captive portal, payment, wallet, wifistr publish --- components/nucula_lib/nucula_wallet.cpp | 199 ++++++++++++++++++++++++++++++++ 1 file changed, 199 insertions(+) create mode 100644 components/nucula_lib/nucula_wallet.cpp (limited to 'components/nucula_lib/nucula_wallet.cpp') diff --git a/components/nucula_lib/nucula_wallet.cpp b/components/nucula_lib/nucula_wallet.cpp new file mode 100644 index 0000000..50583f9 --- /dev/null +++ b/components/nucula_lib/nucula_wallet.cpp @@ -0,0 +1,199 @@ +#include "nucula_wallet.h" +#include "wallet.hpp" +#include "cashu_json.hpp" +#include "crypto.h" +#include "hex.h" +#include "esp_log.h" +#include "secp256k1.h" +#include "cJSON.h" +#include +#include +#include + +static const char *TAG = "nucula_wallet"; + +static secp256k1_context *s_ctx = nullptr; +static cashu::Wallet *s_wallet = nullptr; + +static std::vector &mutable_proofs() +{ + return const_cast &>(s_wallet->proofs()); +} + +esp_err_t nucula_wallet_init(const char *mint_url) +{ + if (s_wallet) return ESP_OK; + + s_ctx = secp256k1_context_create(SECP256K1_CONTEXT_SIGN | SECP256K1_CONTEXT_VERIFY); + if (!s_ctx) { + ESP_LOGE(TAG, "Failed to create secp256k1 context"); + return ESP_FAIL; + } + + s_wallet = new cashu::Wallet(std::string(mint_url), s_ctx, 0); + if (!s_wallet) { + ESP_LOGE(TAG, "Failed to create wallet"); + secp256k1_context_destroy(s_ctx); + s_ctx = nullptr; + return ESP_FAIL; + } + + s_wallet->load_from_nvs(); + + if (!s_wallet->load_keysets()) { + ESP_LOGW(TAG, "Keyset load failed (may be offline)"); + } + + ESP_LOGI(TAG, "Wallet initialized: balance=%d proofs=%d keysets=%d", + s_wallet->balance(), (int)s_wallet->proofs().size(), + (int)s_wallet->keysets().size()); + return ESP_OK; +} + +esp_err_t nucula_wallet_receive(const char *token_str) +{ + if (!s_wallet || !token_str) return ESP_FAIL; + + cashu::Token tok; + bool decoded = false; + + if (strncmp(token_str, "cashuA", 6) == 0) { + decoded = cashu::deserialize_token_v3(token_str, tok); + } + + if (!decoded) { + ESP_LOGE(TAG, "Failed to decode token"); + return ESP_FAIL; + } + + std::vector proofs_out; + if (!s_wallet->receive(tok, proofs_out)) { + ESP_LOGE(TAG, "Receive failed"); + return ESP_FAIL; + } + + int total = 0; + for (const auto &p : proofs_out) total += p.amount; + ESP_LOGI(TAG, "Received %d sat (%d proofs), new balance=%d", + total, (int)proofs_out.size(), s_wallet->balance()); + return ESP_OK; +} + +esp_err_t nucula_wallet_send(uint64_t amount_sat, char *token_out, size_t token_out_size) +{ + if (!s_wallet) return ESP_FAIL; + + int amount = (int)amount_sat; + std::vector selected, remaining; + if (!s_wallet->select_proofs(amount, selected, remaining)) { + ESP_LOGE(TAG, "Insufficient balance for %d sat", amount); + return ESP_FAIL; + } + + std::vector new_proofs, change; + if (!s_wallet->swap(selected, (int)amount_sat, new_proofs, change)) { + ESP_LOGE(TAG, "Swap for send failed"); + return ESP_FAIL; + } + + cashu::Token token; + token.mint = s_wallet->mint_url(); + token.unit = "sat"; + for (auto &p : new_proofs) token.proofs.push_back(p); + + std::string encoded = cashu::serialize_token_v3(token); + if (encoded.empty()) { + ESP_LOGE(TAG, "Token serialization failed"); + return ESP_FAIL; + } + + if (encoded.size() >= token_out_size) { + ESP_LOGE(TAG, "Token too large: %zu >= %zu", encoded.size(), token_out_size); + return ESP_FAIL; + } + + memcpy(token_out, encoded.c_str(), encoded.size() + 1); + + auto &proofs = mutable_proofs(); + proofs = remaining; + for (auto &p : change) proofs.push_back(p); + s_wallet->save_proofs(); + + ESP_LOGI(TAG, "Sent %llu sat, token=%zu bytes, remaining balance=%d", + (unsigned long long)amount_sat, encoded.size(), s_wallet->balance()); + return ESP_OK; +} + +uint64_t nucula_wallet_balance(void) +{ + if (!s_wallet) return 0; + return (uint64_t)s_wallet->balance(); +} + +int nucula_wallet_proof_count(void) +{ + if (!s_wallet) return 0; + return (int)s_wallet->proofs().size(); +} + +char *nucula_wallet_proofs_json(void) +{ + if (!s_wallet) return nullptr; + + const auto &proofs = s_wallet->proofs(); + cJSON *arr = cJSON_CreateArray(); + for (const auto &p : proofs) { + cJSON *obj = cJSON_CreateObject(); + cJSON_AddNumberToObject(obj, "amount", p.amount); + cJSON_AddStringToObject(obj, "id", p.id.c_str()); + cJSON_AddItemToArray(arr, obj); + } + char *json = cJSON_PrintUnformatted(arr); + cJSON_Delete(arr); + return json; +} + +esp_err_t nucula_wallet_swap_all(void) +{ + if (!s_wallet) return ESP_FAIL; + + auto &proofs = mutable_proofs(); + if (proofs.empty()) { + ESP_LOGW(TAG, "No proofs to swap"); + return ESP_FAIL; + } + + int old_balance = s_wallet->balance(); + + std::vector inputs = proofs; + std::vector new_proofs, change; + if (!s_wallet->swap(inputs, -1, new_proofs, change)) { + ESP_LOGE(TAG, "Swap failed"); + return ESP_FAIL; + } + + proofs.clear(); + for (auto &p : new_proofs) proofs.push_back(p); + for (auto &p : change) proofs.push_back(p); + s_wallet->save_proofs(); + + ESP_LOGI(TAG, "Swap complete: %d -> %d sat (%d proofs)", + old_balance, s_wallet->balance(), (int)proofs.size()); + return ESP_OK; +} + +void nucula_wallet_print_status(void) +{ + if (!s_wallet) { + ESP_LOGI(TAG, "Wallet not initialized"); + return; + } + ESP_LOGI(TAG, "Wallet: balance=%d proofs=%d keysets=%d", + s_wallet->balance(), (int)s_wallet->proofs().size(), + (int)s_wallet->keysets().size()); + const auto &proofs = s_wallet->proofs(); + for (size_t i = 0; i < proofs.size(); i++) { + ESP_LOGI(TAG, " [%d] amount=%d id=%s", (int)i, + proofs[i].amount, proofs[i].id.c_str()); + } +} -- cgit v1.2.3