2779 lines
103 KiB
C
2779 lines
103 KiB
C
#define _POSIX_C_SOURCE 200809L
|
|
|
|
#include "cashu_wallet.h"
|
|
|
|
#include <pthread.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <strings.h>
|
|
#include <time.h>
|
|
#include <stdbool.h>
|
|
|
|
|
|
#include "debug.h"
|
|
#include "nostr_handler.h"
|
|
#include "nostr_block_list.h"
|
|
#include "../../nostr_core_lib/nostr_core/nostr_core.h"
|
|
|
|
#define CASHU_WALLET_QUERY_TIMEOUT_MS 6000
|
|
#define CASHU_WALLET_MAX_SPLIT_PARTS 64
|
|
|
|
typedef struct {
|
|
char event_id[65];
|
|
nostr_nip60_token_data_t token;
|
|
} wallet_token_entry_t;
|
|
|
|
/* private secp256k1 wrappers from nostr_core_lib utils.c */
|
|
typedef struct {
|
|
unsigned char data[64];
|
|
} nostr_secp256k1_pubkey;
|
|
|
|
int nostr_secp256k1_ec_pubkey_parse(nostr_secp256k1_pubkey* pubkey, const unsigned char* input, size_t inputlen);
|
|
|
|
typedef struct {
|
|
didactyl_config_t* cfg;
|
|
int initialized;
|
|
int wallet_loaded;
|
|
pthread_mutex_t mutex;
|
|
|
|
nostr_nip60_wallet_data_t wallet;
|
|
wallet_token_entry_t* tokens;
|
|
int token_count;
|
|
} cashu_wallet_state_t;
|
|
|
|
static cashu_wallet_state_t g_wallet = {0};
|
|
|
|
|
|
static void wallet_clear_tokens_locked(void) {
|
|
if (!g_wallet.tokens) {
|
|
g_wallet.token_count = 0;
|
|
return;
|
|
}
|
|
for (int i = 0; i < g_wallet.token_count; i++) {
|
|
nostr_nip60_free_token_data(&g_wallet.tokens[i].token);
|
|
}
|
|
free(g_wallet.tokens);
|
|
g_wallet.tokens = NULL;
|
|
g_wallet.token_count = 0;
|
|
}
|
|
|
|
static void wallet_clear_all_locked(void) {
|
|
wallet_clear_tokens_locked();
|
|
nostr_nip60_free_wallet_data(&g_wallet.wallet);
|
|
memset(&g_wallet.wallet, 0, sizeof(g_wallet.wallet));
|
|
g_wallet.wallet_loaded = 0;
|
|
}
|
|
|
|
static int wallet_append_token_locked(const char* event_id, nostr_nip60_token_data_t* token) {
|
|
if (!token) return -1;
|
|
|
|
wallet_token_entry_t* grown =
|
|
(wallet_token_entry_t*)realloc(g_wallet.tokens, (size_t)(g_wallet.token_count + 1) * sizeof(wallet_token_entry_t));
|
|
if (!grown) {
|
|
return -1;
|
|
}
|
|
|
|
g_wallet.tokens = grown;
|
|
wallet_token_entry_t* dst = &g_wallet.tokens[g_wallet.token_count];
|
|
memset(dst, 0, sizeof(*dst));
|
|
if (event_id && event_id[0] != '\0') {
|
|
snprintf(dst->event_id, sizeof(dst->event_id), "%s", event_id);
|
|
}
|
|
dst->token = *token;
|
|
memset(token, 0, sizeof(*token));
|
|
|
|
g_wallet.token_count++;
|
|
return 0;
|
|
}
|
|
|
|
static int publish_from_signed_event_template(cJSON* ev_template, char out_event_id[65]) {
|
|
if (!ev_template) return -1;
|
|
|
|
cJSON* kind = cJSON_GetObjectItemCaseSensitive(ev_template, "kind");
|
|
cJSON* content = cJSON_GetObjectItemCaseSensitive(ev_template, "content");
|
|
cJSON* tags = cJSON_GetObjectItemCaseSensitive(ev_template, "tags");
|
|
|
|
if (!kind || !cJSON_IsNumber(kind) || !content || !cJSON_IsString(content)) {
|
|
return -1;
|
|
}
|
|
|
|
cJSON* tags_dup = tags ? cJSON_Duplicate(tags, 1) : cJSON_CreateArray();
|
|
if (!tags_dup) return -1;
|
|
|
|
nostr_publish_result_t result;
|
|
memset(&result, 0, sizeof(result));
|
|
int rc = nostr_handler_publish_kind_event((int)kind->valuedouble,
|
|
content->valuestring ? content->valuestring : "",
|
|
tags_dup,
|
|
&result);
|
|
cJSON_Delete(tags_dup);
|
|
if (rc != 0) {
|
|
nostr_handler_publish_result_free(&result);
|
|
return -1;
|
|
}
|
|
|
|
if (out_event_id) {
|
|
snprintf(out_event_id, 65, "%s", result.event_id);
|
|
}
|
|
nostr_handler_publish_result_free(&result);
|
|
return 0;
|
|
}
|
|
|
|
static int publish_wallet_event_and_store_locked(const nostr_nip60_wallet_data_t* wallet_data, char out_event_id[65]) {
|
|
cJSON* ev = nostr_nip60_create_wallet_event(wallet_data, g_wallet.cfg->keys.private_key, time(NULL));
|
|
if (!ev) return -1;
|
|
|
|
int rc = publish_from_signed_event_template(ev, out_event_id);
|
|
cJSON_Delete(ev);
|
|
if (rc != 0) return -1;
|
|
|
|
nostr_nip60_free_wallet_data(&g_wallet.wallet);
|
|
memset(&g_wallet.wallet, 0, sizeof(g_wallet.wallet));
|
|
|
|
g_wallet.wallet.privkey[0] = '\0';
|
|
snprintf(g_wallet.wallet.privkey, sizeof(g_wallet.wallet.privkey), "%s", wallet_data->privkey);
|
|
g_wallet.wallet.mint_count = wallet_data->mint_count;
|
|
if (wallet_data->mint_count > 0) {
|
|
g_wallet.wallet.mint_urls = (char**)calloc((size_t)wallet_data->mint_count, sizeof(char*));
|
|
if (!g_wallet.wallet.mint_urls) {
|
|
return -1;
|
|
}
|
|
for (int i = 0; i < wallet_data->mint_count; i++) {
|
|
g_wallet.wallet.mint_urls[i] = strdup(wallet_data->mint_urls[i] ? wallet_data->mint_urls[i] : "");
|
|
if (!g_wallet.wallet.mint_urls[i]) {
|
|
nostr_nip60_free_wallet_data(&g_wallet.wallet);
|
|
memset(&g_wallet.wallet, 0, sizeof(g_wallet.wallet));
|
|
return -1;
|
|
}
|
|
}
|
|
}
|
|
|
|
g_wallet.wallet_loaded = 1;
|
|
return 0;
|
|
}
|
|
|
|
static int publish_token_event_and_store_locked(nostr_nip60_token_data_t* token_data, char out_event_id[65]) {
|
|
if (!token_data) return -1;
|
|
|
|
cJSON* ev = nostr_nip60_create_token_event(token_data, g_wallet.cfg->keys.private_key, time(NULL));
|
|
if (!ev) return -1;
|
|
|
|
char event_id[65] = {0};
|
|
int rc = publish_from_signed_event_template(ev, event_id);
|
|
cJSON_Delete(ev);
|
|
if (rc != 0) {
|
|
return -1;
|
|
}
|
|
|
|
if (wallet_append_token_locked(event_id, token_data) != 0) {
|
|
return -1;
|
|
}
|
|
|
|
if (out_event_id) {
|
|
snprintf(out_event_id, 65, "%s", event_id);
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static int publish_history_event(nostr_nip60_direction_t direction,
|
|
uint64_t amount,
|
|
const char* primary_event_id,
|
|
nostr_nip60_ref_marker_t marker) {
|
|
nostr_nip60_history_ref_t ref;
|
|
memset(&ref, 0, sizeof(ref));
|
|
if (primary_event_id && primary_event_id[0] != '\0') {
|
|
snprintf(ref.event_id, sizeof(ref.event_id), "%s", primary_event_id);
|
|
}
|
|
ref.marker = marker;
|
|
|
|
nostr_nip60_history_data_t history;
|
|
memset(&history, 0, sizeof(history));
|
|
history.direction = direction;
|
|
history.amount = amount;
|
|
history.refs = &ref;
|
|
history.ref_count = (ref.event_id[0] != '\0') ? 1 : 0;
|
|
|
|
cJSON* ev = nostr_nip60_create_history_event(&history, g_wallet.cfg->keys.private_key, time(NULL));
|
|
if (!ev) return -1;
|
|
|
|
int rc = publish_from_signed_event_template(ev, NULL);
|
|
cJSON_Delete(ev);
|
|
return rc;
|
|
}
|
|
|
|
static int publish_token_deletion_event(const char* event_id) {
|
|
cJSON* del = nostr_nip60_create_token_deletion(event_id, g_wallet.cfg->keys.private_key, time(NULL));
|
|
if (!del) return -1;
|
|
int rc = publish_from_signed_event_template(del, NULL);
|
|
cJSON_Delete(del);
|
|
return rc;
|
|
}
|
|
|
|
static const char* resolve_mint_url_locked(const char* mint_url) {
|
|
if (mint_url && mint_url[0] != '\0') return mint_url;
|
|
|
|
if (g_wallet.wallet_loaded && g_wallet.wallet.mint_count > 0 && g_wallet.wallet.mint_urls && g_wallet.wallet.mint_urls[0]) {
|
|
return g_wallet.wallet.mint_urls[0];
|
|
}
|
|
|
|
if (g_wallet.cfg && g_wallet.cfg->cashu_wallet.mint_count > 0 && g_wallet.cfg->cashu_wallet.mint_urls && g_wallet.cfg->cashu_wallet.mint_urls[0]) {
|
|
return g_wallet.cfg->cashu_wallet.mint_urls[0];
|
|
}
|
|
|
|
return NULL;
|
|
}
|
|
|
|
static int token_proofs_total_for_mint_locked(const char* mint_url, uint64_t* out_total) {
|
|
if (!out_total) return -1;
|
|
uint64_t total = 0;
|
|
for (int i = 0; i < g_wallet.token_count; i++) {
|
|
const wallet_token_entry_t* entry = &g_wallet.tokens[i];
|
|
if (!entry->token.mint_url) continue;
|
|
if (mint_url && mint_url[0] != '\0' && strcmp(entry->token.mint_url, mint_url) != 0) continue;
|
|
total += nostr_nip60_sum_proofs(entry->token.proofs, entry->token.proof_count);
|
|
}
|
|
*out_total = total;
|
|
return 0;
|
|
}
|
|
|
|
static int dup_string_array(const char** src, int count, char*** out_arr, int* out_count) {
|
|
if (!out_arr || !out_count) return -1;
|
|
*out_arr = NULL;
|
|
*out_count = 0;
|
|
if (!src || count <= 0) return 0;
|
|
|
|
char** arr = (char**)calloc((size_t)count, sizeof(char*));
|
|
if (!arr) return -1;
|
|
|
|
int n = 0;
|
|
for (int i = 0; i < count; i++) {
|
|
if (!src[i] || src[i][0] == '\0') continue;
|
|
arr[n] = strdup(src[i]);
|
|
if (!arr[n]) {
|
|
for (int j = 0; j < n; j++) free(arr[j]);
|
|
free(arr);
|
|
return -1;
|
|
}
|
|
n++;
|
|
}
|
|
|
|
*out_arr = arr;
|
|
*out_count = n;
|
|
return 0;
|
|
}
|
|
|
|
static int build_nutzap_info_from_mints_locked(const char** mint_urls,
|
|
int mint_count,
|
|
nostr_nip61_nutzap_info_t* out_info) {
|
|
if (!g_wallet.cfg || !out_info) return -1;
|
|
if (!g_wallet.wallet_loaded || g_wallet.wallet.privkey[0] == '\0') return -1;
|
|
|
|
memset(out_info, 0, sizeof(*out_info));
|
|
|
|
if (g_wallet.cfg->relay_count > 0 && g_wallet.cfg->relays) {
|
|
out_info->relay_urls = (char**)calloc((size_t)g_wallet.cfg->relay_count, sizeof(char*));
|
|
if (!out_info->relay_urls) return -1;
|
|
|
|
for (int i = 0; i < g_wallet.cfg->relay_count; i++) {
|
|
out_info->relay_urls[i] = strdup(g_wallet.cfg->relays[i] ? g_wallet.cfg->relays[i] : "");
|
|
if (!out_info->relay_urls[i]) {
|
|
nostr_nip61_free_nutzap_info(out_info);
|
|
return -1;
|
|
}
|
|
out_info->relay_count++;
|
|
}
|
|
}
|
|
|
|
out_info->mints = (nostr_nip61_mint_entry_t*)calloc((size_t)mint_count, sizeof(nostr_nip61_mint_entry_t));
|
|
if (!out_info->mints) {
|
|
nostr_nip61_free_nutzap_info(out_info);
|
|
return -1;
|
|
}
|
|
|
|
for (int i = 0; i < mint_count; i++) {
|
|
out_info->mints[i].url = strdup(mint_urls[i]);
|
|
if (!out_info->mints[i].url) {
|
|
nostr_nip61_free_nutzap_info(out_info);
|
|
return -1;
|
|
}
|
|
|
|
out_info->mints[i].units = (char**)calloc(1, sizeof(char*));
|
|
if (!out_info->mints[i].units) {
|
|
nostr_nip61_free_nutzap_info(out_info);
|
|
return -1;
|
|
}
|
|
out_info->mints[i].units[0] = strdup(g_wallet.cfg->cashu_wallet.unit[0] ? g_wallet.cfg->cashu_wallet.unit : "sat");
|
|
if (!out_info->mints[i].units[0]) {
|
|
nostr_nip61_free_nutzap_info(out_info);
|
|
return -1;
|
|
}
|
|
out_info->mints[i].unit_count = 1;
|
|
out_info->mint_count++;
|
|
}
|
|
|
|
unsigned char wallet_priv[32] = {0};
|
|
unsigned char wallet_pub[32] = {0};
|
|
if (nostr_hex_to_bytes(g_wallet.wallet.privkey, wallet_priv, 32) != 0 ||
|
|
nostr_ec_public_key_from_private_key(wallet_priv, wallet_pub) != 0) {
|
|
nostr_nip61_free_nutzap_info(out_info);
|
|
return -1;
|
|
}
|
|
|
|
nostr_bytes_to_hex(wallet_pub, 32, out_info->pubkey);
|
|
return 0;
|
|
}
|
|
|
|
static int load_latest_public_nutzap_info(nostr_nip61_nutzap_info_t* out_info) {
|
|
if (!out_info || !g_wallet.cfg) return -1;
|
|
memset(out_info, 0, sizeof(*out_info));
|
|
|
|
cJSON* filter = cJSON_CreateObject();
|
|
cJSON* kinds = cJSON_CreateArray();
|
|
cJSON* authors = cJSON_CreateArray();
|
|
if (!filter || !kinds || !authors) {
|
|
cJSON_Delete(filter);
|
|
cJSON_Delete(kinds);
|
|
cJSON_Delete(authors);
|
|
return -1;
|
|
}
|
|
|
|
cJSON_AddItemToArray(kinds, cJSON_CreateNumber((double)NOSTR_NIP61_NUTZAP_INFO_KIND));
|
|
cJSON_AddItemToArray(authors, cJSON_CreateString(g_wallet.cfg->keys.public_key_hex));
|
|
cJSON_AddItemToObject(filter, "kinds", kinds);
|
|
cJSON_AddItemToObject(filter, "authors", authors);
|
|
cJSON_AddNumberToObject(filter, "limit", 20);
|
|
|
|
char* events_json = nostr_handler_query_json(filter, CASHU_WALLET_QUERY_TIMEOUT_MS);
|
|
cJSON_Delete(filter);
|
|
if (!events_json) return -1;
|
|
|
|
cJSON* arr = cJSON_Parse(events_json);
|
|
free(events_json);
|
|
if (!arr || !cJSON_IsArray(arr)) {
|
|
cJSON_Delete(arr);
|
|
return -1;
|
|
}
|
|
|
|
int found = 0;
|
|
double newest_created_at = -1;
|
|
nostr_nip61_nutzap_info_t newest;
|
|
memset(&newest, 0, sizeof(newest));
|
|
|
|
int n = cJSON_GetArraySize(arr);
|
|
for (int i = 0; i < n; i++) {
|
|
cJSON* ev = cJSON_GetArrayItem(arr, i);
|
|
cJSON* created_at = ev ? cJSON_GetObjectItemCaseSensitive(ev, "created_at") : NULL;
|
|
if (!created_at || !cJSON_IsNumber(created_at)) continue;
|
|
|
|
nostr_nip61_nutzap_info_t parsed;
|
|
memset(&parsed, 0, sizeof(parsed));
|
|
if (nostr_nip61_parse_nutzap_info_event(ev, &parsed) != 0) continue;
|
|
|
|
if (!found || created_at->valuedouble > newest_created_at) {
|
|
nostr_nip61_free_nutzap_info(&newest);
|
|
newest = parsed;
|
|
newest_created_at = created_at->valuedouble;
|
|
found = 1;
|
|
} else {
|
|
nostr_nip61_free_nutzap_info(&parsed);
|
|
}
|
|
}
|
|
|
|
cJSON_Delete(arr);
|
|
if (!found) return -1;
|
|
|
|
*out_info = newest;
|
|
return 0;
|
|
}
|
|
|
|
int cashu_wallet_init(didactyl_config_t* cfg) {
|
|
if (!cfg) return -1;
|
|
|
|
if (!g_wallet.initialized) {
|
|
if (pthread_mutex_init(&g_wallet.mutex, NULL) != 0) {
|
|
return -1;
|
|
}
|
|
g_wallet.initialized = 1;
|
|
}
|
|
|
|
const char* ca_bundle = nostr_http_detect_ca_bundle();
|
|
cashu_mint_set_ca_bundle(ca_bundle);
|
|
|
|
pthread_mutex_lock(&g_wallet.mutex);
|
|
g_wallet.cfg = cfg;
|
|
wallet_clear_all_locked();
|
|
pthread_mutex_unlock(&g_wallet.mutex);
|
|
|
|
return 0;
|
|
}
|
|
|
|
void cashu_wallet_cleanup(void) {
|
|
if (!g_wallet.initialized) return;
|
|
|
|
pthread_mutex_lock(&g_wallet.mutex);
|
|
wallet_clear_all_locked();
|
|
g_wallet.cfg = NULL;
|
|
pthread_mutex_unlock(&g_wallet.mutex);
|
|
|
|
pthread_mutex_destroy(&g_wallet.mutex);
|
|
memset(&g_wallet, 0, sizeof(g_wallet));
|
|
}
|
|
|
|
static int ensure_wallet_ready(void) {
|
|
if (!g_wallet.initialized || !g_wallet.cfg) return -1;
|
|
|
|
int wallet_loaded = 0;
|
|
int auto_load = 0;
|
|
pthread_mutex_lock(&g_wallet.mutex);
|
|
wallet_loaded = g_wallet.wallet_loaded;
|
|
auto_load = g_wallet.cfg->cashu_wallet.auto_load ? 1 : 0;
|
|
pthread_mutex_unlock(&g_wallet.mutex);
|
|
|
|
if (wallet_loaded) return 0;
|
|
|
|
int rc = 0;
|
|
if (auto_load) {
|
|
rc = cashu_wallet_load_from_relays();
|
|
if (rc == 0) return 0;
|
|
}
|
|
|
|
rc = cashu_wallet_create_new_from_config();
|
|
if (rc == 0) return 0;
|
|
|
|
return rc;
|
|
}
|
|
|
|
int cashu_wallet_has_default_mint(void) {
|
|
if (!g_wallet.initialized || !g_wallet.cfg) return 0;
|
|
if (ensure_wallet_ready() != 0) return 0;
|
|
|
|
int has_default = 0;
|
|
pthread_mutex_lock(&g_wallet.mutex);
|
|
has_default = (g_wallet.wallet_loaded &&
|
|
g_wallet.wallet.mint_count > 0 &&
|
|
g_wallet.wallet.mint_urls &&
|
|
g_wallet.wallet.mint_urls[0] &&
|
|
g_wallet.wallet.mint_urls[0][0] != '\0');
|
|
pthread_mutex_unlock(&g_wallet.mutex);
|
|
|
|
return has_default;
|
|
}
|
|
|
|
int cashu_wallet_create_new_from_config(void) {
|
|
if (!g_wallet.initialized || !g_wallet.cfg) return -1;
|
|
|
|
pthread_mutex_lock(&g_wallet.mutex);
|
|
|
|
unsigned char priv[32] = {0};
|
|
unsigned char pub[32] = {0};
|
|
if (nostr_generate_keypair(priv, pub) != NOSTR_SUCCESS) {
|
|
pthread_mutex_unlock(&g_wallet.mutex);
|
|
return -1;
|
|
}
|
|
|
|
nostr_nip60_wallet_data_t wallet_data;
|
|
memset(&wallet_data, 0, sizeof(wallet_data));
|
|
nostr_bytes_to_hex(priv, 32, wallet_data.privkey);
|
|
|
|
wallet_data.mint_count = g_wallet.cfg->cashu_wallet.mint_count;
|
|
if (wallet_data.mint_count > 0) {
|
|
wallet_data.mint_urls = (char**)calloc((size_t)wallet_data.mint_count, sizeof(char*));
|
|
if (!wallet_data.mint_urls) {
|
|
pthread_mutex_unlock(&g_wallet.mutex);
|
|
return -1;
|
|
}
|
|
|
|
for (int i = 0; i < wallet_data.mint_count; i++) {
|
|
wallet_data.mint_urls[i] = strdup(g_wallet.cfg->cashu_wallet.mint_urls[i]);
|
|
if (!wallet_data.mint_urls[i]) {
|
|
nostr_nip60_free_wallet_data(&wallet_data);
|
|
pthread_mutex_unlock(&g_wallet.mutex);
|
|
return -1;
|
|
}
|
|
}
|
|
}
|
|
|
|
int rc = publish_wallet_event_and_store_locked(&wallet_data, NULL);
|
|
nostr_nip60_free_wallet_data(&wallet_data);
|
|
|
|
pthread_mutex_unlock(&g_wallet.mutex);
|
|
return rc;
|
|
}
|
|
|
|
static int deleted_id_index(char** deleted_ids, int deleted_count, const char* event_id) {
|
|
if (!deleted_ids || deleted_count <= 0 || !event_id || event_id[0] == '\0') return -1;
|
|
for (int i = 0; i < deleted_count; i++) {
|
|
if (deleted_ids[i] && strcmp(deleted_ids[i], event_id) == 0) {
|
|
return i;
|
|
}
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
static int deleted_id_add(char*** deleted_ids, int* deleted_count, const char* event_id) {
|
|
if (!deleted_ids || !deleted_count || !event_id || event_id[0] == '\0') return -1;
|
|
if (deleted_id_index(*deleted_ids, *deleted_count, event_id) >= 0) return 0;
|
|
|
|
char** grown = (char**)realloc(*deleted_ids, (size_t)(*deleted_count + 1) * sizeof(char*));
|
|
if (!grown) return -1;
|
|
|
|
grown[*deleted_count] = strdup(event_id);
|
|
if (!grown[*deleted_count]) {
|
|
return -1;
|
|
}
|
|
|
|
*deleted_ids = grown;
|
|
(*deleted_count)++;
|
|
return 0;
|
|
}
|
|
|
|
static void deleted_id_list_free(char** deleted_ids, int deleted_count) {
|
|
if (!deleted_ids) return;
|
|
for (int i = 0; i < deleted_count; i++) {
|
|
free(deleted_ids[i]);
|
|
}
|
|
free(deleted_ids);
|
|
}
|
|
|
|
static void collect_deleted_ids_from_event(cJSON* ev, char*** deleted_ids, int* deleted_count) {
|
|
if (!ev || !deleted_ids || !deleted_count) return;
|
|
|
|
cJSON* kind = cJSON_GetObjectItemCaseSensitive(ev, "kind");
|
|
if (!kind || !cJSON_IsNumber(kind)) return;
|
|
|
|
int kind_value = (int)kind->valuedouble;
|
|
if (kind_value == 5) {
|
|
cJSON* tags = cJSON_GetObjectItemCaseSensitive(ev, "tags");
|
|
if (!tags || !cJSON_IsArray(tags)) return;
|
|
|
|
cJSON* tag = NULL;
|
|
cJSON_ArrayForEach(tag, tags) {
|
|
if (!tag || !cJSON_IsArray(tag) || cJSON_GetArraySize(tag) < 2) continue;
|
|
cJSON* t0 = cJSON_GetArrayItem(tag, 0);
|
|
cJSON* t1 = cJSON_GetArrayItem(tag, 1);
|
|
if (!t0 || !t1 || !cJSON_IsString(t0) || !cJSON_IsString(t1) || !t0->valuestring || !t1->valuestring) continue;
|
|
if (strcmp(t0->valuestring, "e") != 0) continue;
|
|
(void)deleted_id_add(deleted_ids, deleted_count, t1->valuestring);
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (kind_value == NOSTR_NIP60_TOKEN_KIND) {
|
|
nostr_nip60_token_data_t token;
|
|
memset(&token, 0, sizeof(token));
|
|
if (nostr_nip60_parse_token_event(ev, g_wallet.cfg->keys.private_key, &token) == 0) {
|
|
for (int i = 0; i < token.deleted_count; i++) {
|
|
if (token.deleted_token_ids[i] && token.deleted_token_ids[i][0] != '\0') {
|
|
(void)deleted_id_add(deleted_ids, deleted_count, token.deleted_token_ids[i]);
|
|
}
|
|
}
|
|
}
|
|
nostr_nip60_free_token_data(&token);
|
|
}
|
|
}
|
|
|
|
int cashu_wallet_load_from_relays(void) {
|
|
if (!g_wallet.initialized || !g_wallet.cfg) return -1;
|
|
|
|
pthread_mutex_lock(&g_wallet.mutex);
|
|
|
|
cJSON* filter = nostr_nip60_create_wallet_filter(g_wallet.cfg->keys.public_key_hex);
|
|
if (!filter) {
|
|
pthread_mutex_unlock(&g_wallet.mutex);
|
|
return -1;
|
|
}
|
|
|
|
char* events_json = nostr_handler_query_json(filter, CASHU_WALLET_QUERY_TIMEOUT_MS);
|
|
cJSON_Delete(filter);
|
|
if (!events_json) {
|
|
pthread_mutex_unlock(&g_wallet.mutex);
|
|
return -1;
|
|
}
|
|
|
|
cJSON* arr = cJSON_Parse(events_json);
|
|
free(events_json);
|
|
if (!arr || !cJSON_IsArray(arr)) {
|
|
cJSON_Delete(arr);
|
|
pthread_mutex_unlock(&g_wallet.mutex);
|
|
return -1;
|
|
}
|
|
|
|
nostr_nip60_wallet_data_t newest_wallet;
|
|
memset(&newest_wallet, 0, sizeof(newest_wallet));
|
|
int have_wallet = 0;
|
|
double newest_created_at = -1;
|
|
|
|
char** deleted_ids = NULL;
|
|
int deleted_count = 0;
|
|
|
|
int n = cJSON_GetArraySize(arr);
|
|
for (int i = 0; i < n; i++) {
|
|
cJSON* ev = cJSON_GetArrayItem(arr, i);
|
|
collect_deleted_ids_from_event(ev, &deleted_ids, &deleted_count);
|
|
|
|
cJSON* kind = ev ? cJSON_GetObjectItemCaseSensitive(ev, "kind") : NULL;
|
|
cJSON* created_at = ev ? cJSON_GetObjectItemCaseSensitive(ev, "created_at") : NULL;
|
|
if (!kind || !cJSON_IsNumber(kind) || !created_at || !cJSON_IsNumber(created_at)) continue;
|
|
|
|
if ((int)kind->valuedouble != NOSTR_NIP60_WALLET_KIND) continue;
|
|
|
|
nostr_nip60_wallet_data_t parsed;
|
|
memset(&parsed, 0, sizeof(parsed));
|
|
if (nostr_nip60_parse_wallet_event(ev, g_wallet.cfg->keys.private_key, &parsed) != 0) {
|
|
continue;
|
|
}
|
|
|
|
if (!have_wallet || created_at->valuedouble > newest_created_at) {
|
|
nostr_nip60_free_wallet_data(&newest_wallet);
|
|
newest_wallet = parsed;
|
|
newest_created_at = created_at->valuedouble;
|
|
have_wallet = 1;
|
|
} else {
|
|
nostr_nip60_free_wallet_data(&parsed);
|
|
}
|
|
}
|
|
|
|
wallet_clear_all_locked();
|
|
|
|
if (!have_wallet) {
|
|
deleted_id_list_free(deleted_ids, deleted_count);
|
|
cJSON_Delete(arr);
|
|
pthread_mutex_unlock(&g_wallet.mutex);
|
|
return NOSTR_ERROR_NIP60_INVALID_WALLET;
|
|
}
|
|
|
|
g_wallet.wallet = newest_wallet;
|
|
g_wallet.wallet_loaded = 1;
|
|
|
|
for (int i = 0; i < n; i++) {
|
|
cJSON* ev = cJSON_GetArrayItem(arr, i);
|
|
cJSON* kind = ev ? cJSON_GetObjectItemCaseSensitive(ev, "kind") : NULL;
|
|
cJSON* id = ev ? cJSON_GetObjectItemCaseSensitive(ev, "id") : NULL;
|
|
if (!kind || !cJSON_IsNumber(kind) || !id || !cJSON_IsString(id) || !id->valuestring) continue;
|
|
|
|
if ((int)kind->valuedouble != NOSTR_NIP60_TOKEN_KIND) continue;
|
|
|
|
if (deleted_id_index(deleted_ids, deleted_count, id->valuestring) >= 0) {
|
|
continue;
|
|
}
|
|
|
|
nostr_nip60_token_data_t token;
|
|
memset(&token, 0, sizeof(token));
|
|
if (nostr_nip60_parse_token_event(ev, g_wallet.cfg->keys.private_key, &token) != 0) {
|
|
continue;
|
|
}
|
|
|
|
if (wallet_append_token_locked(id->valuestring, &token) != 0) {
|
|
nostr_nip60_free_token_data(&token);
|
|
deleted_id_list_free(deleted_ids, deleted_count);
|
|
cJSON_Delete(arr);
|
|
pthread_mutex_unlock(&g_wallet.mutex);
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
deleted_id_list_free(deleted_ids, deleted_count);
|
|
cJSON_Delete(arr);
|
|
pthread_mutex_unlock(&g_wallet.mutex);
|
|
return 0;
|
|
}
|
|
|
|
int cashu_wallet_balance(cJSON** out_json) {
|
|
if (!out_json || !g_wallet.initialized || !g_wallet.cfg) return -1;
|
|
(void)ensure_wallet_ready();
|
|
|
|
pthread_mutex_lock(&g_wallet.mutex);
|
|
|
|
cJSON* root = cJSON_CreateObject();
|
|
cJSON* by_mint = cJSON_CreateArray();
|
|
if (!root || !by_mint) {
|
|
cJSON_Delete(root);
|
|
cJSON_Delete(by_mint);
|
|
pthread_mutex_unlock(&g_wallet.mutex);
|
|
return -1;
|
|
}
|
|
|
|
cJSON_AddBoolToObject(root, "success", 1);
|
|
|
|
uint64_t total = 0;
|
|
for (int i = 0; i < g_wallet.token_count; i++) {
|
|
uint64_t amount = nostr_nip60_sum_proofs(g_wallet.tokens[i].token.proofs, g_wallet.tokens[i].token.proof_count);
|
|
total += amount;
|
|
|
|
cJSON* item = cJSON_CreateObject();
|
|
if (!item) continue;
|
|
cJSON_AddStringToObject(item, "mint", g_wallet.tokens[i].token.mint_url ? g_wallet.tokens[i].token.mint_url : "");
|
|
cJSON_AddNumberToObject(item, "amount", (double)amount);
|
|
cJSON_AddStringToObject(item, "token_event_id", g_wallet.tokens[i].event_id);
|
|
cJSON_AddItemToArray(by_mint, item);
|
|
}
|
|
|
|
cJSON_AddNumberToObject(root, "total", (double)total);
|
|
cJSON_AddItemToObject(root, "entries", by_mint);
|
|
cJSON_AddBoolToObject(root, "wallet_loaded", g_wallet.wallet_loaded ? 1 : 0);
|
|
|
|
*out_json = root;
|
|
|
|
pthread_mutex_unlock(&g_wallet.mutex);
|
|
return 0;
|
|
}
|
|
|
|
int cashu_wallet_info(const char* mint_url, cJSON** out_json) {
|
|
if (!out_json || !g_wallet.initialized || !g_wallet.cfg) return -1;
|
|
if (ensure_wallet_ready() != 0) return -1;
|
|
|
|
pthread_mutex_lock(&g_wallet.mutex);
|
|
const char* resolved = resolve_mint_url_locked(mint_url);
|
|
int timeout = g_wallet.cfg->cashu_wallet.mint_timeout_seconds > 0 ? g_wallet.cfg->cashu_wallet.mint_timeout_seconds : 30;
|
|
pthread_mutex_unlock(&g_wallet.mutex);
|
|
|
|
if (!resolved) return -1;
|
|
|
|
cashu_mint_info_t info;
|
|
memset(&info, 0, sizeof(info));
|
|
if (cashu_mint_get_info(resolved, &info, timeout) != 0) {
|
|
return -1;
|
|
}
|
|
|
|
cJSON* root = cJSON_CreateObject();
|
|
cJSON* keysets = cJSON_CreateArray();
|
|
if (!root || !keysets) {
|
|
cJSON_Delete(root);
|
|
cJSON_Delete(keysets);
|
|
cashu_mint_free_info(&info);
|
|
return -1;
|
|
}
|
|
|
|
cJSON_AddBoolToObject(root, "success", 1);
|
|
cJSON_AddStringToObject(root, "mint", resolved);
|
|
cJSON_AddStringToObject(root, "name", info.name ? info.name : "");
|
|
cJSON_AddStringToObject(root, "pubkey", info.pubkey ? info.pubkey : "");
|
|
cJSON_AddStringToObject(root, "version", info.version ? info.version : "");
|
|
|
|
for (int i = 0; i < info.keyset_count; i++) {
|
|
cJSON* ks = cJSON_CreateObject();
|
|
if (!ks) continue;
|
|
cJSON_AddStringToObject(ks, "id", info.keysets[i].id);
|
|
cJSON_AddStringToObject(ks, "unit", info.keysets[i].unit);
|
|
cJSON_AddBoolToObject(ks, "active", info.keysets[i].active ? 1 : 0);
|
|
cJSON_AddItemToArray(keysets, ks);
|
|
}
|
|
|
|
cJSON_AddItemToObject(root, "keysets", keysets);
|
|
*out_json = root;
|
|
|
|
cashu_mint_free_info(&info);
|
|
return 0;
|
|
}
|
|
|
|
int cashu_wallet_mint_quote(const char* mint_url, uint64_t amount, const char* unit, cJSON** out_json) {
|
|
if (!out_json || amount == 0 || !g_wallet.initialized || !g_wallet.cfg) return -1;
|
|
if (ensure_wallet_ready() != 0) return -1;
|
|
|
|
pthread_mutex_lock(&g_wallet.mutex);
|
|
const char* resolved = resolve_mint_url_locked(mint_url);
|
|
const char* resolved_unit = (unit && unit[0] != '\0') ? unit :
|
|
(g_wallet.cfg->cashu_wallet.unit[0] ? g_wallet.cfg->cashu_wallet.unit : "sat");
|
|
int timeout = g_wallet.cfg->cashu_wallet.mint_timeout_seconds > 0 ? g_wallet.cfg->cashu_wallet.mint_timeout_seconds : 30;
|
|
pthread_mutex_unlock(&g_wallet.mutex);
|
|
|
|
if (!resolved) return -1;
|
|
|
|
cashu_mint_quote_t quote;
|
|
memset("e, 0, sizeof(quote));
|
|
if (cashu_mint_request_mint_quote(resolved, amount, resolved_unit, "e, 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("e, 0, sizeof(quote));
|
|
if (cashu_mint_check_mint_quote(resolved, quote_id, "e, 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("e, 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, "e, 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("e, 0, sizeof(quote));
|
|
if (cashu_mint_request_melt_quote(resolved, payment_request, resolved_unit, "e, 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("e, 0, sizeof(quote));
|
|
if (cashu_mint_check_melt_quote(resolved, quote_id, "e, 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;
|
|
}
|