Files
didactyl/src/cashu_wallet.c
T

1541 lines
53 KiB
C

#define _POSIX_C_SOURCE 200809L
#include "cashu_wallet.h"
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
#include <time.h>
#include <stdbool.h>
#include "debug.h"
#include "nostr_handler.h"
#include "../../nostr_core_lib/nostr_core/nostr_core.h"
#define CASHU_WALLET_QUERY_TIMEOUT_MS 6000
#define CASHU_WALLET_MAX_SPLIT_PARTS 64
typedef struct {
char event_id[65];
nostr_nip60_token_data_t token;
} wallet_token_entry_t;
typedef struct {
didactyl_config_t* cfg;
int initialized;
int wallet_loaded;
pthread_mutex_t mutex;
nostr_nip60_wallet_data_t wallet;
wallet_token_entry_t* tokens;
int token_count;
} cashu_wallet_state_t;
static cashu_wallet_state_t g_wallet = {0};
static void wallet_clear_tokens_locked(void) {
if (!g_wallet.tokens) {
g_wallet.token_count = 0;
return;
}
for (int i = 0; i < g_wallet.token_count; i++) {
nostr_nip60_free_token_data(&g_wallet.tokens[i].token);
}
free(g_wallet.tokens);
g_wallet.tokens = NULL;
g_wallet.token_count = 0;
}
static void wallet_clear_all_locked(void) {
wallet_clear_tokens_locked();
nostr_nip60_free_wallet_data(&g_wallet.wallet);
memset(&g_wallet.wallet, 0, sizeof(g_wallet.wallet));
g_wallet.wallet_loaded = 0;
}
static int wallet_append_token_locked(const char* event_id, nostr_nip60_token_data_t* token) {
if (!token) return -1;
wallet_token_entry_t* grown =
(wallet_token_entry_t*)realloc(g_wallet.tokens, (size_t)(g_wallet.token_count + 1) * sizeof(wallet_token_entry_t));
if (!grown) {
return -1;
}
g_wallet.tokens = grown;
wallet_token_entry_t* dst = &g_wallet.tokens[g_wallet.token_count];
memset(dst, 0, sizeof(*dst));
if (event_id && event_id[0] != '\0') {
snprintf(dst->event_id, sizeof(dst->event_id), "%s", event_id);
}
dst->token = *token;
memset(token, 0, sizeof(*token));
g_wallet.token_count++;
return 0;
}
static int publish_from_signed_event_template(cJSON* ev_template, char out_event_id[65]) {
if (!ev_template) return -1;
cJSON* kind = cJSON_GetObjectItemCaseSensitive(ev_template, "kind");
cJSON* content = cJSON_GetObjectItemCaseSensitive(ev_template, "content");
cJSON* tags = cJSON_GetObjectItemCaseSensitive(ev_template, "tags");
if (!kind || !cJSON_IsNumber(kind) || !content || !cJSON_IsString(content)) {
return -1;
}
cJSON* tags_dup = tags ? cJSON_Duplicate(tags, 1) : cJSON_CreateArray();
if (!tags_dup) return -1;
nostr_publish_result_t result;
memset(&result, 0, sizeof(result));
int rc = nostr_handler_publish_kind_event((int)kind->valuedouble,
content->valuestring ? content->valuestring : "",
tags_dup,
&result);
cJSON_Delete(tags_dup);
if (rc != 0) {
nostr_handler_publish_result_free(&result);
return -1;
}
if (out_event_id) {
snprintf(out_event_id, 65, "%s", result.event_id);
}
nostr_handler_publish_result_free(&result);
return 0;
}
static int publish_wallet_event_and_store_locked(const nostr_nip60_wallet_data_t* wallet_data, char out_event_id[65]) {
cJSON* ev = nostr_nip60_create_wallet_event(wallet_data, g_wallet.cfg->keys.private_key, time(NULL));
if (!ev) return -1;
int rc = publish_from_signed_event_template(ev, out_event_id);
cJSON_Delete(ev);
if (rc != 0) return -1;
nostr_nip60_free_wallet_data(&g_wallet.wallet);
memset(&g_wallet.wallet, 0, sizeof(g_wallet.wallet));
g_wallet.wallet.privkey[0] = '\0';
snprintf(g_wallet.wallet.privkey, sizeof(g_wallet.wallet.privkey), "%s", wallet_data->privkey);
g_wallet.wallet.mint_count = wallet_data->mint_count;
if (wallet_data->mint_count > 0) {
g_wallet.wallet.mint_urls = (char**)calloc((size_t)wallet_data->mint_count, sizeof(char*));
if (!g_wallet.wallet.mint_urls) {
return -1;
}
for (int i = 0; i < wallet_data->mint_count; i++) {
g_wallet.wallet.mint_urls[i] = strdup(wallet_data->mint_urls[i] ? wallet_data->mint_urls[i] : "");
if (!g_wallet.wallet.mint_urls[i]) {
nostr_nip60_free_wallet_data(&g_wallet.wallet);
memset(&g_wallet.wallet, 0, sizeof(g_wallet.wallet));
return -1;
}
}
}
g_wallet.wallet_loaded = 1;
return 0;
}
static int publish_token_event_and_store_locked(nostr_nip60_token_data_t* token_data, char out_event_id[65]) {
if (!token_data) return -1;
cJSON* ev = nostr_nip60_create_token_event(token_data, g_wallet.cfg->keys.private_key, time(NULL));
if (!ev) return -1;
char event_id[65] = {0};
int rc = publish_from_signed_event_template(ev, event_id);
cJSON_Delete(ev);
if (rc != 0) {
return -1;
}
if (wallet_append_token_locked(event_id, token_data) != 0) {
return -1;
}
if (out_event_id) {
snprintf(out_event_id, 65, "%s", event_id);
}
return 0;
}
static int publish_history_event(nostr_nip60_direction_t direction,
uint64_t amount,
const char* primary_event_id,
nostr_nip60_ref_marker_t marker) {
nostr_nip60_history_ref_t ref;
memset(&ref, 0, sizeof(ref));
if (primary_event_id && primary_event_id[0] != '\0') {
snprintf(ref.event_id, sizeof(ref.event_id), "%s", primary_event_id);
}
ref.marker = marker;
nostr_nip60_history_data_t history;
memset(&history, 0, sizeof(history));
history.direction = direction;
history.amount = amount;
history.refs = &ref;
history.ref_count = (ref.event_id[0] != '\0') ? 1 : 0;
cJSON* ev = nostr_nip60_create_history_event(&history, g_wallet.cfg->keys.private_key, time(NULL));
if (!ev) return -1;
int rc = publish_from_signed_event_template(ev, NULL);
cJSON_Delete(ev);
return rc;
}
static int publish_token_deletion_event(const char* event_id) {
cJSON* del = nostr_nip60_create_token_deletion(event_id, g_wallet.cfg->keys.private_key, time(NULL));
if (!del) return -1;
int rc = publish_from_signed_event_template(del, NULL);
cJSON_Delete(del);
return rc;
}
static const char* resolve_mint_url_locked(const char* mint_url) {
if (mint_url && mint_url[0] != '\0') return mint_url;
if (g_wallet.wallet_loaded && g_wallet.wallet.mint_count > 0 && g_wallet.wallet.mint_urls && g_wallet.wallet.mint_urls[0]) {
return g_wallet.wallet.mint_urls[0];
}
if (g_wallet.cfg && g_wallet.cfg->cashu_wallet.mint_count > 0 && g_wallet.cfg->cashu_wallet.mint_urls && g_wallet.cfg->cashu_wallet.mint_urls[0]) {
return g_wallet.cfg->cashu_wallet.mint_urls[0];
}
return NULL;
}
static int token_proofs_total_for_mint_locked(const char* mint_url, uint64_t* out_total) {
if (!out_total) return -1;
uint64_t total = 0;
for (int i = 0; i < g_wallet.token_count; i++) {
const wallet_token_entry_t* entry = &g_wallet.tokens[i];
if (!entry->token.mint_url) continue;
if (mint_url && mint_url[0] != '\0' && strcmp(entry->token.mint_url, mint_url) != 0) continue;
total += nostr_nip60_sum_proofs(entry->token.proofs, entry->token.proof_count);
}
*out_total = total;
return 0;
}
static int dup_string_array(const char** src, int count, char*** out_arr, int* out_count) {
if (!out_arr || !out_count) return -1;
*out_arr = NULL;
*out_count = 0;
if (!src || count <= 0) return 0;
char** arr = (char**)calloc((size_t)count, sizeof(char*));
if (!arr) return -1;
int n = 0;
for (int i = 0; i < count; i++) {
if (!src[i] || src[i][0] == '\0') continue;
arr[n] = strdup(src[i]);
if (!arr[n]) {
for (int j = 0; j < n; j++) free(arr[j]);
free(arr);
return -1;
}
n++;
}
*out_arr = arr;
*out_count = n;
return 0;
}
static int build_nutzap_info_from_mints_locked(const char** mint_urls,
int mint_count,
nostr_nip61_nutzap_info_t* out_info) {
if (!g_wallet.cfg || !out_info) return -1;
if (!g_wallet.wallet_loaded || g_wallet.wallet.privkey[0] == '\0') return -1;
memset(out_info, 0, sizeof(*out_info));
if (g_wallet.cfg->relay_count > 0 && g_wallet.cfg->relays) {
out_info->relay_urls = (char**)calloc((size_t)g_wallet.cfg->relay_count, sizeof(char*));
if (!out_info->relay_urls) return -1;
for (int i = 0; i < g_wallet.cfg->relay_count; i++) {
out_info->relay_urls[i] = strdup(g_wallet.cfg->relays[i] ? g_wallet.cfg->relays[i] : "");
if (!out_info->relay_urls[i]) {
nostr_nip61_free_nutzap_info(out_info);
return -1;
}
out_info->relay_count++;
}
}
out_info->mints = (nostr_nip61_mint_entry_t*)calloc((size_t)mint_count, sizeof(nostr_nip61_mint_entry_t));
if (!out_info->mints) {
nostr_nip61_free_nutzap_info(out_info);
return -1;
}
for (int i = 0; i < mint_count; i++) {
out_info->mints[i].url = strdup(mint_urls[i]);
if (!out_info->mints[i].url) {
nostr_nip61_free_nutzap_info(out_info);
return -1;
}
out_info->mints[i].units = (char**)calloc(1, sizeof(char*));
if (!out_info->mints[i].units) {
nostr_nip61_free_nutzap_info(out_info);
return -1;
}
out_info->mints[i].units[0] = strdup(g_wallet.cfg->cashu_wallet.unit[0] ? g_wallet.cfg->cashu_wallet.unit : "sat");
if (!out_info->mints[i].units[0]) {
nostr_nip61_free_nutzap_info(out_info);
return -1;
}
out_info->mints[i].unit_count = 1;
out_info->mint_count++;
}
unsigned char wallet_priv[32] = {0};
unsigned char wallet_pub[32] = {0};
if (nostr_hex_to_bytes(g_wallet.wallet.privkey, wallet_priv, 32) != 0 ||
nostr_ec_public_key_from_private_key(wallet_priv, wallet_pub) != 0) {
nostr_nip61_free_nutzap_info(out_info);
return -1;
}
nostr_bytes_to_hex(wallet_pub, 32, out_info->pubkey);
return 0;
}
static int load_latest_public_nutzap_info(nostr_nip61_nutzap_info_t* out_info) {
if (!out_info || !g_wallet.cfg) return -1;
memset(out_info, 0, sizeof(*out_info));
cJSON* filter = cJSON_CreateObject();
cJSON* kinds = cJSON_CreateArray();
cJSON* authors = cJSON_CreateArray();
if (!filter || !kinds || !authors) {
cJSON_Delete(filter);
cJSON_Delete(kinds);
cJSON_Delete(authors);
return -1;
}
cJSON_AddItemToArray(kinds, cJSON_CreateNumber((double)NOSTR_NIP61_NUTZAP_INFO_KIND));
cJSON_AddItemToArray(authors, cJSON_CreateString(g_wallet.cfg->keys.public_key_hex));
cJSON_AddItemToObject(filter, "kinds", kinds);
cJSON_AddItemToObject(filter, "authors", authors);
cJSON_AddNumberToObject(filter, "limit", 20);
char* events_json = nostr_handler_query_json(filter, CASHU_WALLET_QUERY_TIMEOUT_MS);
cJSON_Delete(filter);
if (!events_json) return -1;
cJSON* arr = cJSON_Parse(events_json);
free(events_json);
if (!arr || !cJSON_IsArray(arr)) {
cJSON_Delete(arr);
return -1;
}
int found = 0;
double newest_created_at = -1;
nostr_nip61_nutzap_info_t newest;
memset(&newest, 0, sizeof(newest));
int n = cJSON_GetArraySize(arr);
for (int i = 0; i < n; i++) {
cJSON* ev = cJSON_GetArrayItem(arr, i);
cJSON* created_at = ev ? cJSON_GetObjectItemCaseSensitive(ev, "created_at") : NULL;
if (!created_at || !cJSON_IsNumber(created_at)) continue;
nostr_nip61_nutzap_info_t parsed;
memset(&parsed, 0, sizeof(parsed));
if (nostr_nip61_parse_nutzap_info_event(ev, &parsed) != 0) continue;
if (!found || created_at->valuedouble > newest_created_at) {
nostr_nip61_free_nutzap_info(&newest);
newest = parsed;
newest_created_at = created_at->valuedouble;
found = 1;
} else {
nostr_nip61_free_nutzap_info(&parsed);
}
}
cJSON_Delete(arr);
if (!found) return -1;
*out_info = newest;
return 0;
}
int cashu_wallet_init(didactyl_config_t* cfg) {
if (!cfg) return -1;
if (!g_wallet.initialized) {
if (pthread_mutex_init(&g_wallet.mutex, NULL) != 0) {
return -1;
}
g_wallet.initialized = 1;
}
pthread_mutex_lock(&g_wallet.mutex);
g_wallet.cfg = cfg;
wallet_clear_all_locked();
pthread_mutex_unlock(&g_wallet.mutex);
return 0;
}
void cashu_wallet_cleanup(void) {
if (!g_wallet.initialized) return;
pthread_mutex_lock(&g_wallet.mutex);
wallet_clear_all_locked();
g_wallet.cfg = NULL;
pthread_mutex_unlock(&g_wallet.mutex);
pthread_mutex_destroy(&g_wallet.mutex);
memset(&g_wallet, 0, sizeof(g_wallet));
}
static int ensure_wallet_ready(void) {
if (!g_wallet.initialized || !g_wallet.cfg) return -1;
int wallet_loaded = 0;
int auto_load = 0;
pthread_mutex_lock(&g_wallet.mutex);
wallet_loaded = g_wallet.wallet_loaded;
auto_load = g_wallet.cfg->cashu_wallet.auto_load ? 1 : 0;
pthread_mutex_unlock(&g_wallet.mutex);
if (wallet_loaded) return 0;
int rc = 0;
if (auto_load) {
rc = cashu_wallet_load_from_relays();
if (rc == 0) return 0;
}
rc = cashu_wallet_create_new_from_config();
if (rc == 0) return 0;
return rc;
}
int cashu_wallet_create_new_from_config(void) {
if (!g_wallet.initialized || !g_wallet.cfg) return -1;
pthread_mutex_lock(&g_wallet.mutex);
unsigned char priv[32] = {0};
unsigned char pub[32] = {0};
if (nostr_generate_keypair(priv, pub) != NOSTR_SUCCESS) {
pthread_mutex_unlock(&g_wallet.mutex);
return -1;
}
nostr_nip60_wallet_data_t wallet_data;
memset(&wallet_data, 0, sizeof(wallet_data));
nostr_bytes_to_hex(priv, 32, wallet_data.privkey);
wallet_data.mint_count = g_wallet.cfg->cashu_wallet.mint_count;
if (wallet_data.mint_count > 0) {
wallet_data.mint_urls = (char**)calloc((size_t)wallet_data.mint_count, sizeof(char*));
if (!wallet_data.mint_urls) {
pthread_mutex_unlock(&g_wallet.mutex);
return -1;
}
for (int i = 0; i < wallet_data.mint_count; i++) {
wallet_data.mint_urls[i] = strdup(g_wallet.cfg->cashu_wallet.mint_urls[i]);
if (!wallet_data.mint_urls[i]) {
nostr_nip60_free_wallet_data(&wallet_data);
pthread_mutex_unlock(&g_wallet.mutex);
return -1;
}
}
}
int rc = publish_wallet_event_and_store_locked(&wallet_data, NULL);
nostr_nip60_free_wallet_data(&wallet_data);
pthread_mutex_unlock(&g_wallet.mutex);
return rc;
}
int cashu_wallet_load_from_relays(void) {
if (!g_wallet.initialized || !g_wallet.cfg) return -1;
pthread_mutex_lock(&g_wallet.mutex);
cJSON* filter = nostr_nip60_create_wallet_filter(g_wallet.cfg->keys.public_key_hex);
if (!filter) {
pthread_mutex_unlock(&g_wallet.mutex);
return -1;
}
char* events_json = nostr_handler_query_json(filter, CASHU_WALLET_QUERY_TIMEOUT_MS);
cJSON_Delete(filter);
if (!events_json) {
pthread_mutex_unlock(&g_wallet.mutex);
return -1;
}
cJSON* arr = cJSON_Parse(events_json);
free(events_json);
if (!arr || !cJSON_IsArray(arr)) {
cJSON_Delete(arr);
pthread_mutex_unlock(&g_wallet.mutex);
return -1;
}
nostr_nip60_wallet_data_t newest_wallet;
memset(&newest_wallet, 0, sizeof(newest_wallet));
int have_wallet = 0;
double newest_created_at = -1;
int n = cJSON_GetArraySize(arr);
for (int i = 0; i < n; i++) {
cJSON* ev = cJSON_GetArrayItem(arr, i);
cJSON* kind = ev ? cJSON_GetObjectItemCaseSensitive(ev, "kind") : NULL;
cJSON* created_at = ev ? cJSON_GetObjectItemCaseSensitive(ev, "created_at") : NULL;
if (!kind || !cJSON_IsNumber(kind) || !created_at || !cJSON_IsNumber(created_at)) continue;
if ((int)kind->valuedouble != NOSTR_NIP60_WALLET_KIND) continue;
nostr_nip60_wallet_data_t parsed;
memset(&parsed, 0, sizeof(parsed));
if (nostr_nip60_parse_wallet_event(ev, g_wallet.cfg->keys.private_key, &parsed) != 0) {
continue;
}
if (!have_wallet || created_at->valuedouble > newest_created_at) {
nostr_nip60_free_wallet_data(&newest_wallet);
newest_wallet = parsed;
newest_created_at = created_at->valuedouble;
have_wallet = 1;
} else {
nostr_nip60_free_wallet_data(&parsed);
}
}
wallet_clear_all_locked();
if (!have_wallet) {
cJSON_Delete(arr);
pthread_mutex_unlock(&g_wallet.mutex);
return NOSTR_ERROR_NIP60_INVALID_WALLET;
}
g_wallet.wallet = newest_wallet;
g_wallet.wallet_loaded = 1;
for (int i = 0; i < n; i++) {
cJSON* ev = cJSON_GetArrayItem(arr, i);
cJSON* kind = ev ? cJSON_GetObjectItemCaseSensitive(ev, "kind") : NULL;
cJSON* id = ev ? cJSON_GetObjectItemCaseSensitive(ev, "id") : NULL;
if (!kind || !cJSON_IsNumber(kind) || !id || !cJSON_IsString(id) || !id->valuestring) continue;
if ((int)kind->valuedouble != NOSTR_NIP60_TOKEN_KIND) continue;
nostr_nip60_token_data_t token;
memset(&token, 0, sizeof(token));
if (nostr_nip60_parse_token_event(ev, g_wallet.cfg->keys.private_key, &token) != 0) {
continue;
}
if (wallet_append_token_locked(id->valuestring, &token) != 0) {
nostr_nip60_free_token_data(&token);
cJSON_Delete(arr);
pthread_mutex_unlock(&g_wallet.mutex);
return -1;
}
}
cJSON_Delete(arr);
pthread_mutex_unlock(&g_wallet.mutex);
return 0;
}
int cashu_wallet_balance(cJSON** out_json) {
if (!out_json || !g_wallet.initialized || !g_wallet.cfg) return -1;
(void)ensure_wallet_ready();
pthread_mutex_lock(&g_wallet.mutex);
cJSON* root = cJSON_CreateObject();
cJSON* by_mint = cJSON_CreateArray();
if (!root || !by_mint) {
cJSON_Delete(root);
cJSON_Delete(by_mint);
pthread_mutex_unlock(&g_wallet.mutex);
return -1;
}
cJSON_AddBoolToObject(root, "success", 1);
uint64_t total = 0;
for (int i = 0; i < g_wallet.token_count; i++) {
uint64_t amount = nostr_nip60_sum_proofs(g_wallet.tokens[i].token.proofs, g_wallet.tokens[i].token.proof_count);
total += amount;
cJSON* item = cJSON_CreateObject();
if (!item) continue;
cJSON_AddStringToObject(item, "mint", g_wallet.tokens[i].token.mint_url ? g_wallet.tokens[i].token.mint_url : "");
cJSON_AddNumberToObject(item, "amount", (double)amount);
cJSON_AddStringToObject(item, "token_event_id", g_wallet.tokens[i].event_id);
cJSON_AddItemToArray(by_mint, item);
}
cJSON_AddNumberToObject(root, "total", (double)total);
cJSON_AddItemToObject(root, "entries", by_mint);
cJSON_AddBoolToObject(root, "wallet_loaded", g_wallet.wallet_loaded ? 1 : 0);
*out_json = root;
pthread_mutex_unlock(&g_wallet.mutex);
return 0;
}
int cashu_wallet_info(const char* mint_url, cJSON** out_json) {
if (!out_json || !g_wallet.initialized || !g_wallet.cfg) return -1;
if (ensure_wallet_ready() != 0) return -1;
pthread_mutex_lock(&g_wallet.mutex);
const char* resolved = resolve_mint_url_locked(mint_url);
int timeout = g_wallet.cfg->cashu_wallet.mint_timeout_seconds > 0 ? g_wallet.cfg->cashu_wallet.mint_timeout_seconds : 30;
pthread_mutex_unlock(&g_wallet.mutex);
if (!resolved) return -1;
cashu_mint_info_t info;
memset(&info, 0, sizeof(info));
if (cashu_mint_get_info(resolved, &info, timeout) != 0) {
return -1;
}
cJSON* root = cJSON_CreateObject();
cJSON* keysets = cJSON_CreateArray();
if (!root || !keysets) {
cJSON_Delete(root);
cJSON_Delete(keysets);
cashu_mint_free_info(&info);
return -1;
}
cJSON_AddBoolToObject(root, "success", 1);
cJSON_AddStringToObject(root, "mint", resolved);
cJSON_AddStringToObject(root, "name", info.name ? info.name : "");
cJSON_AddStringToObject(root, "pubkey", info.pubkey ? info.pubkey : "");
cJSON_AddStringToObject(root, "version", info.version ? info.version : "");
for (int i = 0; i < info.keyset_count; i++) {
cJSON* ks = cJSON_CreateObject();
if (!ks) continue;
cJSON_AddStringToObject(ks, "id", info.keysets[i].id);
cJSON_AddStringToObject(ks, "unit", info.keysets[i].unit);
cJSON_AddBoolToObject(ks, "active", info.keysets[i].active ? 1 : 0);
cJSON_AddItemToArray(keysets, ks);
}
cJSON_AddItemToObject(root, "keysets", keysets);
*out_json = root;
cashu_mint_free_info(&info);
return 0;
}
int cashu_wallet_mint_quote(const char* mint_url, uint64_t amount, const char* unit, cJSON** out_json) {
if (!out_json || amount == 0 || !g_wallet.initialized || !g_wallet.cfg) return -1;
if (ensure_wallet_ready() != 0) return -1;
pthread_mutex_lock(&g_wallet.mutex);
const char* resolved = resolve_mint_url_locked(mint_url);
const char* resolved_unit = (unit && unit[0] != '\0') ? unit :
(g_wallet.cfg->cashu_wallet.unit[0] ? g_wallet.cfg->cashu_wallet.unit : "sat");
int timeout = g_wallet.cfg->cashu_wallet.mint_timeout_seconds > 0 ? g_wallet.cfg->cashu_wallet.mint_timeout_seconds : 30;
pthread_mutex_unlock(&g_wallet.mutex);
if (!resolved) return -1;
cashu_mint_quote_t quote;
memset(&quote, 0, sizeof(quote));
if (cashu_mint_request_mint_quote(resolved, amount, resolved_unit, &quote, timeout) != 0) {
return -1;
}
cJSON* root = cJSON_CreateObject();
if (!root) return -1;
cJSON_AddBoolToObject(root, "success", 1);
cJSON_AddStringToObject(root, "mint", resolved);
cJSON_AddStringToObject(root, "unit", resolved_unit);
cJSON_AddStringToObject(root, "quote_id", quote.quote_id);
cJSON_AddStringToObject(root, "payment_request", quote.payment_request);
cJSON_AddNumberToObject(root, "amount", (double)quote.amount);
cJSON_AddBoolToObject(root, "paid", quote.paid ? 1 : 0);
cJSON_AddNumberToObject(root, "expiry", (double)quote.expiry);
*out_json = root;
return 0;
}
int cashu_wallet_mint_check(const char* mint_url, const char* quote_id, cJSON** out_json) {
if (!out_json || !quote_id || quote_id[0] == '\0' || !g_wallet.initialized || !g_wallet.cfg) return -1;
if (ensure_wallet_ready() != 0) return -1;
pthread_mutex_lock(&g_wallet.mutex);
const char* resolved = resolve_mint_url_locked(mint_url);
int timeout = g_wallet.cfg->cashu_wallet.mint_timeout_seconds > 0 ? g_wallet.cfg->cashu_wallet.mint_timeout_seconds : 30;
pthread_mutex_unlock(&g_wallet.mutex);
if (!resolved) return -1;
cashu_mint_quote_t quote;
memset(&quote, 0, sizeof(quote));
if (cashu_mint_check_mint_quote(resolved, quote_id, &quote, timeout) != 0) {
return -1;
}
cJSON* root = cJSON_CreateObject();
if (!root) return -1;
cJSON_AddBoolToObject(root, "success", 1);
cJSON_AddStringToObject(root, "mint", resolved);
cJSON_AddStringToObject(root, "quote_id", quote.quote_id);
cJSON_AddBoolToObject(root, "paid", quote.paid ? 1 : 0);
cJSON_AddNumberToObject(root, "amount", (double)quote.amount);
cJSON_AddStringToObject(root, "payment_request", quote.payment_request);
cJSON_AddNumberToObject(root, "expiry", (double)quote.expiry);
*out_json = root;
return 0;
}
int cashu_wallet_mint_claim(const char* mint_url, const char* quote_id, uint64_t amount, cJSON** out_json) {
if (!out_json || !quote_id || quote_id[0] == '\0' || !g_wallet.initialized || !g_wallet.cfg) return -1;
if (ensure_wallet_ready() != 0) return -1;
pthread_mutex_lock(&g_wallet.mutex);
const char* resolved = resolve_mint_url_locked(mint_url);
int timeout = g_wallet.cfg->cashu_wallet.mint_timeout_seconds > 0 ? g_wallet.cfg->cashu_wallet.mint_timeout_seconds : 30;
const char* unit = g_wallet.cfg->cashu_wallet.unit[0] ? g_wallet.cfg->cashu_wallet.unit : "sat";
pthread_mutex_unlock(&g_wallet.mutex);
if (!resolved) return -1;
cashu_mint_quote_t quote;
memset(&quote, 0, sizeof(quote));
if (cashu_mint_check_mint_quote(resolved, quote_id, &quote, timeout) != 0) {
return -1;
}
if (!quote.paid) {
return NOSTR_ERROR_CASHU_QUOTE_NOT_PAID;
}
uint64_t claim_amount = amount > 0 ? amount : quote.amount;
if (claim_amount == 0) return -1;
cashu_mint_info_t info;
memset(&info, 0, sizeof(info));
if (cashu_mint_get_info(resolved, &info, timeout) != 0) {
return -1;
}
cashu_keyset_t keyset;
memset(&keyset, 0, sizeof(keyset));
if (cashu_mint_get_active_keyset(&info, unit, &keyset) != 0) {
cashu_mint_free_info(&info);
return -1;
}
cashu_mint_free_info(&info);
uint64_t parts[CASHU_WALLET_MAX_SPLIT_PARTS] = {0};
int part_count = 0;
if (cashu_mint_plan_split_amounts(claim_amount, parts, CASHU_WALLET_MAX_SPLIT_PARTS, &part_count) != 0 || part_count <= 0) {
return -1;
}
cashu_blinded_output_t* outputs = (cashu_blinded_output_t*)calloc((size_t)part_count, sizeof(cashu_blinded_output_t));
if (!outputs) return -1;
for (int i = 0; i < part_count; i++) {
snprintf(outputs[i].id, sizeof(outputs[i].id), "%s", keyset.id);
outputs[i].amount = parts[i];
snprintf(outputs[i].B_, sizeof(outputs[i].B_), "placeholder_%d", i + 1);
}
cashu_mint_tokens_request_t req;
memset(&req, 0, sizeof(req));
snprintf(req.quote_id, sizeof(req.quote_id), "%s", quote_id);
snprintf(req.unit, sizeof(req.unit), "%s", unit);
req.outputs = outputs;
req.output_count = part_count;
cJSON* request_body = NULL;
int rc = cashu_build_mint_tokens_request(&req, &request_body);
free(outputs);
if (rc != 0 || !request_body) {
cJSON_Delete(request_body);
return -1;
}
cJSON* response_body = NULL;
rc = cashu_mint_mint_tokens(resolved, request_body, &response_body, timeout);
cJSON_Delete(request_body);
if (rc != 0 || !response_body) {
cJSON_Delete(response_body);
return -1;
}
cashu_mint_tokens_response_t parsed;
memset(&parsed, 0, sizeof(parsed));
rc = cashu_parse_mint_tokens_response(response_body, &parsed);
cJSON_Delete(response_body);
if (rc != 0 || parsed.signature_count <= 0) {
cashu_mint_free_mint_tokens_response(&parsed);
return -1;
}
nostr_nip60_token_data_t token;
memset(&token, 0, sizeof(token));
token.mint_url = strdup(resolved);
token.proof_count = parsed.signature_count;
token.proofs = (nostr_cashu_proof_t*)calloc((size_t)token.proof_count, sizeof(nostr_cashu_proof_t));
if (!token.mint_url || !token.proofs) {
nostr_nip60_free_token_data(&token);
cashu_mint_free_mint_tokens_response(&parsed);
return -1;
}
for (int i = 0; i < parsed.signature_count; i++) {
snprintf(token.proofs[i].id, sizeof(token.proofs[i].id), "%s", parsed.signatures[i].id);
token.proofs[i].amount = parsed.signatures[i].amount;
token.proofs[i].secret = strdup(parsed.signatures[i].C_);
token.proofs[i].C = strdup(parsed.signatures[i].C_);
if (!token.proofs[i].secret || !token.proofs[i].C) {
nostr_nip60_free_token_data(&token);
cashu_mint_free_mint_tokens_response(&parsed);
return -1;
}
}
cashu_mint_free_mint_tokens_response(&parsed);
char token_event_id[65] = {0};
pthread_mutex_lock(&g_wallet.mutex);
rc = publish_token_event_and_store_locked(&token, token_event_id);
pthread_mutex_unlock(&g_wallet.mutex);
if (rc != 0) {
nostr_nip60_free_token_data(&token);
return -1;
}
(void)publish_history_event(NOSTR_NIP60_DIRECTION_IN,
claim_amount,
token_event_id,
NOSTR_NIP60_REF_CREATED);
cJSON* root = cJSON_CreateObject();
if (!root) return -1;
cJSON_AddBoolToObject(root, "success", 1);
cJSON_AddStringToObject(root, "mint", resolved);
cJSON_AddStringToObject(root, "quote_id", quote_id);
cJSON_AddNumberToObject(root, "claimed_amount", (double)claim_amount);
cJSON_AddStringToObject(root, "token_event_id", token_event_id);
*out_json = root;
return 0;
}
int cashu_wallet_melt_quote(const char* mint_url, const char* payment_request, const char* unit, cJSON** out_json) {
if (!out_json || !payment_request || payment_request[0] == '\0' || !g_wallet.initialized || !g_wallet.cfg) return -1;
if (ensure_wallet_ready() != 0) return -1;
pthread_mutex_lock(&g_wallet.mutex);
const char* resolved = resolve_mint_url_locked(mint_url);
const char* resolved_unit = (unit && unit[0] != '\0') ? unit :
(g_wallet.cfg->cashu_wallet.unit[0] ? g_wallet.cfg->cashu_wallet.unit : "sat");
int timeout = g_wallet.cfg->cashu_wallet.mint_timeout_seconds > 0 ? g_wallet.cfg->cashu_wallet.mint_timeout_seconds : 30;
pthread_mutex_unlock(&g_wallet.mutex);
if (!resolved) return -1;
cashu_melt_quote_t quote;
memset(&quote, 0, sizeof(quote));
if (cashu_mint_request_melt_quote(resolved, payment_request, resolved_unit, &quote, timeout) != 0) {
return -1;
}
cJSON* root = cJSON_CreateObject();
if (!root) return -1;
cJSON_AddBoolToObject(root, "success", 1);
cJSON_AddStringToObject(root, "mint", resolved);
cJSON_AddStringToObject(root, "quote_id", quote.quote_id);
cJSON_AddNumberToObject(root, "amount", (double)quote.amount);
cJSON_AddNumberToObject(root, "fee_reserve", (double)quote.fee_reserve);
cJSON_AddBoolToObject(root, "paid", quote.paid ? 1 : 0);
cJSON_AddNumberToObject(root, "expiry", (double)quote.expiry);
*out_json = root;
return 0;
}
int cashu_wallet_melt_pay(const char* mint_url, const char* quote_id, cJSON** out_json) {
if (!out_json || !quote_id || quote_id[0] == '\0' || !g_wallet.initialized || !g_wallet.cfg) return -1;
if (ensure_wallet_ready() != 0) return -1;
pthread_mutex_lock(&g_wallet.mutex);
const char* resolved = resolve_mint_url_locked(mint_url);
int timeout = g_wallet.cfg->cashu_wallet.mint_timeout_seconds > 0 ? g_wallet.cfg->cashu_wallet.mint_timeout_seconds : 30;
pthread_mutex_unlock(&g_wallet.mutex);
if (!resolved) return -1;
cashu_melt_quote_t quote;
memset(&quote, 0, sizeof(quote));
if (cashu_mint_check_melt_quote(resolved, quote_id, &quote, timeout) != 0) {
return -1;
}
uint64_t required = quote.amount + quote.fee_reserve;
if (required == 0) return -1;
pthread_mutex_lock(&g_wallet.mutex);
int proof_count = 0;
for (int i = 0; i < g_wallet.token_count; i++) {
if (!g_wallet.tokens[i].token.mint_url || strcmp(g_wallet.tokens[i].token.mint_url, resolved) != 0) continue;
proof_count += g_wallet.tokens[i].token.proof_count;
}
if (proof_count <= 0) {
pthread_mutex_unlock(&g_wallet.mutex);
return NOSTR_ERROR_NIP60_INSUFFICIENT_FUNDS;
}
nostr_cashu_proof_t* flattened = (nostr_cashu_proof_t*)calloc((size_t)proof_count, sizeof(nostr_cashu_proof_t));
int* proof_to_token = (int*)calloc((size_t)proof_count, sizeof(int));
if (!flattened || !proof_to_token) {
free(flattened);
free(proof_to_token);
pthread_mutex_unlock(&g_wallet.mutex);
return -1;
}
int idx = 0;
for (int i = 0; i < g_wallet.token_count; i++) {
if (!g_wallet.tokens[i].token.mint_url || strcmp(g_wallet.tokens[i].token.mint_url, resolved) != 0) continue;
for (int j = 0; j < g_wallet.tokens[i].token.proof_count; j++) {
flattened[idx] = g_wallet.tokens[i].token.proofs[j];
proof_to_token[idx] = i;
idx++;
}
}
int* selected = (int*)calloc((size_t)proof_count, sizeof(int));
uint64_t selected_total = 0;
int selected_count = cashu_mint_select_proofs_for_amount(flattened,
proof_count,
required,
selected,
proof_count,
&selected_total);
if (selected_count <= 0) {
free(selected);
free(proof_to_token);
free(flattened);
pthread_mutex_unlock(&g_wallet.mutex);
return NOSTR_ERROR_NIP60_INSUFFICIENT_FUNDS;
}
nostr_cashu_proof_t* melt_inputs = (nostr_cashu_proof_t*)calloc((size_t)selected_count, sizeof(nostr_cashu_proof_t));
int* used_token_flags = (int*)calloc((size_t)g_wallet.token_count, sizeof(int));
if (!melt_inputs || !used_token_flags) {
free(melt_inputs);
free(used_token_flags);
free(selected);
free(proof_to_token);
free(flattened);
pthread_mutex_unlock(&g_wallet.mutex);
return -1;
}
for (int i = 0; i < selected_count; i++) {
int pidx = selected[i];
melt_inputs[i] = flattened[pidx];
used_token_flags[proof_to_token[pidx]] = 1;
}
free(selected);
free(proof_to_token);
free(flattened);
cashu_melt_tokens_request_t req;
memset(&req, 0, sizeof(req));
snprintf(req.quote_id, sizeof(req.quote_id), "%s", quote_id);
req.inputs = melt_inputs;
req.input_count = selected_count;
cJSON* request_body = NULL;
int rc = cashu_build_melt_tokens_request(&req, &request_body);
free(melt_inputs);
if (rc != 0 || !request_body) {
free(used_token_flags);
pthread_mutex_unlock(&g_wallet.mutex);
return -1;
}
cJSON* response_body = NULL;
rc = cashu_mint_melt_tokens(resolved, request_body, &response_body, timeout);
cJSON_Delete(request_body);
if (rc != 0 || !response_body) {
free(used_token_flags);
cJSON_Delete(response_body);
pthread_mutex_unlock(&g_wallet.mutex);
return -1;
}
cashu_melt_tokens_response_t melt;
memset(&melt, 0, sizeof(melt));
rc = cashu_parse_melt_tokens_response(response_body, &melt);
cJSON_Delete(response_body);
if (rc != 0) {
free(used_token_flags);
pthread_mutex_unlock(&g_wallet.mutex);
return -1;
}
int deleted_count = 0;
for (int i = 0; i < g_wallet.token_count; i++) {
if (!used_token_flags[i]) continue;
if (g_wallet.tokens[i].event_id[0] != '\0') {
(void)publish_token_deletion_event(g_wallet.tokens[i].event_id);
deleted_count++;
}
nostr_nip60_free_token_data(&g_wallet.tokens[i].token);
memset(&g_wallet.tokens[i], 0, sizeof(g_wallet.tokens[i]));
}
int compact = 0;
for (int i = 0; i < g_wallet.token_count; i++) {
if (g_wallet.tokens[i].event_id[0] == '\0' && !g_wallet.tokens[i].token.mint_url) continue;
if (compact != i) g_wallet.tokens[compact] = g_wallet.tokens[i];
compact++;
}
g_wallet.token_count = compact;
if (g_wallet.token_count == 0) {
free(g_wallet.tokens);
g_wallet.tokens = NULL;
}
uint64_t remaining = 0;
token_proofs_total_for_mint_locked(resolved, &remaining);
cJSON* root = cJSON_CreateObject();
if (!root) {
free(used_token_flags);
cashu_mint_free_melt_tokens_response(&melt);
pthread_mutex_unlock(&g_wallet.mutex);
return -1;
}
cJSON_AddBoolToObject(root, "success", 1);
cJSON_AddStringToObject(root, "mint", resolved);
cJSON_AddStringToObject(root, "quote_id", quote_id);
cJSON_AddBoolToObject(root, "paid", melt.paid ? 1 : 0);
cJSON_AddNumberToObject(root, "deleted_token_events", deleted_count);
cJSON_AddNumberToObject(root, "remaining_balance", (double)remaining);
if (melt.payment_preimage && melt.payment_preimage[0] != '\0') {
cJSON_AddStringToObject(root, "payment_preimage", melt.payment_preimage);
}
*out_json = root;
(void)publish_history_event(NOSTR_NIP60_DIRECTION_OUT,
required,
NULL,
NOSTR_NIP60_REF_DESTROYED);
free(used_token_flags);
cashu_mint_free_melt_tokens_response(&melt);
pthread_mutex_unlock(&g_wallet.mutex);
return 0;
}
int cashu_wallet_check_proofs(const char* mint_url, cJSON** out_json) {
if (!out_json || !g_wallet.initialized || !g_wallet.cfg) return -1;
if (ensure_wallet_ready() != 0) return -1;
pthread_mutex_lock(&g_wallet.mutex);
const char* resolved = resolve_mint_url_locked(mint_url);
int timeout = g_wallet.cfg->cashu_wallet.mint_timeout_seconds > 0 ? g_wallet.cfg->cashu_wallet.mint_timeout_seconds : 30;
pthread_mutex_unlock(&g_wallet.mutex);
if (!resolved) return -1;
pthread_mutex_lock(&g_wallet.mutex);
int target_proofs = 0;
for (int i = 0; i < g_wallet.token_count; i++) {
if (!g_wallet.tokens[i].token.mint_url || strcmp(g_wallet.tokens[i].token.mint_url, resolved) != 0) continue;
target_proofs += g_wallet.tokens[i].token.proof_count;
}
if (target_proofs <= 0) {
cJSON* root = cJSON_CreateObject();
if (!root) {
pthread_mutex_unlock(&g_wallet.mutex);
return -1;
}
cJSON_AddBoolToObject(root, "success", 1);
cJSON_AddStringToObject(root, "mint", resolved);
cJSON_AddNumberToObject(root, "proof_count", 0);
cJSON_AddNumberToObject(root, "spent_count", 0);
cJSON_AddNumberToObject(root, "unknown_count", 0);
*out_json = root;
pthread_mutex_unlock(&g_wallet.mutex);
return 0;
}
const char** ys = (const char**)calloc((size_t)target_proofs, sizeof(char*));
if (!ys) {
pthread_mutex_unlock(&g_wallet.mutex);
return -1;
}
int y_idx = 0;
for (int i = 0; i < g_wallet.token_count; i++) {
if (!g_wallet.tokens[i].token.mint_url || strcmp(g_wallet.tokens[i].token.mint_url, resolved) != 0) continue;
for (int j = 0; j < g_wallet.tokens[i].token.proof_count; j++) {
ys[y_idx++] = g_wallet.tokens[i].token.proofs[j].C ? g_wallet.tokens[i].token.proofs[j].C : "";
}
}
pthread_mutex_unlock(&g_wallet.mutex);
cJSON* request_body = NULL;
if (cashu_build_checkstate_request(ys, y_idx, &request_body) != 0 || !request_body) {
free(ys);
cJSON_Delete(request_body);
return -1;
}
cJSON* response_body = NULL;
int rc = cashu_mint_check_proofs_state(resolved, request_body, &response_body, timeout);
cJSON_Delete(request_body);
free(ys);
if (rc != 0 || !response_body) {
cJSON_Delete(response_body);
return -1;
}
cashu_checkstate_response_t parsed;
memset(&parsed, 0, sizeof(parsed));
rc = cashu_parse_checkstate_response(response_body, &parsed);
cJSON_Delete(response_body);
if (rc != 0) {
return -1;
}
int spent_count = 0;
int unknown_count = 0;
for (int i = 0; i < parsed.state_count; i++) {
if (strcasecmp(parsed.states[i].state, "SPENT") == 0) {
spent_count++;
} else if (strcasecmp(parsed.states[i].state, "UNSPENT") != 0) {
unknown_count++;
}
}
cJSON* root = cJSON_CreateObject();
if (!root) {
cashu_mint_free_checkstate_response(&parsed);
return -1;
}
cJSON_AddBoolToObject(root, "success", 1);
cJSON_AddStringToObject(root, "mint", resolved);
cJSON_AddNumberToObject(root, "proof_count", y_idx);
cJSON_AddNumberToObject(root, "spent_count", spent_count);
cJSON_AddNumberToObject(root, "unknown_count", unknown_count);
*out_json = root;
cashu_mint_free_checkstate_response(&parsed);
return 0;
}
int cashu_wallet_receive_token(const char* token_string, const char* mint_url, cJSON** out_json) {
if (!out_json || !token_string || token_string[0] == '\0' || !g_wallet.initialized || !g_wallet.cfg) return -1;
if (ensure_wallet_ready() != 0) return -1;
cashu_decoded_token_t decoded;
memset(&decoded, 0, sizeof(decoded));
if (cashu_decode_token(token_string, &decoded) != 0) {
return -1;
}
pthread_mutex_lock(&g_wallet.mutex);
const char* cfg_resolved = resolve_mint_url_locked(mint_url);
int timeout = g_wallet.cfg->cashu_wallet.mint_timeout_seconds > 0 ? g_wallet.cfg->cashu_wallet.mint_timeout_seconds : 30;
const char* unit = g_wallet.cfg->cashu_wallet.unit[0] ? g_wallet.cfg->cashu_wallet.unit : "sat";
pthread_mutex_unlock(&g_wallet.mutex);
const char* resolved = cfg_resolved && cfg_resolved[0] ? cfg_resolved : decoded.mint_url;
if (!resolved || resolved[0] == '\0') {
cashu_free_decoded_token(&decoded);
return -1;
}
if (decoded.mint_url && decoded.mint_url[0] && strcmp(decoded.mint_url, resolved) != 0) {
cashu_free_decoded_token(&decoded);
return NOSTR_ERROR_INVALID_INPUT;
}
uint64_t incoming_total = nostr_nip60_sum_proofs(decoded.proofs, decoded.proof_count);
if (incoming_total == 0) {
cashu_free_decoded_token(&decoded);
return NOSTR_ERROR_INVALID_INPUT;
}
cashu_mint_info_t info;
memset(&info, 0, sizeof(info));
if (cashu_mint_get_info(resolved, &info, timeout) != 0) {
cashu_free_decoded_token(&decoded);
return -1;
}
cashu_keyset_t keyset;
memset(&keyset, 0, sizeof(keyset));
if (cashu_mint_get_active_keyset(&info, unit, &keyset) != 0 || !info.pubkey || info.pubkey[0] == '\0') {
cashu_mint_free_info(&info);
cashu_free_decoded_token(&decoded);
return -1;
}
uint64_t parts[CASHU_WALLET_MAX_SPLIT_PARTS] = {0};
int part_count = 0;
if (cashu_mint_plan_split_amounts(incoming_total, parts, CASHU_WALLET_MAX_SPLIT_PARTS, &part_count) != 0 || part_count <= 0) {
cashu_mint_free_info(&info);
cashu_free_decoded_token(&decoded);
return -1;
}
cashu_blinded_output_t* outputs = (cashu_blinded_output_t*)calloc((size_t)part_count, sizeof(cashu_blinded_output_t));
unsigned char* blinds = (unsigned char*)calloc((size_t)part_count, 32);
char** secrets = (char**)calloc((size_t)part_count, sizeof(char*));
if (!outputs || !blinds || !secrets) {
free(outputs);
free(blinds);
free(secrets);
cashu_mint_free_info(&info);
cashu_free_decoded_token(&decoded);
return -1;
}
int rc = 0;
for (int i = 0; i < part_count; i++) {
unsigned char rnd_priv[32] = {0};
unsigned char rnd_pub[32] = {0};
if (nostr_generate_keypair(rnd_priv, rnd_pub) != NOSTR_SUCCESS) {
rc = -1;
break;
}
char* secret = (char*)malloc(65);
if (!secret) {
rc = -1;
break;
}
nostr_bytes_to_hex(rnd_priv, 32, secret);
snprintf(outputs[i].id, sizeof(outputs[i].id), "%s", keyset.id);
outputs[i].amount = parts[i];
if (cashu_blind_message(secret, info.pubkey, outputs[i].B_, &blinds[(size_t)i * 32]) != 0) {
free(secret);
rc = -1;
break;
}
secrets[i] = secret;
}
if (rc != 0) {
for (int i = 0; i < part_count; i++) free(secrets[i]);
free(secrets);
free(blinds);
free(outputs);
cashu_mint_free_info(&info);
cashu_free_decoded_token(&decoded);
return -1;
}
cashu_swap_request_t swap_req;
memset(&swap_req, 0, sizeof(swap_req));
swap_req.inputs = decoded.proofs;
swap_req.input_count = decoded.proof_count;
swap_req.outputs = outputs;
swap_req.output_count = part_count;
cJSON* request_body = NULL;
rc = cashu_build_swap_request(&swap_req, &request_body);
free(outputs);
if (rc != 0 || !request_body) {
for (int i = 0; i < part_count; i++) free(secrets[i]);
free(secrets);
free(blinds);
cashu_mint_free_info(&info);
cashu_free_decoded_token(&decoded);
cJSON_Delete(request_body);
return -1;
}
cJSON* response_body = NULL;
rc = cashu_mint_swap(resolved, request_body, &response_body, timeout);
cJSON_Delete(request_body);
if (rc != 0 || !response_body) {
for (int i = 0; i < part_count; i++) free(secrets[i]);
free(secrets);
free(blinds);
cashu_mint_free_info(&info);
cashu_free_decoded_token(&decoded);
cJSON_Delete(response_body);
return -1;
}
cashu_swap_response_t parsed;
memset(&parsed, 0, sizeof(parsed));
rc = cashu_parse_swap_response(response_body, &parsed);
cJSON_Delete(response_body);
if (rc != 0 || parsed.signature_count <= 0 || parsed.signature_count != part_count) {
cashu_mint_free_swap_response(&parsed);
for (int i = 0; i < part_count; i++) free(secrets[i]);
free(secrets);
free(blinds);
cashu_mint_free_info(&info);
cashu_free_decoded_token(&decoded);
return -1;
}
nostr_nip60_token_data_t token;
memset(&token, 0, sizeof(token));
token.mint_url = strdup(resolved);
token.proof_count = parsed.signature_count;
token.proofs = (nostr_cashu_proof_t*)calloc((size_t)token.proof_count, sizeof(nostr_cashu_proof_t));
if (!token.mint_url || !token.proofs) {
nostr_nip60_free_token_data(&token);
cashu_mint_free_swap_response(&parsed);
for (int i = 0; i < part_count; i++) free(secrets[i]);
free(secrets);
free(blinds);
cashu_mint_free_info(&info);
cashu_free_decoded_token(&decoded);
return -1;
}
for (int i = 0; i < parsed.signature_count; i++) {
snprintf(token.proofs[i].id, sizeof(token.proofs[i].id), "%s", parsed.signatures[i].id);
token.proofs[i].amount = parsed.signatures[i].amount;
char unblinded_C[67] = {0};
if (cashu_unblind_signature(parsed.signatures[i].C_, info.pubkey, &blinds[(size_t)i * 32], unblinded_C) != 0) {
nostr_nip60_free_token_data(&token);
cashu_mint_free_swap_response(&parsed);
for (int j = 0; j < part_count; j++) free(secrets[j]);
free(secrets);
free(blinds);
cashu_mint_free_info(&info);
cashu_free_decoded_token(&decoded);
return -1;
}
token.proofs[i].secret = secrets[i] ? strdup(secrets[i]) : NULL;
token.proofs[i].C = strdup(unblinded_C);
if (!token.proofs[i].secret || !token.proofs[i].C) {
nostr_nip60_free_token_data(&token);
cashu_mint_free_swap_response(&parsed);
for (int j = 0; j < part_count; j++) free(secrets[j]);
free(secrets);
free(blinds);
cashu_mint_free_info(&info);
cashu_free_decoded_token(&decoded);
return -1;
}
}
cashu_mint_free_swap_response(&parsed);
for (int i = 0; i < part_count; i++) free(secrets[i]);
free(secrets);
free(blinds);
cashu_mint_free_info(&info);
char token_event_id[65] = {0};
pthread_mutex_lock(&g_wallet.mutex);
rc = publish_token_event_and_store_locked(&token, token_event_id);
pthread_mutex_unlock(&g_wallet.mutex);
if (rc != 0) {
nostr_nip60_free_token_data(&token);
cashu_free_decoded_token(&decoded);
return -1;
}
(void)publish_history_event(NOSTR_NIP60_DIRECTION_IN,
incoming_total,
token_event_id,
NOSTR_NIP60_REF_CREATED);
cJSON* root = cJSON_CreateObject();
if (!root) {
cashu_free_decoded_token(&decoded);
return -1;
}
cJSON_AddBoolToObject(root, "success", 1);
cJSON_AddStringToObject(root, "mint", resolved);
cJSON_AddNumberToObject(root, "received_amount", (double)incoming_total);
cJSON_AddStringToObject(root, "token_event_id", token_event_id);
cJSON_AddNumberToObject(root, "received_proofs", decoded.proof_count);
cJSON_AddNumberToObject(root, "new_proofs", token.proof_count);
*out_json = root;
cashu_free_decoded_token(&decoded);
return 0;
}
int cashu_wallet_mints_get(cJSON** out_json) {
if (!out_json || !g_wallet.initialized || !g_wallet.cfg) return -1;
(void)ensure_wallet_ready();
cJSON* root = cJSON_CreateObject();
cJSON* wallet_mints = cJSON_CreateArray();
cJSON* public_mints = cJSON_CreateArray();
if (!root || !wallet_mints || !public_mints) {
cJSON_Delete(root);
cJSON_Delete(wallet_mints);
cJSON_Delete(public_mints);
return -1;
}
cJSON_AddBoolToObject(root, "success", 1);
pthread_mutex_lock(&g_wallet.mutex);
int wallet_loaded = g_wallet.wallet_loaded;
if (g_wallet.wallet_loaded && g_wallet.wallet.mint_urls) {
for (int i = 0; i < g_wallet.wallet.mint_count; i++) {
if (g_wallet.wallet.mint_urls[i] && g_wallet.wallet.mint_urls[i][0] != '\0') {
cJSON_AddItemToArray(wallet_mints, cJSON_CreateString(g_wallet.wallet.mint_urls[i]));
}
}
}
pthread_mutex_unlock(&g_wallet.mutex);
nostr_nip61_nutzap_info_t info;
memset(&info, 0, sizeof(info));
int have_public = (load_latest_public_nutzap_info(&info) == 0);
if (have_public) {
for (int i = 0; i < info.mint_count; i++) {
if (info.mints[i].url && info.mints[i].url[0] != '\0') {
cJSON_AddItemToArray(public_mints, cJSON_CreateString(info.mints[i].url));
}
}
}
cJSON_AddBoolToObject(root, "wallet_loaded", wallet_loaded ? 1 : 0);
cJSON_AddBoolToObject(root, "public_info_found", have_public ? 1 : 0);
cJSON_AddItemToObject(root, "wallet_event_mints", wallet_mints);
cJSON_AddItemToObject(root, "public_nutzap_mints", public_mints);
if (have_public) {
cJSON_AddStringToObject(root, "wallet_pubkey", info.pubkey);
nostr_nip61_free_nutzap_info(&info);
}
*out_json = root;
return 0;
}
int cashu_wallet_mints_set(const char** wallet_mints,
int wallet_mint_count,
const char** public_mints,
int public_mint_count,
int set_wallet,
int set_public,
cJSON** out_json) {
if (!out_json || !g_wallet.initialized || !g_wallet.cfg) return -1;
if (!set_wallet && !set_public) return NOSTR_ERROR_INVALID_INPUT;
int wallet_updated = 0;
int public_updated = 0;
if (set_wallet) {
if (!wallet_mints || wallet_mint_count <= 0) return NOSTR_ERROR_INVALID_INPUT;
pthread_mutex_lock(&g_wallet.mutex);
nostr_nip60_wallet_data_t next_wallet;
memset(&next_wallet, 0, sizeof(next_wallet));
if (g_wallet.wallet_loaded && g_wallet.wallet.privkey[0] != '\0') {
snprintf(next_wallet.privkey, sizeof(next_wallet.privkey), "%s", g_wallet.wallet.privkey);
} else {
unsigned char priv[32] = {0};
unsigned char pub[32] = {0};
if (nostr_generate_keypair(priv, pub) != NOSTR_SUCCESS) {
pthread_mutex_unlock(&g_wallet.mutex);
return -1;
}
nostr_bytes_to_hex(priv, 32, next_wallet.privkey);
}
if (dup_string_array(wallet_mints, wallet_mint_count, &next_wallet.mint_urls, &next_wallet.mint_count) != 0 ||
next_wallet.mint_count <= 0) {
nostr_nip60_free_wallet_data(&next_wallet);
pthread_mutex_unlock(&g_wallet.mutex);
return -1;
}
int rc = publish_wallet_event_and_store_locked(&next_wallet, NULL);
nostr_nip60_free_wallet_data(&next_wallet);
pthread_mutex_unlock(&g_wallet.mutex);
if (rc != 0) return rc;
wallet_updated = 1;
}
if (set_public) {
if (!public_mints || public_mint_count <= 0) return NOSTR_ERROR_INVALID_INPUT;
if (ensure_wallet_ready() != 0) return -1;
pthread_mutex_lock(&g_wallet.mutex);
nostr_nip61_nutzap_info_t info;
int rc = build_nutzap_info_from_mints_locked(public_mints, public_mint_count, &info);
pthread_mutex_unlock(&g_wallet.mutex);
if (rc != 0) return -1;
cJSON* ev = nostr_nip61_create_nutzap_info_event(&info, g_wallet.cfg->keys.private_key, time(NULL));
nostr_nip61_free_nutzap_info(&info);
if (!ev) return -1;
rc = publish_from_signed_event_template(ev, NULL);
cJSON_Delete(ev);
if (rc != 0) return rc;
public_updated = 1;
}
cJSON* root = cJSON_CreateObject();
if (!root) return -1;
cJSON_AddBoolToObject(root, "success", 1);
cJSON_AddBoolToObject(root, "wallet_updated", wallet_updated ? 1 : 0);
cJSON_AddBoolToObject(root, "public_updated", public_updated ? 1 : 0);
*out_json = root;
return 0;
}