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