#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()); } }