Files
didactyl/src/cashu_wallet.c
T

2779 lines
103 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_block_list.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;
/* private secp256k1 wrappers from nostr_core_lib utils.c */
typedef struct {
unsigned char data[64];
} nostr_secp256k1_pubkey;
int nostr_secp256k1_ec_pubkey_parse(nostr_secp256k1_pubkey* pubkey, const unsigned char* input, size_t inputlen);
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;
}
const char* ca_bundle = nostr_http_detect_ca_bundle();
cashu_mint_set_ca_bundle(ca_bundle);
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_has_default_mint(void) {
if (!g_wallet.initialized || !g_wallet.cfg) return 0;
if (ensure_wallet_ready() != 0) return 0;
int has_default = 0;
pthread_mutex_lock(&g_wallet.mutex);
has_default = (g_wallet.wallet_loaded &&
g_wallet.wallet.mint_count > 0 &&
g_wallet.wallet.mint_urls &&
g_wallet.wallet.mint_urls[0] &&
g_wallet.wallet.mint_urls[0][0] != '\0');
pthread_mutex_unlock(&g_wallet.mutex);
return has_default;
}
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;
}
static int deleted_id_index(char** deleted_ids, int deleted_count, const char* event_id) {
if (!deleted_ids || deleted_count <= 0 || !event_id || event_id[0] == '\0') return -1;
for (int i = 0; i < deleted_count; i++) {
if (deleted_ids[i] && strcmp(deleted_ids[i], event_id) == 0) {
return i;
}
}
return -1;
}
static int deleted_id_add(char*** deleted_ids, int* deleted_count, const char* event_id) {
if (!deleted_ids || !deleted_count || !event_id || event_id[0] == '\0') return -1;
if (deleted_id_index(*deleted_ids, *deleted_count, event_id) >= 0) return 0;
char** grown = (char**)realloc(*deleted_ids, (size_t)(*deleted_count + 1) * sizeof(char*));
if (!grown) return -1;
grown[*deleted_count] = strdup(event_id);
if (!grown[*deleted_count]) {
return -1;
}
*deleted_ids = grown;
(*deleted_count)++;
return 0;
}
static void deleted_id_list_free(char** deleted_ids, int deleted_count) {
if (!deleted_ids) return;
for (int i = 0; i < deleted_count; i++) {
free(deleted_ids[i]);
}
free(deleted_ids);
}
static void collect_deleted_ids_from_event(cJSON* ev, char*** deleted_ids, int* deleted_count) {
if (!ev || !deleted_ids || !deleted_count) return;
cJSON* kind = cJSON_GetObjectItemCaseSensitive(ev, "kind");
if (!kind || !cJSON_IsNumber(kind)) return;
int kind_value = (int)kind->valuedouble;
if (kind_value == 5) {
cJSON* tags = cJSON_GetObjectItemCaseSensitive(ev, "tags");
if (!tags || !cJSON_IsArray(tags)) return;
cJSON* tag = NULL;
cJSON_ArrayForEach(tag, tags) {
if (!tag || !cJSON_IsArray(tag) || cJSON_GetArraySize(tag) < 2) continue;
cJSON* t0 = cJSON_GetArrayItem(tag, 0);
cJSON* t1 = cJSON_GetArrayItem(tag, 1);
if (!t0 || !t1 || !cJSON_IsString(t0) || !cJSON_IsString(t1) || !t0->valuestring || !t1->valuestring) continue;
if (strcmp(t0->valuestring, "e") != 0) continue;
(void)deleted_id_add(deleted_ids, deleted_count, t1->valuestring);
}
return;
}
if (kind_value == NOSTR_NIP60_TOKEN_KIND) {
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) {
for (int i = 0; i < token.deleted_count; i++) {
if (token.deleted_token_ids[i] && token.deleted_token_ids[i][0] != '\0') {
(void)deleted_id_add(deleted_ids, deleted_count, token.deleted_token_ids[i]);
}
}
}
nostr_nip60_free_token_data(&token);
}
}
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;
char** deleted_ids = NULL;
int deleted_count = 0;
int n = cJSON_GetArraySize(arr);
for (int i = 0; i < n; i++) {
cJSON* ev = cJSON_GetArrayItem(arr, i);
collect_deleted_ids_from_event(ev, &deleted_ids, &deleted_count);
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) {
deleted_id_list_free(deleted_ids, deleted_count);
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;
if (deleted_id_index(deleted_ids, deleted_count, id->valuestring) >= 0) {
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);
deleted_id_list_free(deleted_ids, deleted_count);
cJSON_Delete(arr);
pthread_mutex_unlock(&g_wallet.mutex);
return -1;
}
}
deleted_id_list_free(deleted_ids, deleted_count);
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;
int rc = -1;
cJSON* request_body = NULL;
cJSON* response_body = NULL;
cJSON* root = NULL;
cashu_mint_quote_t quote;
memset(&quote, 0, sizeof(quote));
cashu_mint_info_t info;
memset(&info, 0, sizeof(info));
cashu_keyset_t keyset;
memset(&keyset, 0, sizeof(keyset));
cashu_keyset_keys_t keyset_keys;
memset(&keyset_keys, 0, sizeof(keyset_keys));
cashu_blinded_output_t* outputs = NULL;
unsigned char* blinds = NULL;
char** secrets = NULL;
int output_count = 0;
cashu_mint_tokens_response_t parsed;
memset(&parsed, 0, sizeof(parsed));
nostr_nip60_token_data_t token;
memset(&token, 0, sizeof(token));
char token_event_id[65] = {0};
rc = cashu_mint_check_mint_quote(resolved, quote_id, &quote, timeout);
if (rc != 0) goto cleanup;
if (!quote.paid) return NOSTR_ERROR_CASHU_QUOTE_NOT_PAID;
uint64_t claim_amount = amount > 0 ? amount : quote.amount;
if (claim_amount == 0) {
rc = -1;
goto cleanup;
}
rc = cashu_mint_get_info(resolved, &info, timeout);
if (rc != 0) goto cleanup;
rc = cashu_mint_get_active_keyset(&info, unit, &keyset);
if (rc != 0) goto cleanup;
rc = cashu_mint_get_keys(resolved, keyset.id, &keyset_keys, timeout);
if (rc != 0) goto cleanup;
uint64_t parts[CASHU_WALLET_MAX_SPLIT_PARTS] = {0};
int part_count = 0;
rc = cashu_mint_plan_split_amounts(claim_amount,
parts,
CASHU_WALLET_MAX_SPLIT_PARTS,
&part_count);
if (rc != 0 || part_count <= 0) {
rc = (rc != 0) ? rc : -1;
goto cleanup;
}
output_count = part_count;
outputs = (cashu_blinded_output_t*)calloc((size_t)output_count, sizeof(cashu_blinded_output_t));
blinds = (unsigned char*)calloc((size_t)output_count, 32);
secrets = (char**)calloc((size_t)output_count, sizeof(char*));
if (!outputs || !blinds || !secrets) {
rc = NOSTR_ERROR_MEMORY_FAILED;
goto cleanup;
}
for (int i = 0; i < output_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;
goto cleanup;
}
char* secret = (char*)malloc(65);
if (!secret) {
rc = NOSTR_ERROR_MEMORY_FAILED;
goto cleanup;
}
nostr_bytes_to_hex(rnd_priv, 32, secret);
snprintf(outputs[i].id, sizeof(outputs[i].id), "%s", keyset.id);
outputs[i].amount = parts[i];
const char* amount_pubkey = NULL;
rc = cashu_keyset_keys_find_pubkey_for_amount(&keyset_keys,
outputs[i].amount,
&amount_pubkey);
if (rc != 0 || !amount_pubkey || amount_pubkey[0] == '\0') {
free(secret);
rc = (rc != 0) ? rc : NOSTR_ERROR_CASHU_INVALID_KEYSET;
goto cleanup;
}
if (cashu_blind_message(secret,
amount_pubkey,
outputs[i].B_,
&blinds[(size_t)i * 32]) != 0) {
free(secret);
rc = -1;
goto cleanup;
}
secrets[i] = secret;
}
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 = output_count;
rc = cashu_build_mint_tokens_request(&req, &request_body);
if (rc != 0 || !request_body) {
rc = (rc != 0) ? rc : -1;
goto cleanup;
}
rc = cashu_mint_mint_tokens(resolved, request_body, &response_body, timeout);
if (rc != 0 || !response_body) {
rc = (rc != 0) ? rc : -1;
goto cleanup;
}
rc = cashu_parse_mint_tokens_response(response_body, &parsed);
if (rc != 0 || parsed.signature_count <= 0 || parsed.signature_count != output_count) {
rc = (rc != 0) ? rc : -1;
goto cleanup;
}
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) {
rc = NOSTR_ERROR_MEMORY_FAILED;
goto cleanup;
}
for (int i = 0; i < parsed.signature_count; i++) {
char unblinded_C[67] = {0};
const char* amount_pubkey = NULL;
rc = cashu_keyset_keys_find_pubkey_for_amount(&keyset_keys,
parsed.signatures[i].amount,
&amount_pubkey);
if (rc != 0 || !amount_pubkey || amount_pubkey[0] == '\0' ||
cashu_unblind_signature(parsed.signatures[i].C_,
amount_pubkey,
&blinds[(size_t)i * 32],
unblinded_C) != 0) {
rc = (rc != 0) ? rc : -1;
goto cleanup;
}
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 = secrets[i] ? strdup(secrets[i]) : NULL;
token.proofs[i].C = strdup(unblinded_C);
if (!token.proofs[i].secret || !token.proofs[i].C) {
rc = NOSTR_ERROR_MEMORY_FAILED;
goto cleanup;
}
}
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) goto cleanup;
(void)publish_history_event(NOSTR_NIP60_DIRECTION_IN,
claim_amount,
token_event_id,
NOSTR_NIP60_REF_CREATED);
root = cJSON_CreateObject();
if (!root) {
rc = NOSTR_ERROR_MEMORY_FAILED;
goto cleanup;
}
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;
root = NULL;
rc = 0;
cleanup:
cJSON_Delete(request_body);
cJSON_Delete(response_body);
cJSON_Delete(root);
if (secrets) {
for (int i = 0; i < output_count; i++) {
free(secrets[i]);
}
}
free(secrets);
free(blinds);
free(outputs);
cashu_mint_free_keyset_keys(&keyset_keys);
cashu_mint_free_info(&info);
cashu_mint_free_mint_tokens_response(&parsed);
if (rc != 0) {
nostr_nip60_free_token_data(&token);
}
return rc;
}
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;
}
static int cashu_secret_to_y_hex(const char* secret, char out_y_hex[67]) {
if (!secret || !out_y_hex) return -1;
static const unsigned char domain_separator[] = "Secp256k1_HashToCurve_Cashu_";
size_t secret_len = strlen(secret);
size_t domain_len = sizeof(domain_separator) - 1;
size_t preimage_len = domain_len + secret_len;
unsigned char* preimage = (unsigned char*)malloc(preimage_len);
if (!preimage) return -1;
memcpy(preimage, domain_separator, domain_len);
memcpy(preimage + domain_len, (const unsigned char*)secret, secret_len);
unsigned char msg_hash[32] = {0};
if (nostr_sha256(preimage, preimage_len, msg_hash) != 0) {
free(preimage);
return -1;
}
free(preimage);
unsigned char candidate[33] = {0};
for (unsigned int counter = 0; counter < 65536U; counter++) {
unsigned char h[32] = {0};
unsigned char data[36] = {0};
memcpy(data, msg_hash, 32);
data[32] = (unsigned char)(counter & 0xffU);
data[33] = (unsigned char)((counter >> 8) & 0xffU);
data[34] = (unsigned char)((counter >> 16) & 0xffU);
data[35] = (unsigned char)((counter >> 24) & 0xffU);
if (nostr_sha256(data, sizeof(data), h) != 0) {
return -1;
}
candidate[0] = 0x02;
memcpy(candidate + 1, h, 32);
nostr_secp256k1_pubkey pub;
if (nostr_secp256k1_ec_pubkey_parse(&pub, candidate, sizeof(candidate))) {
nostr_bytes_to_hex(candidate, sizeof(candidate), out_y_hex);
return 0;
}
}
return -1;
}
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 token_count_snapshot = g_wallet.token_count;
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);
cJSON_AddNumberToObject(root, "deleted_token_events", 0);
cJSON_AddNumberToObject(root, "ignored_token_events", 0);
cJSON_AddNumberToObject(root, "remaining_balance", 0);
*out_json = root;
pthread_mutex_unlock(&g_wallet.mutex);
return 0;
}
const char** ys = (const char**)calloc((size_t)target_proofs, sizeof(char*));
char (*y_hexes)[67] = (char(*)[67])calloc((size_t)target_proofs, sizeof(*y_hexes));
int* proof_to_token_idx = (int*)calloc((size_t)target_proofs, sizeof(int));
int* token_total_counts = (int*)calloc((size_t)token_count_snapshot, sizeof(int));
int* token_spent_counts = (int*)calloc((size_t)token_count_snapshot, sizeof(int));
if (!ys || !y_hexes || !proof_to_token_idx || !token_total_counts || !token_spent_counts) {
free(ys);
free(y_hexes);
free(proof_to_token_idx);
free(token_total_counts);
free(token_spent_counts);
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++) {
const char* secret = g_wallet.tokens[i].token.proofs[j].secret;
if (!secret || cashu_secret_to_y_hex(secret, y_hexes[y_idx]) != 0) {
free(ys);
free(y_hexes);
free(proof_to_token_idx);
free(token_total_counts);
free(token_spent_counts);
pthread_mutex_unlock(&g_wallet.mutex);
return -1;
}
ys[y_idx] = y_hexes[y_idx];
proof_to_token_idx[y_idx] = i;
token_total_counts[i]++;
y_idx++;
}
}
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);
free(y_hexes);
free(proof_to_token_idx);
free(token_total_counts);
free(token_spent_counts);
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);
free(y_hexes);
if (rc != 0 || !response_body) {
free(proof_to_token_idx);
free(token_total_counts);
free(token_spent_counts);
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) {
free(proof_to_token_idx);
free(token_total_counts);
free(token_spent_counts);
return -1;
}
int spent_count = 0;
int unknown_count = 0;
int state_n = parsed.state_count < y_idx ? parsed.state_count : y_idx;
for (int i = 0; i < state_n; i++) {
if (strcasecmp(parsed.states[i].state, "SPENT") == 0) {
spent_count++;
int token_idx = proof_to_token_idx[i];
if (token_idx >= 0 && token_idx < token_count_snapshot) {
token_spent_counts[token_idx]++;
}
} else if (strcasecmp(parsed.states[i].state, "UNSPENT") != 0) {
unknown_count++;
}
}
int deleted_token_events = 0;
int ignored_token_events = 0;
uint64_t remaining = 0;
pthread_mutex_lock(&g_wallet.mutex);
for (int i = 0; i < g_wallet.token_count && i < token_count_snapshot; i++) {
if (!g_wallet.tokens[i].token.mint_url || strcmp(g_wallet.tokens[i].token.mint_url, resolved) != 0) continue;
if (token_total_counts[i] <= 0) continue;
if (token_spent_counts[i] < token_total_counts[i]) continue;
if (g_wallet.tokens[i].event_id[0] != '\0') {
if (publish_token_deletion_event(g_wallet.tokens[i].event_id) == 0) {
deleted_token_events++;
}
if (nostr_block_list_add("e", g_wallet.tokens[i].event_id, 0) == 0) {
ignored_token_events++;
}
}
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;
}
token_proofs_total_for_mint_locked(resolved, &remaining);
pthread_mutex_unlock(&g_wallet.mutex);
cJSON* root = cJSON_CreateObject();
if (!root) {
cashu_mint_free_checkstate_response(&parsed);
free(proof_to_token_idx);
free(token_total_counts);
free(token_spent_counts);
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);
cJSON_AddNumberToObject(root, "deleted_token_events", deleted_token_events);
cJSON_AddNumberToObject(root, "ignored_token_events", ignored_token_events);
cJSON_AddNumberToObject(root, "remaining_balance", (double)remaining);
*out_json = root;
cashu_mint_free_checkstate_response(&parsed);
free(proof_to_token_idx);
free(token_total_counts);
free(token_spent_counts);
return 0;
}
static int receive_token_error(cJSON** out_json,
const char* step,
int rc,
const char* error,
cJSON* mint_error_json) {
if (out_json) {
cJSON* root = cJSON_CreateObject();
if (root) {
cJSON_AddBoolToObject(root, "success", 0);
cJSON_AddStringToObject(root,
"error",
(error && error[0]) ? error : "cashu_wallet_receive_token failed");
if (step && step[0]) {
cJSON_AddStringToObject(root, "step", step);
}
cJSON_AddNumberToObject(root, "code", (double)rc);
if (mint_error_json) {
cJSON* dup = cJSON_Duplicate(mint_error_json, 1);
if (dup) {
cJSON_AddItemToObject(root, "mint_error", dup);
}
cJSON* detail = cJSON_GetObjectItemCaseSensitive(mint_error_json, "detail");
if (detail && cJSON_IsString(detail) && detail->valuestring) {
cJSON_AddStringToObject(root, "mint_error_detail", detail->valuestring);
}
}
*out_json = root;
}
}
return rc;
}
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) {
DEBUG_WARN("cashu_wallet_receive_token: invalid input or wallet not initialized");
return receive_token_error(out_json,
"preconditions",
NOSTR_ERROR_INVALID_INPUT,
"cashu_wallet_receive_token preconditions failed",
NULL);
}
*out_json = NULL;
int rc = ensure_wallet_ready();
if (rc != 0) {
DEBUG_WARN("cashu_wallet_receive_token: ensure_wallet_ready failed rc=%d; continuing with explicit token mint path", rc);
}
cashu_decoded_token_t decoded;
memset(&decoded, 0, sizeof(decoded));
rc = cashu_decode_token(token_string, &decoded);
if (rc != 0) {
DEBUG_WARN("cashu_wallet_receive_token: cashu_decode_token failed rc=%d", rc);
return receive_token_error(out_json,
"decode_token",
rc,
"failed to decode cashu token",
NULL);
}
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') {
DEBUG_WARN("cashu_wallet_receive_token: no resolved mint URL");
cashu_free_decoded_token(&decoded);
return receive_token_error(out_json,
"resolve_mint_url",
NOSTR_ERROR_INVALID_INPUT,
"no mint URL available for token receive",
NULL);
}
if (decoded.mint_url && decoded.mint_url[0] && strcmp(decoded.mint_url, resolved) != 0) {
DEBUG_WARN("cashu_wallet_receive_token: token mint mismatch token_mint=%s resolved_mint=%s",
decoded.mint_url,
resolved);
cashu_free_decoded_token(&decoded);
return receive_token_error(out_json,
"validate_mint_match",
NOSTR_ERROR_INVALID_INPUT,
"token mint does not match selected wallet mint",
NULL);
}
uint64_t incoming_total = nostr_nip60_sum_proofs(decoded.proofs, decoded.proof_count);
if (incoming_total == 0) {
DEBUG_WARN("cashu_wallet_receive_token: incoming token proof total is zero");
cashu_free_decoded_token(&decoded);
return receive_token_error(out_json,
"sum_proofs",
NOSTR_ERROR_INVALID_INPUT,
"token proofs total to zero",
NULL);
}
cashu_mint_info_t info;
memset(&info, 0, sizeof(info));
rc = cashu_mint_get_info(resolved, &info, timeout);
if (rc != 0) {
DEBUG_WARN("cashu_wallet_receive_token: cashu_mint_get_info failed rc=%d mint=%s", rc, resolved);
cashu_free_decoded_token(&decoded);
return receive_token_error(out_json,
"mint_get_info",
rc,
"failed to query mint info",
NULL);
}
cashu_keyset_t keyset;
memset(&keyset, 0, sizeof(keyset));
rc = cashu_mint_get_active_keyset(&info, unit, &keyset);
if (rc != 0) {
DEBUG_WARN("cashu_wallet_receive_token: active keyset unavailable rc=%d unit=%s", rc, unit);
cashu_mint_free_info(&info);
cashu_free_decoded_token(&decoded);
return receive_token_error(out_json,
"get_active_keyset",
rc,
"mint has no usable active keyset for requested unit",
NULL);
}
cashu_keyset_keys_t keyset_keys;
memset(&keyset_keys, 0, sizeof(keyset_keys));
rc = cashu_mint_get_keys(resolved, keyset.id, &keyset_keys, timeout);
if (rc != 0) {
DEBUG_WARN("cashu_wallet_receive_token: cashu_mint_get_keys failed rc=%d keyset=%s", rc, keyset.id);
cashu_mint_free_info(&info);
cashu_free_decoded_token(&decoded);
return receive_token_error(out_json,
"get_keyset_keys",
rc,
"failed to load mint keyset keys",
NULL);
}
uint64_t parts[CASHU_WALLET_MAX_SPLIT_PARTS] = {0};
int part_count = 0;
rc = cashu_mint_plan_split_amounts(incoming_total, parts, CASHU_WALLET_MAX_SPLIT_PARTS, &part_count);
if (rc != 0 || part_count <= 0) {
DEBUG_WARN("cashu_wallet_receive_token: cashu_mint_plan_split_amounts failed rc=%d incoming_total=%llu",
rc,
(unsigned long long)incoming_total);
cashu_mint_free_info(&info);
cashu_free_decoded_token(&decoded);
return receive_token_error(out_json,
"plan_split_amounts",
rc != 0 ? rc : -1,
"failed to split receive amount into blinded output parts",
NULL);
}
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) {
DEBUG_WARN("cashu_wallet_receive_token: allocation failed for outputs/blinds/secrets part_count=%d", part_count);
free(outputs);
free(blinds);
free(secrets);
cashu_mint_free_info(&info);
cashu_free_decoded_token(&decoded);
return receive_token_error(out_json,
"allocate_receive_buffers",
NOSTR_ERROR_MEMORY_FAILED,
"failed to allocate receive buffers",
NULL);
}
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) {
DEBUG_WARN("cashu_wallet_receive_token: nostr_generate_keypair failed at part=%d", i);
rc = -1;
break;
}
char* secret = (char*)malloc(65);
if (!secret) {
DEBUG_WARN("cashu_wallet_receive_token: secret allocation failed at part=%d", i);
rc = NOSTR_ERROR_MEMORY_FAILED;
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];
const char* amount_pubkey = NULL;
rc = cashu_keyset_keys_find_pubkey_for_amount(&keyset_keys, outputs[i].amount, &amount_pubkey);
if (rc != 0 || !amount_pubkey || amount_pubkey[0] == '\0') {
DEBUG_WARN("cashu_wallet_receive_token: missing keyset pubkey for amount=%llu rc=%d",
(unsigned long long)outputs[i].amount,
rc);
free(secret);
rc = (rc != 0) ? rc : NOSTR_ERROR_CASHU_INVALID_KEYSET;
break;
}
if (cashu_blind_message(secret, amount_pubkey, outputs[i].B_, &blinds[(size_t)i * 32]) != 0) {
DEBUG_WARN("cashu_wallet_receive_token: cashu_blind_message failed at part=%d", i);
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_keyset_keys(&keyset_keys);
cashu_mint_free_info(&info);
cashu_free_decoded_token(&decoded);
return receive_token_error(out_json,
"prepare_blinded_outputs",
rc,
"failed to prepare blinded outputs for swap",
NULL);
}
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) {
DEBUG_WARN("cashu_wallet_receive_token: cashu_build_swap_request failed rc=%d", rc);
for (int i = 0; i < part_count; i++) free(secrets[i]);
free(secrets);
free(blinds);
cashu_mint_free_keyset_keys(&keyset_keys);
cashu_mint_free_info(&info);
cashu_free_decoded_token(&decoded);
cJSON_Delete(request_body);
return receive_token_error(out_json,
"build_swap_request",
rc != 0 ? rc : -1,
"failed to build swap request",
NULL);
}
cJSON* response_body = NULL;
rc = cashu_mint_swap(resolved, request_body, &response_body, timeout);
cJSON_Delete(request_body);
if (rc != 0 || !response_body) {
DEBUG_WARN("cashu_wallet_receive_token: cashu_mint_swap failed rc=%d mint=%s", rc, resolved);
for (int i = 0; i < part_count; i++) free(secrets[i]);
free(secrets);
free(blinds);
cashu_mint_free_keyset_keys(&keyset_keys);
cashu_mint_free_info(&info);
cashu_free_decoded_token(&decoded);
int err_rc = receive_token_error(out_json,
"mint_swap",
rc != 0 ? rc : -1,
"mint rejected swap request",
response_body);
cJSON_Delete(response_body);
return err_rc;
}
cashu_swap_response_t parsed;
memset(&parsed, 0, sizeof(parsed));
rc = cashu_parse_swap_response(response_body, &parsed);
if (rc != 0 || parsed.signature_count <= 0 || parsed.signature_count != part_count) {
DEBUG_WARN("cashu_wallet_receive_token: cashu_parse_swap_response failed rc=%d signature_count=%d expected=%d",
rc,
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_keyset_keys(&keyset_keys);
cashu_mint_free_info(&info);
cashu_free_decoded_token(&decoded);
int err_rc = receive_token_error(out_json,
"parse_swap_response",
rc != 0 ? rc : -1,
"invalid swap response from mint",
response_body);
cJSON_Delete(response_body);
return err_rc;
}
cJSON_Delete(response_body);
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) {
DEBUG_WARN("cashu_wallet_receive_token: token allocation failed proof_count=%d", token.proof_count);
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_keyset_keys(&keyset_keys);
cashu_mint_free_info(&info);
cashu_free_decoded_token(&decoded);
return receive_token_error(out_json,
"allocate_token",
NOSTR_ERROR_MEMORY_FAILED,
"failed to allocate received token storage",
NULL);
}
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};
const char* amount_pubkey = NULL;
rc = cashu_keyset_keys_find_pubkey_for_amount(&keyset_keys,
parsed.signatures[i].amount,
&amount_pubkey);
if (rc != 0 || !amount_pubkey || amount_pubkey[0] == '\0') {
DEBUG_WARN("cashu_wallet_receive_token: missing keyset pubkey for signature amount=%llu rc=%d",
(unsigned long long)parsed.signatures[i].amount,
rc);
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_keyset_keys(&keyset_keys);
cashu_mint_free_info(&info);
cashu_free_decoded_token(&decoded);
return receive_token_error(out_json,
"resolve_amount_pubkey",
rc != 0 ? rc : NOSTR_ERROR_CASHU_INVALID_KEYSET,
"mint keyset missing pubkey for signature amount",
NULL);
}
if (cashu_unblind_signature(parsed.signatures[i].C_, amount_pubkey, &blinds[(size_t)i * 32], unblinded_C) != 0) {
DEBUG_WARN("cashu_wallet_receive_token: cashu_unblind_signature failed at index=%d", i);
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_keyset_keys(&keyset_keys);
cashu_mint_free_info(&info);
cashu_free_decoded_token(&decoded);
return receive_token_error(out_json,
"unblind_signature",
-1,
"failed to unblind mint signature",
NULL);
}
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) {
DEBUG_WARN("cashu_wallet_receive_token: proof string allocation failed at index=%d", i);
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_keyset_keys(&keyset_keys);
cashu_mint_free_info(&info);
cashu_free_decoded_token(&decoded);
return receive_token_error(out_json,
"store_unblinded_proofs",
NOSTR_ERROR_MEMORY_FAILED,
"failed to store unblinded proofs",
NULL);
}
}
cashu_mint_free_swap_response(&parsed);
for (int i = 0; i < part_count; i++) free(secrets[i]);
free(secrets);
free(blinds);
cashu_mint_free_keyset_keys(&keyset_keys);
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) {
DEBUG_WARN("cashu_wallet_receive_token: publish_token_event_and_store_locked failed rc=%d", rc);
nostr_nip60_free_token_data(&token);
cashu_free_decoded_token(&decoded);
return receive_token_error(out_json,
"publish_token_event",
rc,
"failed to persist received token event",
NULL);
}
(void)publish_history_event(NOSTR_NIP60_DIRECTION_IN,
incoming_total,
token_event_id,
NOSTR_NIP60_REF_CREATED);
cJSON* root = cJSON_CreateObject();
if (!root) {
DEBUG_WARN("cashu_wallet_receive_token: failed to create success JSON root");
cashu_free_decoded_token(&decoded);
return receive_token_error(out_json,
"build_success_json",
NOSTR_ERROR_MEMORY_FAILED,
"failed to build success response",
NULL);
}
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;
}
static int send_token_error_ex(cJSON** out_json,
const char* step,
int rc,
const char* error,
cJSON* mint_error_json) {
if (out_json) {
cJSON* root = cJSON_CreateObject();
if (root) {
cJSON_AddBoolToObject(root, "success", 0);
cJSON_AddStringToObject(root,
"error",
(error && error[0]) ? error : "cashu_wallet_send_token failed");
if (step && step[0]) {
cJSON_AddStringToObject(root, "step", step);
}
cJSON_AddNumberToObject(root, "code", (double)rc);
if (mint_error_json) {
cJSON* dup = cJSON_Duplicate(mint_error_json, 1);
if (dup) {
cJSON_AddItemToObject(root, "mint_error", dup);
}
cJSON* detail = cJSON_GetObjectItemCaseSensitive(mint_error_json, "detail");
if (detail && cJSON_IsString(detail) && detail->valuestring) {
cJSON_AddStringToObject(root, "mint_error_detail", detail->valuestring);
}
}
*out_json = root;
}
}
return rc;
}
static int send_token_error(cJSON** out_json,
const char* step,
int rc,
const char* error) {
return send_token_error_ex(out_json, step, rc, error, NULL);
}
int cashu_wallet_send_token(const char* mint_url, uint64_t amount, const char* format, cJSON** out_json) {
if (!out_json || amount == 0 || !g_wallet.initialized || !g_wallet.cfg) {
return send_token_error(out_json,
"preconditions",
NOSTR_ERROR_INVALID_INPUT,
"cashu_wallet_send_token preconditions failed");
}
*out_json = NULL;
int ready_rc = ensure_wallet_ready();
if (ready_rc != 0) {
DEBUG_WARN("cashu_wallet_send_token: ensure_wallet_ready failed rc=%d; continuing with explicit mint path", ready_rc);
}
cashu_token_format_t token_format = CASHU_TOKEN_FORMAT_B;
if (format && format[0] != '\0') {
if (strcasecmp(format, "cashuA") == 0 || strcasecmp(format, "A") == 0) {
token_format = CASHU_TOKEN_FORMAT_A;
} else if (strcasecmp(format, "cashuB") == 0 || strcasecmp(format, "B") == 0) {
token_format = CASHU_TOKEN_FORMAT_B;
} else {
return send_token_error(out_json,
"parse_format",
NOSTR_ERROR_INVALID_INPUT,
"format must be one of: cashuA, cashuB, A, B");
}
}
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";
if (!resolved || resolved[0] == '\0') {
pthread_mutex_unlock(&g_wallet.mutex);
return send_token_error(out_json,
"resolve_mint_url",
NOSTR_ERROR_INVALID_INPUT,
"no mint URL available for token send");
}
int mint_token_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;
uint64_t token_total = nostr_nip60_sum_proofs(g_wallet.tokens[i].token.proofs, g_wallet.tokens[i].token.proof_count);
if (token_total == 0) continue;
mint_token_count++;
}
if (mint_token_count <= 0) {
pthread_mutex_unlock(&g_wallet.mutex);
return send_token_error(out_json,
"collect_tokens",
NOSTR_ERROR_NIP60_INSUFFICIENT_FUNDS,
"no token proofs available for requested mint");
}
int* mint_token_indices = (int*)calloc((size_t)mint_token_count, sizeof(int));
nostr_cashu_proof_t* token_totals_as_proofs = (nostr_cashu_proof_t*)calloc((size_t)mint_token_count, sizeof(nostr_cashu_proof_t));
if (!mint_token_indices || !token_totals_as_proofs) {
free(mint_token_indices);
free(token_totals_as_proofs);
pthread_mutex_unlock(&g_wallet.mutex);
return send_token_error(out_json,
"allocate_token_selection",
NOSTR_ERROR_MEMORY_FAILED,
"failed to allocate token selection buffers");
}
int tpos = 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;
uint64_t token_total = nostr_nip60_sum_proofs(g_wallet.tokens[i].token.proofs, g_wallet.tokens[i].token.proof_count);
if (token_total == 0) continue;
mint_token_indices[tpos] = i;
token_totals_as_proofs[tpos].amount = token_total;
tpos++;
}
int* selected_token_pos = (int*)calloc((size_t)mint_token_count, sizeof(int));
uint64_t selected_total = 0;
int selected_token_count = cashu_mint_select_proofs_for_amount(token_totals_as_proofs,
mint_token_count,
amount,
selected_token_pos,
mint_token_count,
&selected_total);
if (selected_token_count <= 0 || selected_total < amount) {
free(selected_token_pos);
free(token_totals_as_proofs);
free(mint_token_indices);
pthread_mutex_unlock(&g_wallet.mutex);
return send_token_error(out_json,
"select_tokens",
NOSTR_ERROR_NIP60_INSUFFICIENT_FUNDS,
"insufficient funds for requested amount");
}
uint64_t change_amount = selected_total - amount;
int* selected_token_flags = (int*)calloc((size_t)g_wallet.token_count, sizeof(int));
if (!selected_token_flags) {
free(selected_token_pos);
free(token_totals_as_proofs);
free(mint_token_indices);
pthread_mutex_unlock(&g_wallet.mutex);
return send_token_error(out_json,
"allocate_selected_flags",
NOSTR_ERROR_MEMORY_FAILED,
"failed to allocate selected token flags");
}
for (int i = 0; i < selected_token_count; i++) {
int pos = selected_token_pos[i];
if (pos >= 0 && pos < mint_token_count) {
selected_token_flags[mint_token_indices[pos]] = 1;
}
}
int input_count = 0;
for (int i = 0; i < g_wallet.token_count; i++) {
if (selected_token_flags[i]) {
input_count += g_wallet.tokens[i].token.proof_count;
}
}
if (input_count <= 0) {
free(selected_token_flags);
free(selected_token_pos);
free(token_totals_as_proofs);
free(mint_token_indices);
pthread_mutex_unlock(&g_wallet.mutex);
return send_token_error(out_json,
"collect_inputs",
NOSTR_ERROR_NIP60_INSUFFICIENT_FUNDS,
"selected tokens contain no spendable proofs");
}
nostr_cashu_proof_t* swap_inputs = (nostr_cashu_proof_t*)calloc((size_t)input_count, sizeof(nostr_cashu_proof_t));
if (!swap_inputs) {
free(selected_token_flags);
free(selected_token_pos);
free(token_totals_as_proofs);
free(mint_token_indices);
pthread_mutex_unlock(&g_wallet.mutex);
return send_token_error(out_json,
"allocate_inputs",
NOSTR_ERROR_MEMORY_FAILED,
"failed to allocate swap input proofs");
}
int in_idx = 0;
for (int i = 0; i < g_wallet.token_count; i++) {
if (!selected_token_flags[i]) continue;
for (int j = 0; j < g_wallet.tokens[i].token.proof_count; j++) {
swap_inputs[in_idx++] = g_wallet.tokens[i].token.proofs[j];
}
}
uint64_t send_parts[CASHU_WALLET_MAX_SPLIT_PARTS] = {0};
int send_part_count = 0;
int rc = cashu_mint_plan_split_amounts(amount,
send_parts,
CASHU_WALLET_MAX_SPLIT_PARTS,
&send_part_count);
if (rc != 0 || send_part_count <= 0) {
free(swap_inputs);
free(selected_token_flags);
free(selected_token_pos);
free(token_totals_as_proofs);
free(mint_token_indices);
pthread_mutex_unlock(&g_wallet.mutex);
return send_token_error(out_json,
"plan_send_split",
rc != 0 ? rc : -1,
"failed to split requested send amount");
}
uint64_t change_parts[CASHU_WALLET_MAX_SPLIT_PARTS] = {0};
int change_part_count = 0;
if (change_amount > 0) {
rc = cashu_mint_plan_split_amounts(change_amount,
change_parts,
CASHU_WALLET_MAX_SPLIT_PARTS,
&change_part_count);
if (rc != 0 || change_part_count <= 0) {
free(swap_inputs);
free(selected_token_flags);
free(selected_token_pos);
free(token_totals_as_proofs);
free(mint_token_indices);
pthread_mutex_unlock(&g_wallet.mutex);
return send_token_error(out_json,
"plan_change_split",
rc != 0 ? rc : -1,
"failed to split change amount");
}
}
int output_count = send_part_count + change_part_count;
if (output_count <= 0 || output_count > CASHU_WALLET_MAX_SPLIT_PARTS) {
free(swap_inputs);
free(selected_token_flags);
free(selected_token_pos);
free(token_totals_as_proofs);
free(mint_token_indices);
pthread_mutex_unlock(&g_wallet.mutex);
return send_token_error(out_json,
"output_count",
NOSTR_ERROR_INVALID_INPUT,
"invalid swap output count");
}
cashu_mint_info_t info;
memset(&info, 0, sizeof(info));
rc = cashu_mint_get_info(resolved, &info, timeout);
if (rc != 0) {
free(swap_inputs);
free(selected_token_flags);
free(selected_token_pos);
free(token_totals_as_proofs);
free(mint_token_indices);
pthread_mutex_unlock(&g_wallet.mutex);
return send_token_error(out_json,
"mint_get_info",
rc,
"failed to query mint info");
}
cashu_keyset_t keyset;
memset(&keyset, 0, sizeof(keyset));
rc = cashu_mint_get_active_keyset(&info, unit, &keyset);
if (rc != 0) {
cashu_mint_free_info(&info);
free(swap_inputs);
free(selected_token_flags);
free(selected_token_pos);
free(token_totals_as_proofs);
free(mint_token_indices);
pthread_mutex_unlock(&g_wallet.mutex);
return send_token_error(out_json,
"get_active_keyset",
rc,
"mint has no usable active keyset for requested unit");
}
cashu_keyset_keys_t keyset_keys;
memset(&keyset_keys, 0, sizeof(keyset_keys));
rc = cashu_mint_get_keys(resolved, keyset.id, &keyset_keys, timeout);
if (rc != 0) {
cashu_mint_free_info(&info);
free(swap_inputs);
free(selected_token_flags);
free(selected_token_pos);
free(token_totals_as_proofs);
free(mint_token_indices);
pthread_mutex_unlock(&g_wallet.mutex);
return send_token_error(out_json,
"get_keyset_keys",
rc,
"failed to load mint keyset keys");
}
cashu_blinded_output_t* outputs = (cashu_blinded_output_t*)calloc((size_t)output_count, sizeof(cashu_blinded_output_t));
unsigned char* blinds = (unsigned char*)calloc((size_t)output_count, 32);
char** secrets = (char**)calloc((size_t)output_count, sizeof(char*));
if (!outputs || !blinds || !secrets) {
free(outputs);
free(blinds);
free(secrets);
cashu_mint_free_keyset_keys(&keyset_keys);
cashu_mint_free_info(&info);
free(swap_inputs);
free(selected_token_flags);
free(selected_token_pos);
free(token_totals_as_proofs);
free(mint_token_indices);
pthread_mutex_unlock(&g_wallet.mutex);
return send_token_error(out_json,
"allocate_outputs",
NOSTR_ERROR_MEMORY_FAILED,
"failed to allocate swap outputs");
}
int out_i = 0;
for (int i = 0; i < send_part_count; i++, out_i++) {
snprintf(outputs[out_i].id, sizeof(outputs[out_i].id), "%s", keyset.id);
outputs[out_i].amount = send_parts[i];
}
for (int i = 0; i < change_part_count; i++, out_i++) {
snprintf(outputs[out_i].id, sizeof(outputs[out_i].id), "%s", keyset.id);
outputs[out_i].amount = change_parts[i];
}
rc = 0;
for (int i = 0; i < output_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 = NOSTR_ERROR_MEMORY_FAILED;
break;
}
nostr_bytes_to_hex(rnd_priv, 32, secret);
const char* amount_pubkey = NULL;
rc = cashu_keyset_keys_find_pubkey_for_amount(&keyset_keys,
outputs[i].amount,
&amount_pubkey);
if (rc != 0 || !amount_pubkey || amount_pubkey[0] == '\0') {
free(secret);
rc = (rc != 0) ? rc : NOSTR_ERROR_CASHU_INVALID_KEYSET;
break;
}
if (cashu_blind_message(secret,
amount_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 < output_count; i++) free(secrets[i]);
free(secrets);
free(blinds);
free(outputs);
cashu_mint_free_keyset_keys(&keyset_keys);
cashu_mint_free_info(&info);
free(swap_inputs);
free(selected_token_flags);
free(selected_token_pos);
free(token_totals_as_proofs);
free(mint_token_indices);
pthread_mutex_unlock(&g_wallet.mutex);
return send_token_error(out_json,
"prepare_outputs",
rc,
"failed to prepare swap outputs");
}
cashu_swap_request_t swap_req;
memset(&swap_req, 0, sizeof(swap_req));
swap_req.inputs = swap_inputs;
swap_req.input_count = input_count;
swap_req.outputs = outputs;
swap_req.output_count = output_count;
cJSON* request_body = NULL;
rc = cashu_build_swap_request(&swap_req, &request_body);
if (request_body) {
char* request_json = cJSON_PrintUnformatted(request_body);
if (request_json) {
DEBUG_WARN("cashu_wallet_send_token: swap request mint=%s inputs=%d outputs=%d body=%s",
resolved,
input_count,
output_count,
request_json);
free(request_json);
}
}
if (rc != 0 || !request_body) {
for (int i = 0; i < output_count; i++) free(secrets[i]);
free(secrets);
free(blinds);
free(outputs);
cashu_mint_free_keyset_keys(&keyset_keys);
cashu_mint_free_info(&info);
free(swap_inputs);
free(selected_token_flags);
free(selected_token_pos);
free(token_totals_as_proofs);
free(mint_token_indices);
pthread_mutex_unlock(&g_wallet.mutex);
cJSON_Delete(request_body);
return send_token_error(out_json,
"build_swap_request",
rc != 0 ? rc : -1,
"failed to build swap request");
}
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 < output_count; i++) free(secrets[i]);
free(secrets);
free(blinds);
free(outputs);
cashu_mint_free_keyset_keys(&keyset_keys);
cashu_mint_free_info(&info);
free(swap_inputs);
free(selected_token_flags);
free(selected_token_pos);
free(token_totals_as_proofs);
free(mint_token_indices);
pthread_mutex_unlock(&g_wallet.mutex);
int err_rc = send_token_error_ex(out_json,
"mint_swap",
rc != 0 ? rc : -1,
"mint rejected swap request",
response_body);
cJSON_Delete(response_body);
return err_rc;
}
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 != output_count) {
cashu_mint_free_swap_response(&parsed);
for (int i = 0; i < output_count; i++) free(secrets[i]);
free(secrets);
free(blinds);
free(outputs);
cashu_mint_free_keyset_keys(&keyset_keys);
cashu_mint_free_info(&info);
free(swap_inputs);
free(selected_token_flags);
free(selected_token_pos);
free(token_totals_as_proofs);
free(mint_token_indices);
pthread_mutex_unlock(&g_wallet.mutex);
return send_token_error(out_json,
"parse_swap_response",
rc != 0 ? rc : -1,
"invalid swap response from mint");
}
cashu_decoded_token_t outgoing;
memset(&outgoing, 0, sizeof(outgoing));
outgoing.mint_url = strdup(resolved);
outgoing.proof_count = send_part_count;
outgoing.proofs = (nostr_cashu_proof_t*)calloc((size_t)send_part_count, sizeof(nostr_cashu_proof_t));
nostr_nip60_token_data_t change_token;
memset(&change_token, 0, sizeof(change_token));
if (change_part_count > 0) {
change_token.mint_url = strdup(resolved);
change_token.proof_count = change_part_count;
change_token.proofs = (nostr_cashu_proof_t*)calloc((size_t)change_part_count, sizeof(nostr_cashu_proof_t));
}
if (!outgoing.mint_url || !outgoing.proofs ||
(change_part_count > 0 && (!change_token.mint_url || !change_token.proofs))) {
cashu_mint_free_swap_response(&parsed);
for (int i = 0; i < output_count; i++) free(secrets[i]);
free(secrets);
free(blinds);
free(outputs);
cashu_mint_free_keyset_keys(&keyset_keys);
cashu_mint_free_info(&info);
free(swap_inputs);
free(selected_token_flags);
free(selected_token_pos);
free(token_totals_as_proofs);
free(mint_token_indices);
cashu_free_decoded_token(&outgoing);
nostr_nip60_free_token_data(&change_token);
pthread_mutex_unlock(&g_wallet.mutex);
return send_token_error(out_json,
"allocate_outgoing_or_change",
NOSTR_ERROR_MEMORY_FAILED,
"failed to allocate outgoing/change proofs");
}
for (int i = 0; i < output_count; i++) {
char unblinded_C[67] = {0};
const char* amount_pubkey = NULL;
rc = cashu_keyset_keys_find_pubkey_for_amount(&keyset_keys,
parsed.signatures[i].amount,
&amount_pubkey);
if (rc != 0 || !amount_pubkey || amount_pubkey[0] == '\0' ||
cashu_unblind_signature(parsed.signatures[i].C_,
amount_pubkey,
&blinds[(size_t)i * 32],
unblinded_C) != 0) {
cashu_mint_free_swap_response(&parsed);
for (int j = 0; j < output_count; j++) free(secrets[j]);
free(secrets);
free(blinds);
free(outputs);
cashu_mint_free_keyset_keys(&keyset_keys);
cashu_mint_free_info(&info);
free(swap_inputs);
free(selected_token_flags);
free(selected_token_pos);
free(token_totals_as_proofs);
free(mint_token_indices);
cashu_free_decoded_token(&outgoing);
nostr_nip60_free_token_data(&change_token);
pthread_mutex_unlock(&g_wallet.mutex);
return send_token_error(out_json,
"unblind_signature",
rc != 0 ? rc : -1,
"failed to unblind swap signature");
}
nostr_cashu_proof_t* dst = NULL;
int dst_idx = 0;
if (i < send_part_count) {
dst = &outgoing.proofs[i];
dst_idx = i;
} else {
dst_idx = i - send_part_count;
dst = &change_token.proofs[dst_idx];
}
snprintf(dst->id, sizeof(dst->id), "%s", parsed.signatures[i].id);
dst->amount = parsed.signatures[i].amount;
dst->secret = secrets[i] ? strdup(secrets[i]) : NULL;
dst->C = strdup(unblinded_C);
if (!dst->secret || !dst->C) {
cashu_mint_free_swap_response(&parsed);
for (int j = 0; j < output_count; j++) free(secrets[j]);
free(secrets);
free(blinds);
free(outputs);
cashu_mint_free_keyset_keys(&keyset_keys);
cashu_mint_free_info(&info);
free(swap_inputs);
free(selected_token_flags);
free(selected_token_pos);
free(token_totals_as_proofs);
free(mint_token_indices);
cashu_free_decoded_token(&outgoing);
nostr_nip60_free_token_data(&change_token);
pthread_mutex_unlock(&g_wallet.mutex);
return send_token_error(out_json,
"store_unblinded",
NOSTR_ERROR_MEMORY_FAILED,
"failed to store unblinded proofs");
}
(void)dst_idx;
}
cashu_mint_free_swap_response(&parsed);
for (int i = 0; i < output_count; i++) free(secrets[i]);
free(secrets);
free(blinds);
free(outputs);
cashu_mint_free_keyset_keys(&keyset_keys);
cashu_mint_free_info(&info);
free(swap_inputs);
free(selected_token_pos);
free(token_totals_as_proofs);
free(mint_token_indices);
char* token_string = NULL;
rc = cashu_encode_token(&outgoing, token_format, &token_string);
if (rc != 0 || !token_string) {
free(token_string);
free(selected_token_flags);
cashu_free_decoded_token(&outgoing);
nostr_nip60_free_token_data(&change_token);
pthread_mutex_unlock(&g_wallet.mutex);
return send_token_error(out_json,
"encode_token",
rc != 0 ? rc : -1,
"failed to encode outgoing token");
}
char change_event_id[65] = {0};
if (change_token.proof_count > 0) {
rc = publish_token_event_and_store_locked(&change_token, change_event_id);
if (rc != 0) {
free(token_string);
free(selected_token_flags);
cashu_free_decoded_token(&outgoing);
nostr_nip60_free_token_data(&change_token);
pthread_mutex_unlock(&g_wallet.mutex);
return send_token_error(out_json,
"publish_change_token",
rc,
"failed to persist change token event");
}
}
int deleted_count = 0;
for (int i = 0; i < g_wallet.token_count; i++) {
if (!selected_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);
pthread_mutex_unlock(&g_wallet.mutex);
(void)publish_history_event(NOSTR_NIP60_DIRECTION_OUT,
amount,
NULL,
NOSTR_NIP60_REF_DESTROYED);
cJSON* root = cJSON_CreateObject();
if (!root) {
free(token_string);
free(selected_token_flags);
cashu_free_decoded_token(&outgoing);
nostr_nip60_free_token_data(&change_token);
return send_token_error(out_json,
"build_success_json",
NOSTR_ERROR_MEMORY_FAILED,
"failed to build send-token success response");
}
cJSON_AddBoolToObject(root, "success", 1);
cJSON_AddStringToObject(root, "mint", resolved);
cJSON_AddNumberToObject(root, "sent_amount", (double)amount);
cJSON_AddNumberToObject(root, "selected_total", (double)selected_total);
cJSON_AddNumberToObject(root, "change_amount", (double)change_amount);
cJSON_AddStringToObject(root, "format", token_format == CASHU_TOKEN_FORMAT_A ? "cashuA" : "cashuB");
cJSON_AddStringToObject(root, "token", token_string);
cJSON_AddNumberToObject(root, "send_proofs", outgoing.proof_count);
cJSON_AddNumberToObject(root, "change_proofs", change_token.proof_count);
cJSON_AddNumberToObject(root, "deleted_token_events", deleted_count);
cJSON_AddNumberToObject(root, "remaining_balance", (double)remaining);
if (change_event_id[0] != '\0') {
cJSON_AddStringToObject(root, "change_token_event_id", change_event_id);
}
*out_json = root;
free(token_string);
free(selected_token_flags);
cashu_free_decoded_token(&outgoing);
nostr_nip60_free_token_data(&change_token);
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;
}