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