upleb.uk

Public git repos — served from a NIP-34 GRASP relay at git.upleb.uk

summaryrefslogtreecommitdiff
path: root/components/nucula_lib/nucula_wallet.cpp
diff options
context:
space:
mode:
authorYour Name <you@example.com>2026-05-16 23:55:05 +0530
committerYour Name <you@example.com>2026-05-16 23:55:05 +0530
commit4c47ae188b288e7d24bd9566ab3e6a6805d9484f (patch)
tree33b74b2090b4f3b7597841734a56a4006a86d73f /components/nucula_lib/nucula_wallet.cpp
parent133e40c82afb4d7659758b1fa57925ac57af4621 (diff)
Phase 3: Nostr identity derivation + wifistr service discovery
- 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
Diffstat (limited to 'components/nucula_lib/nucula_wallet.cpp')
-rw-r--r--components/nucula_lib/nucula_wallet.cpp199
1 files changed, 199 insertions, 0 deletions
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 @@
1#include "nucula_wallet.h"
2#include "wallet.hpp"
3#include "cashu_json.hpp"
4#include "crypto.h"
5#include "hex.h"
6#include "esp_log.h"
7#include "secp256k1.h"
8#include "cJSON.h"
9#include <cstring>
10#include <string>
11#include <vector>
12
13static const char *TAG = "nucula_wallet";
14
15static secp256k1_context *s_ctx = nullptr;
16static cashu::Wallet *s_wallet = nullptr;
17
18static std::vector<cashu::Proof> &mutable_proofs()
19{
20 return const_cast<std::vector<cashu::Proof> &>(s_wallet->proofs());
21}
22
23esp_err_t nucula_wallet_init(const char *mint_url)
24{
25 if (s_wallet) return ESP_OK;
26
27 s_ctx = secp256k1_context_create(SECP256K1_CONTEXT_SIGN | SECP256K1_CONTEXT_VERIFY);
28 if (!s_ctx) {
29 ESP_LOGE(TAG, "Failed to create secp256k1 context");
30 return ESP_FAIL;
31 }
32
33 s_wallet = new cashu::Wallet(std::string(mint_url), s_ctx, 0);
34 if (!s_wallet) {
35 ESP_LOGE(TAG, "Failed to create wallet");
36 secp256k1_context_destroy(s_ctx);
37 s_ctx = nullptr;
38 return ESP_FAIL;
39 }
40
41 s_wallet->load_from_nvs();
42
43 if (!s_wallet->load_keysets()) {
44 ESP_LOGW(TAG, "Keyset load failed (may be offline)");
45 }
46
47 ESP_LOGI(TAG, "Wallet initialized: balance=%d proofs=%d keysets=%d",
48 s_wallet->balance(), (int)s_wallet->proofs().size(),
49 (int)s_wallet->keysets().size());
50 return ESP_OK;
51}
52
53esp_err_t nucula_wallet_receive(const char *token_str)
54{
55 if (!s_wallet || !token_str) return ESP_FAIL;
56
57 cashu::Token tok;
58 bool decoded = false;
59
60 if (strncmp(token_str, "cashuA", 6) == 0) {
61 decoded = cashu::deserialize_token_v3(token_str, tok);
62 }
63
64 if (!decoded) {
65 ESP_LOGE(TAG, "Failed to decode token");
66 return ESP_FAIL;
67 }
68
69 std::vector<cashu::Proof> proofs_out;
70 if (!s_wallet->receive(tok, proofs_out)) {
71 ESP_LOGE(TAG, "Receive failed");
72 return ESP_FAIL;
73 }
74
75 int total = 0;
76 for (const auto &p : proofs_out) total += p.amount;
77 ESP_LOGI(TAG, "Received %d sat (%d proofs), new balance=%d",
78 total, (int)proofs_out.size(), s_wallet->balance());
79 return ESP_OK;
80}
81
82esp_err_t nucula_wallet_send(uint64_t amount_sat, char *token_out, size_t token_out_size)
83{
84 if (!s_wallet) return ESP_FAIL;
85
86 int amount = (int)amount_sat;
87 std::vector<cashu::Proof> selected, remaining;
88 if (!s_wallet->select_proofs(amount, selected, remaining)) {
89 ESP_LOGE(TAG, "Insufficient balance for %d sat", amount);
90 return ESP_FAIL;
91 }
92
93 std::vector<cashu::Proof> new_proofs, change;
94 if (!s_wallet->swap(selected, (int)amount_sat, new_proofs, change)) {
95 ESP_LOGE(TAG, "Swap for send failed");
96 return ESP_FAIL;
97 }
98
99 cashu::Token token;
100 token.mint = s_wallet->mint_url();
101 token.unit = "sat";
102 for (auto &p : new_proofs) token.proofs.push_back(p);
103
104 std::string encoded = cashu::serialize_token_v3(token);
105 if (encoded.empty()) {
106 ESP_LOGE(TAG, "Token serialization failed");
107 return ESP_FAIL;
108 }
109
110 if (encoded.size() >= token_out_size) {
111 ESP_LOGE(TAG, "Token too large: %zu >= %zu", encoded.size(), token_out_size);
112 return ESP_FAIL;
113 }
114
115 memcpy(token_out, encoded.c_str(), encoded.size() + 1);
116
117 auto &proofs = mutable_proofs();
118 proofs = remaining;
119 for (auto &p : change) proofs.push_back(p);
120 s_wallet->save_proofs();
121
122 ESP_LOGI(TAG, "Sent %llu sat, token=%zu bytes, remaining balance=%d",
123 (unsigned long long)amount_sat, encoded.size(), s_wallet->balance());
124 return ESP_OK;
125}
126
127uint64_t nucula_wallet_balance(void)
128{
129 if (!s_wallet) return 0;
130 return (uint64_t)s_wallet->balance();
131}
132
133int nucula_wallet_proof_count(void)
134{
135 if (!s_wallet) return 0;
136 return (int)s_wallet->proofs().size();
137}
138
139char *nucula_wallet_proofs_json(void)
140{
141 if (!s_wallet) return nullptr;
142
143 const auto &proofs = s_wallet->proofs();
144 cJSON *arr = cJSON_CreateArray();
145 for (const auto &p : proofs) {
146 cJSON *obj = cJSON_CreateObject();
147 cJSON_AddNumberToObject(obj, "amount", p.amount);
148 cJSON_AddStringToObject(obj, "id", p.id.c_str());
149 cJSON_AddItemToArray(arr, obj);
150 }
151 char *json = cJSON_PrintUnformatted(arr);
152 cJSON_Delete(arr);
153 return json;
154}
155
156esp_err_t nucula_wallet_swap_all(void)
157{
158 if (!s_wallet) return ESP_FAIL;
159
160 auto &proofs = mutable_proofs();
161 if (proofs.empty()) {
162 ESP_LOGW(TAG, "No proofs to swap");
163 return ESP_FAIL;
164 }
165
166 int old_balance = s_wallet->balance();
167
168 std::vector<cashu::Proof> inputs = proofs;
169 std::vector<cashu::Proof> new_proofs, change;
170 if (!s_wallet->swap(inputs, -1, new_proofs, change)) {
171 ESP_LOGE(TAG, "Swap failed");
172 return ESP_FAIL;
173 }
174
175 proofs.clear();
176 for (auto &p : new_proofs) proofs.push_back(p);
177 for (auto &p : change) proofs.push_back(p);
178 s_wallet->save_proofs();
179
180 ESP_LOGI(TAG, "Swap complete: %d -> %d sat (%d proofs)",
181 old_balance, s_wallet->balance(), (int)proofs.size());
182 return ESP_OK;
183}
184
185void nucula_wallet_print_status(void)
186{
187 if (!s_wallet) {
188 ESP_LOGI(TAG, "Wallet not initialized");
189 return;
190 }
191 ESP_LOGI(TAG, "Wallet: balance=%d proofs=%d keysets=%d",
192 s_wallet->balance(), (int)s_wallet->proofs().size(),
193 (int)s_wallet->keysets().size());
194 const auto &proofs = s_wallet->proofs();
195 for (size_t i = 0; i < proofs.size(); i++) {
196 ESP_LOGI(TAG, " [%d] amount=%d id=%s", (int)i,
197 proofs[i].amount, proofs[i].id.c_str());
198 }
199}