Line data Source code
1 : /*
2 : This file is part of TALER
3 : Copyright (C) 2014-2023 Taler Systems SA
4 :
5 : TALER is free software; you can redistribute it and/or modify it under the
6 : terms of the GNU General Public License as published by the Free Software
7 : Foundation; either version 3, or (at your option) any later version.
8 :
9 : TALER is distributed in the hope that it will be useful, but WITHOUT ANY
10 : WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
11 : A PARTICULAR PURPOSE. See the GNU General Public License for more details.
12 :
13 : You should have received a copy of the GNU General Public License along with
14 : TALER; see the file COPYING. If not, see
15 : <http://www.gnu.org/licenses/>
16 : */
17 : /**
18 : * @file lib/exchange_api_coins_history.c
19 : * @brief Implementation of the POST /coins/$COIN_PUB/history requests
20 : * @author Christian Grothoff
21 : *
22 : * NOTE: this is an incomplete draft, never finished!
23 : */
24 : #include "platform.h"
25 : #include <jansson.h>
26 : #include <microhttpd.h> /* just for HTTP history codes */
27 : #include <gnunet/gnunet_util_lib.h>
28 : #include <gnunet/gnunet_json_lib.h>
29 : #include <gnunet/gnunet_curl_lib.h>
30 : #include "taler_exchange_service.h"
31 : #include "taler_json_lib.h"
32 : #include "exchange_api_handle.h"
33 : #include "taler_signatures.h"
34 : #include "exchange_api_curl_defaults.h"
35 :
36 :
37 : /**
38 : * @brief A /coins/$RID/history Handle
39 : */
40 : struct TALER_EXCHANGE_CoinsHistoryHandle
41 : {
42 :
43 : /**
44 : * The url for this request.
45 : */
46 : char *url;
47 :
48 : /**
49 : * Handle for the request.
50 : */
51 : struct GNUNET_CURL_Job *job;
52 :
53 : /**
54 : * Context for #TEH_curl_easy_post(). Keeps the data that must
55 : * persist for Curl to make the upload.
56 : */
57 : struct TALER_CURL_PostContext post_ctx;
58 :
59 : /**
60 : * Function to call with the result.
61 : */
62 : TALER_EXCHANGE_CoinsHistoryCallback cb;
63 :
64 : /**
65 : * Public key of the coin we are querying.
66 : */
67 : struct TALER_CoinSpendPublicKeyP coin_pub;
68 :
69 : /**
70 : * Closure for @a cb.
71 : */
72 : void *cb_cls;
73 :
74 : };
75 :
76 :
77 : /**
78 : * Context for coin helpers.
79 : */
80 : struct CoinHistoryParseContext
81 : {
82 :
83 : /**
84 : * Keys of the exchange.
85 : */
86 : struct TALER_EXCHANGE_Keys *keys;
87 :
88 : /**
89 : * Denomination of the coin.
90 : */
91 : const struct TALER_EXCHANGE_DenomPublicKey *dk;
92 :
93 : /**
94 : * Our coin public key.
95 : */
96 : const struct TALER_CoinSpendPublicKeyP *coin_pub;
97 :
98 : /**
99 : * Where to sum up total refunds.
100 : */
101 : struct TALER_Amount *total_in;
102 :
103 : /**
104 : * Total amount encountered.
105 : */
106 : struct TALER_Amount *total_out;
107 :
108 : };
109 :
110 :
111 : /**
112 : * Signature of functions that operate on one of
113 : * the coin's history entries.
114 : *
115 : * @param[in,out] pc overall context
116 : * @param[out] rh where to write the history entry
117 : * @param amount main amount of this operation
118 : * @param transaction JSON details for the operation
119 : * @return #GNUNET_SYSERR on error,
120 : * #GNUNET_OK to add, #GNUNET_NO to subtract
121 : */
122 : typedef enum GNUNET_GenericReturnValue
123 : (*CoinCheckHelper)(struct CoinHistoryParseContext *pc,
124 : struct TALER_EXCHANGE_CoinHistoryEntry *rh,
125 : const struct TALER_Amount *amount,
126 : json_t *transaction);
127 :
128 :
129 : /**
130 : * Handle deposit entry in the coin's history.
131 : *
132 : * @param[in,out] pc overall context
133 : * @param[out] rh history entry to initialize
134 : * @param amount main amount of this operation
135 : * @param transaction JSON details for the operation
136 : * @return #GNUNET_SYSERR on error,
137 : * #GNUNET_OK to add, #GNUNET_NO to subtract
138 : */
139 : static enum GNUNET_GenericReturnValue
140 2 : help_deposit (struct CoinHistoryParseContext *pc,
141 : struct TALER_EXCHANGE_CoinHistoryEntry *rh,
142 : const struct TALER_Amount *amount,
143 : json_t *transaction)
144 : {
145 : struct GNUNET_JSON_Specification spec[] = {
146 2 : GNUNET_JSON_spec_fixed_auto ("coin_sig",
147 : &rh->details.deposit.sig),
148 2 : GNUNET_JSON_spec_fixed_auto ("h_contract_terms",
149 : &rh->details.deposit.h_contract_terms),
150 2 : GNUNET_JSON_spec_mark_optional (
151 2 : GNUNET_JSON_spec_fixed_auto ("wallet_data_hash",
152 : &rh->details.deposit.wallet_data_hash),
153 : &rh->details.deposit.no_wallet_data_hash),
154 2 : GNUNET_JSON_spec_fixed_auto ("h_wire",
155 : &rh->details.deposit.h_wire),
156 2 : GNUNET_JSON_spec_mark_optional (
157 2 : GNUNET_JSON_spec_fixed_auto ("h_age_commitment",
158 : &rh->details.deposit.hac),
159 : &rh->details.deposit.no_hac),
160 2 : GNUNET_JSON_spec_mark_optional (
161 2 : GNUNET_JSON_spec_fixed_auto ("h_policy",
162 : &rh->details.deposit.h_policy),
163 : &rh->details.deposit.no_h_policy),
164 2 : GNUNET_JSON_spec_timestamp ("timestamp",
165 : &rh->details.deposit.wallet_timestamp),
166 2 : GNUNET_JSON_spec_mark_optional (
167 : GNUNET_JSON_spec_timestamp ("refund_deadline",
168 : &rh->details.deposit.refund_deadline),
169 : NULL),
170 2 : TALER_JSON_spec_amount_any ("deposit_fee",
171 : &rh->details.deposit.deposit_fee),
172 2 : GNUNET_JSON_spec_fixed_auto ("merchant_pub",
173 : &rh->details.deposit.merchant_pub),
174 2 : GNUNET_JSON_spec_end ()
175 : };
176 :
177 2 : rh->details.deposit.refund_deadline = GNUNET_TIME_UNIT_ZERO_TS;
178 2 : if (GNUNET_OK !=
179 2 : GNUNET_JSON_parse (transaction,
180 : spec,
181 : NULL, NULL))
182 : {
183 0 : GNUNET_break_op (0);
184 0 : return GNUNET_SYSERR;
185 : }
186 2 : if (GNUNET_OK !=
187 6 : TALER_wallet_deposit_verify (
188 : amount,
189 2 : &rh->details.deposit.deposit_fee,
190 2 : &rh->details.deposit.h_wire,
191 2 : &rh->details.deposit.h_contract_terms,
192 2 : rh->details.deposit.no_wallet_data_hash
193 : ? NULL
194 : : &rh->details.deposit.wallet_data_hash,
195 2 : rh->details.deposit.no_hac
196 : ? NULL
197 : : &rh->details.deposit.hac,
198 2 : rh->details.deposit.no_h_policy
199 : ? NULL
200 : : &rh->details.deposit.h_policy,
201 2 : &pc->dk->h_key,
202 : rh->details.deposit.wallet_timestamp,
203 2 : &rh->details.deposit.merchant_pub,
204 : rh->details.deposit.refund_deadline,
205 : pc->coin_pub,
206 2 : &rh->details.deposit.sig))
207 : {
208 0 : GNUNET_break_op (0);
209 0 : return GNUNET_SYSERR;
210 : }
211 : /* check that deposit fee matches our expectations from /keys! */
212 2 : if ( (GNUNET_YES !=
213 2 : TALER_amount_cmp_currency (&rh->details.deposit.deposit_fee,
214 4 : &pc->dk->fees.deposit)) ||
215 : (0 !=
216 2 : TALER_amount_cmp (&rh->details.deposit.deposit_fee,
217 2 : &pc->dk->fees.deposit)) )
218 : {
219 0 : GNUNET_break_op (0);
220 0 : return GNUNET_SYSERR;
221 : }
222 2 : return GNUNET_YES;
223 : }
224 :
225 :
226 : /**
227 : * Handle melt entry in the coin's history.
228 : *
229 : * @param[in,out] pc overall context
230 : * @param[out] rh history entry to initialize
231 : * @param amount main amount of this operation
232 : * @param transaction JSON details for the operation
233 : * @return #GNUNET_SYSERR on error,
234 : * #GNUNET_OK to add, #GNUNET_NO to subtract
235 : */
236 : static enum GNUNET_GenericReturnValue
237 0 : help_melt (struct CoinHistoryParseContext *pc,
238 : struct TALER_EXCHANGE_CoinHistoryEntry *rh,
239 : const struct TALER_Amount *amount,
240 : json_t *transaction)
241 : {
242 : struct GNUNET_JSON_Specification spec[] = {
243 0 : GNUNET_JSON_spec_fixed_auto ("coin_sig",
244 : &rh->details.melt.sig),
245 0 : GNUNET_JSON_spec_fixed_auto ("rc",
246 : &rh->details.melt.rc),
247 0 : GNUNET_JSON_spec_mark_optional (
248 0 : GNUNET_JSON_spec_fixed_auto ("h_age_commitment",
249 : &rh->details.melt.h_age_commitment),
250 : &rh->details.melt.no_hac),
251 0 : TALER_JSON_spec_amount_any ("melt_fee",
252 : &rh->details.melt.melt_fee),
253 0 : GNUNET_JSON_spec_end ()
254 : };
255 :
256 0 : if (GNUNET_OK !=
257 0 : GNUNET_JSON_parse (transaction,
258 : spec,
259 : NULL, NULL))
260 : {
261 0 : GNUNET_break_op (0);
262 0 : return GNUNET_SYSERR;
263 : }
264 :
265 : /* check that melt fee matches our expectations from /keys! */
266 0 : if ( (GNUNET_YES !=
267 0 : TALER_amount_cmp_currency (&rh->details.melt.melt_fee,
268 0 : &pc->dk->fees.refresh)) ||
269 : (0 !=
270 0 : TALER_amount_cmp (&rh->details.melt.melt_fee,
271 0 : &pc->dk->fees.refresh)) )
272 : {
273 0 : GNUNET_break_op (0);
274 0 : return GNUNET_SYSERR;
275 : }
276 0 : if (GNUNET_OK !=
277 0 : TALER_wallet_melt_verify (
278 : amount,
279 0 : &rh->details.melt.melt_fee,
280 0 : &rh->details.melt.rc,
281 0 : &pc->dk->h_key,
282 0 : rh->details.melt.no_hac
283 : ? NULL
284 : : &rh->details.melt.h_age_commitment,
285 : pc->coin_pub,
286 0 : &rh->details.melt.sig))
287 : {
288 0 : GNUNET_break_op (0);
289 0 : return GNUNET_SYSERR;
290 : }
291 0 : return GNUNET_YES;
292 : }
293 :
294 :
295 : /**
296 : * Handle refund entry in the coin's history.
297 : *
298 : * @param[in,out] pc overall context
299 : * @param[out] rh history entry to initialize
300 : * @param amount main amount of this operation
301 : * @param transaction JSON details for the operation
302 : * @return #GNUNET_SYSERR on error,
303 : * #GNUNET_OK to add, #GNUNET_NO to subtract
304 : */
305 : static enum GNUNET_GenericReturnValue
306 0 : help_refund (struct CoinHistoryParseContext *pc,
307 : struct TALER_EXCHANGE_CoinHistoryEntry *rh,
308 : const struct TALER_Amount *amount,
309 : json_t *transaction)
310 : {
311 : struct GNUNET_JSON_Specification spec[] = {
312 0 : TALER_JSON_spec_amount_any ("refund_fee",
313 : &rh->details.refund.refund_fee),
314 0 : GNUNET_JSON_spec_fixed_auto ("merchant_sig",
315 : &rh->details.refund.sig),
316 0 : GNUNET_JSON_spec_fixed_auto ("h_contract_terms",
317 : &rh->details.refund.h_contract_terms),
318 0 : GNUNET_JSON_spec_fixed_auto ("merchant_pub",
319 : &rh->details.refund.merchant_pub),
320 0 : GNUNET_JSON_spec_uint64 ("rtransaction_id",
321 : &rh->details.refund.rtransaction_id),
322 0 : GNUNET_JSON_spec_end ()
323 : };
324 :
325 0 : if (GNUNET_OK !=
326 0 : GNUNET_JSON_parse (transaction,
327 : spec,
328 : NULL, NULL))
329 : {
330 0 : GNUNET_break_op (0);
331 0 : return GNUNET_SYSERR;
332 : }
333 0 : if (0 >
334 0 : TALER_amount_add (&rh->details.refund.sig_amount,
335 0 : &rh->details.refund.refund_fee,
336 : amount))
337 : {
338 0 : GNUNET_break_op (0);
339 0 : return GNUNET_SYSERR;
340 : }
341 0 : if (GNUNET_OK !=
342 0 : TALER_merchant_refund_verify (pc->coin_pub,
343 0 : &rh->details.refund.h_contract_terms,
344 : rh->details.refund.rtransaction_id,
345 0 : &rh->details.refund.sig_amount,
346 0 : &rh->details.refund.merchant_pub,
347 0 : &rh->details.refund.sig))
348 : {
349 0 : GNUNET_break_op (0);
350 0 : return GNUNET_SYSERR;
351 : }
352 : /* NOTE: theoretically, we could also check that the given
353 : merchant_pub and h_contract_terms appear in the
354 : history under deposits. However, there is really no benefit
355 : for the exchange to lie here, so not checking is probably OK
356 : (an auditor ought to check, though). Then again, we similarly
357 : had no reason to check the merchant's signature (other than a
358 : well-formendess check). */
359 :
360 : /* check that refund fee matches our expectations from /keys! */
361 0 : if ( (GNUNET_YES !=
362 0 : TALER_amount_cmp_currency (&rh->details.refund.refund_fee,
363 0 : &pc->dk->fees.refund)) ||
364 : (0 !=
365 0 : TALER_amount_cmp (&rh->details.refund.refund_fee,
366 0 : &pc->dk->fees.refund)) )
367 : {
368 0 : GNUNET_break_op (0);
369 0 : return GNUNET_SYSERR;
370 : }
371 0 : return GNUNET_NO;
372 : }
373 :
374 :
375 : /**
376 : * Handle recoup entry in the coin's history.
377 : *
378 : * @param[in,out] pc overall context
379 : * @param[out] rh history entry to initialize
380 : * @param amount main amount of this operation
381 : * @param transaction JSON details for the operation
382 : * @return #GNUNET_SYSERR on error,
383 : * #GNUNET_OK to add, #GNUNET_NO to subtract
384 : */
385 : static enum GNUNET_GenericReturnValue
386 0 : help_recoup (struct CoinHistoryParseContext *pc,
387 : struct TALER_EXCHANGE_CoinHistoryEntry *rh,
388 : const struct TALER_Amount *amount,
389 : json_t *transaction)
390 : {
391 : struct GNUNET_JSON_Specification spec[] = {
392 0 : GNUNET_JSON_spec_fixed_auto ("exchange_sig",
393 : &rh->details.recoup.exchange_sig),
394 0 : GNUNET_JSON_spec_fixed_auto ("exchange_pub",
395 : &rh->details.recoup.exchange_pub),
396 0 : GNUNET_JSON_spec_fixed_auto ("reserve_pub",
397 : &rh->details.recoup.reserve_pub),
398 0 : GNUNET_JSON_spec_fixed_auto ("coin_sig",
399 : &rh->details.recoup.coin_sig),
400 0 : GNUNET_JSON_spec_fixed_auto ("coin_blind",
401 : &rh->details.recoup.coin_bks),
402 0 : GNUNET_JSON_spec_timestamp ("timestamp",
403 : &rh->details.recoup.timestamp),
404 0 : GNUNET_JSON_spec_end ()
405 : };
406 :
407 0 : if (GNUNET_OK !=
408 0 : GNUNET_JSON_parse (transaction,
409 : spec,
410 : NULL, NULL))
411 : {
412 0 : GNUNET_break_op (0);
413 0 : return GNUNET_SYSERR;
414 : }
415 0 : if (GNUNET_OK !=
416 0 : TALER_exchange_online_confirm_recoup_verify (
417 : rh->details.recoup.timestamp,
418 : amount,
419 : pc->coin_pub,
420 0 : &rh->details.recoup.reserve_pub,
421 0 : &rh->details.recoup.exchange_pub,
422 0 : &rh->details.recoup.exchange_sig))
423 : {
424 0 : GNUNET_break_op (0);
425 0 : return GNUNET_SYSERR;
426 : }
427 0 : if (GNUNET_OK !=
428 0 : TALER_wallet_recoup_verify (&pc->dk->h_key,
429 0 : &rh->details.recoup.coin_bks,
430 : pc->coin_pub,
431 0 : &rh->details.recoup.coin_sig))
432 : {
433 0 : GNUNET_break_op (0);
434 0 : return GNUNET_SYSERR;
435 : }
436 0 : return GNUNET_YES;
437 : }
438 :
439 :
440 : /**
441 : * Handle recoup-refresh entry in the coin's history.
442 : * This is the coin that was subjected to a recoup,
443 : * the value being credited to the old coin.
444 : *
445 : * @param[in,out] pc overall context
446 : * @param[out] rh history entry to initialize
447 : * @param amount main amount of this operation
448 : * @param transaction JSON details for the operation
449 : * @return #GNUNET_SYSERR on error,
450 : * #GNUNET_OK to add, #GNUNET_NO to subtract
451 : */
452 : static enum GNUNET_GenericReturnValue
453 0 : help_recoup_refresh (struct CoinHistoryParseContext *pc,
454 : struct TALER_EXCHANGE_CoinHistoryEntry *rh,
455 : const struct TALER_Amount *amount,
456 : json_t *transaction)
457 : {
458 : struct GNUNET_JSON_Specification spec[] = {
459 0 : GNUNET_JSON_spec_fixed_auto ("exchange_sig",
460 : &rh->details.recoup_refresh.exchange_sig),
461 0 : GNUNET_JSON_spec_fixed_auto ("exchange_pub",
462 : &rh->details.recoup_refresh.exchange_pub),
463 0 : GNUNET_JSON_spec_fixed_auto ("coin_sig",
464 : &rh->details.recoup_refresh.coin_sig),
465 0 : GNUNET_JSON_spec_fixed_auto ("old_coin_pub",
466 : &rh->details.recoup_refresh.old_coin_pub),
467 0 : GNUNET_JSON_spec_fixed_auto ("coin_blind",
468 : &rh->details.recoup_refresh.coin_bks),
469 0 : GNUNET_JSON_spec_timestamp ("timestamp",
470 : &rh->details.recoup_refresh.timestamp),
471 0 : GNUNET_JSON_spec_end ()
472 : };
473 :
474 0 : if (GNUNET_OK !=
475 0 : GNUNET_JSON_parse (transaction,
476 : spec,
477 : NULL, NULL))
478 : {
479 0 : GNUNET_break_op (0);
480 0 : return GNUNET_SYSERR;
481 : }
482 0 : if (GNUNET_OK !=
483 0 : TALER_exchange_online_confirm_recoup_refresh_verify (
484 : rh->details.recoup_refresh.timestamp,
485 : amount,
486 : pc->coin_pub,
487 0 : &rh->details.recoup_refresh.old_coin_pub,
488 0 : &rh->details.recoup_refresh.exchange_pub,
489 0 : &rh->details.recoup_refresh.exchange_sig))
490 : {
491 0 : GNUNET_break_op (0);
492 0 : return GNUNET_SYSERR;
493 : }
494 0 : if (GNUNET_OK !=
495 0 : TALER_wallet_recoup_verify (&pc->dk->h_key,
496 0 : &rh->details.recoup_refresh.coin_bks,
497 : pc->coin_pub,
498 0 : &rh->details.recoup_refresh.coin_sig))
499 : {
500 0 : GNUNET_break_op (0);
501 0 : return GNUNET_SYSERR;
502 : }
503 0 : return GNUNET_YES;
504 : }
505 :
506 :
507 : /**
508 : * Handle old coin recoup entry in the coin's history.
509 : * This is the coin that was credited in a recoup,
510 : * the value being credited to the this coin.
511 : *
512 : * @param[in,out] pc overall context
513 : * @param[out] rh history entry to initialize
514 : * @param amount main amount of this operation
515 : * @param transaction JSON details for the operation
516 : * @return #GNUNET_SYSERR on error,
517 : * #GNUNET_OK to add, #GNUNET_NO to subtract
518 : */
519 : static enum GNUNET_GenericReturnValue
520 0 : help_old_coin_recoup (struct CoinHistoryParseContext *pc,
521 : struct TALER_EXCHANGE_CoinHistoryEntry *rh,
522 : const struct TALER_Amount *amount,
523 : json_t *transaction)
524 : {
525 : struct GNUNET_JSON_Specification spec[] = {
526 0 : GNUNET_JSON_spec_fixed_auto ("exchange_sig",
527 : &rh->details.old_coin_recoup.exchange_sig),
528 0 : GNUNET_JSON_spec_fixed_auto ("exchange_pub",
529 : &rh->details.old_coin_recoup.exchange_pub),
530 0 : GNUNET_JSON_spec_fixed_auto ("coin_pub",
531 : &rh->details.old_coin_recoup.new_coin_pub),
532 0 : GNUNET_JSON_spec_timestamp ("timestamp",
533 : &rh->details.old_coin_recoup.timestamp),
534 0 : GNUNET_JSON_spec_end ()
535 : };
536 :
537 0 : if (GNUNET_OK !=
538 0 : GNUNET_JSON_parse (transaction,
539 : spec,
540 : NULL, NULL))
541 : {
542 0 : GNUNET_break_op (0);
543 0 : return GNUNET_SYSERR;
544 : }
545 0 : if (GNUNET_OK !=
546 0 : TALER_exchange_online_confirm_recoup_refresh_verify (
547 : rh->details.old_coin_recoup.timestamp,
548 : amount,
549 0 : &rh->details.old_coin_recoup.new_coin_pub,
550 : pc->coin_pub,
551 0 : &rh->details.old_coin_recoup.exchange_pub,
552 0 : &rh->details.old_coin_recoup.exchange_sig))
553 : {
554 0 : GNUNET_break_op (0);
555 0 : return GNUNET_SYSERR;
556 : }
557 0 : return GNUNET_NO;
558 : }
559 :
560 :
561 : /**
562 : * Handle purse deposit entry in the coin's history.
563 : *
564 : * @param[in,out] pc overall context
565 : * @param[out] rh history entry to initialize
566 : * @param amount main amount of this operation
567 : * @param transaction JSON details for the operation
568 : * @return #GNUNET_SYSERR on error,
569 : * #GNUNET_OK to add, #GNUNET_NO to subtract
570 : */
571 : static enum GNUNET_GenericReturnValue
572 3 : help_purse_deposit (struct CoinHistoryParseContext *pc,
573 : struct TALER_EXCHANGE_CoinHistoryEntry *rh,
574 : const struct TALER_Amount *amount,
575 : json_t *transaction)
576 : {
577 : struct GNUNET_JSON_Specification spec[] = {
578 3 : GNUNET_JSON_spec_fixed_auto ("purse_pub",
579 : &rh->details.purse_deposit.purse_pub),
580 3 : GNUNET_JSON_spec_fixed_auto ("coin_sig",
581 : &rh->details.purse_deposit.coin_sig),
582 3 : GNUNET_JSON_spec_mark_optional (
583 3 : GNUNET_JSON_spec_fixed_auto ("h_age_commitment",
584 : &rh->details.purse_deposit.phac),
585 : NULL),
586 3 : TALER_JSON_spec_web_url ("exchange_base_url",
587 : &rh->details.purse_deposit.exchange_base_url),
588 3 : GNUNET_JSON_spec_bool ("refunded",
589 : &rh->details.purse_deposit.refunded),
590 3 : GNUNET_JSON_spec_end ()
591 : };
592 :
593 3 : if (GNUNET_OK !=
594 3 : GNUNET_JSON_parse (transaction,
595 : spec,
596 : NULL, NULL))
597 : {
598 0 : GNUNET_break_op (0);
599 0 : return GNUNET_SYSERR;
600 : }
601 3 : if (GNUNET_OK !=
602 3 : TALER_wallet_purse_deposit_verify (
603 : rh->details.purse_deposit.exchange_base_url,
604 3 : &rh->details.purse_deposit.purse_pub,
605 : amount,
606 3 : &pc->dk->h_key,
607 3 : &rh->details.purse_deposit.phac,
608 : pc->coin_pub,
609 3 : &rh->details.purse_deposit.coin_sig))
610 : {
611 0 : GNUNET_break_op (0);
612 0 : return GNUNET_SYSERR;
613 : }
614 3 : if (rh->details.purse_deposit.refunded)
615 : {
616 : /* We wave the deposit fee. */
617 0 : if (0 >
618 0 : TALER_amount_add (pc->total_in,
619 0 : pc->total_in,
620 0 : &pc->dk->fees.deposit))
621 : {
622 : /* overflow in refund history? inconceivable! Bad exchange! */
623 0 : GNUNET_break_op (0);
624 0 : return GNUNET_SYSERR;
625 : }
626 : }
627 3 : return GNUNET_YES;
628 : }
629 :
630 :
631 : /**
632 : * Handle purse refund entry in the coin's history.
633 : *
634 : * @param[in,out] pc overall context
635 : * @param[out] rh history entry to initialize
636 : * @param amount main amount of this operation
637 : * @param transaction JSON details for the operation
638 : * @return #GNUNET_SYSERR on error,
639 : * #GNUNET_OK to add, #GNUNET_NO to subtract
640 : */
641 : static enum GNUNET_GenericReturnValue
642 0 : help_purse_refund (struct CoinHistoryParseContext *pc,
643 : struct TALER_EXCHANGE_CoinHistoryEntry *rh,
644 : const struct TALER_Amount *amount,
645 : json_t *transaction)
646 : {
647 : struct GNUNET_JSON_Specification spec[] = {
648 0 : TALER_JSON_spec_amount_any ("refund_fee",
649 : &rh->details.purse_refund.refund_fee),
650 0 : GNUNET_JSON_spec_fixed_auto ("purse_pub",
651 : &rh->details.purse_refund.purse_pub),
652 0 : GNUNET_JSON_spec_fixed_auto ("exchange_sig",
653 : &rh->details.purse_refund.exchange_sig),
654 0 : GNUNET_JSON_spec_fixed_auto ("exchange_pub",
655 : &rh->details.purse_refund.exchange_pub),
656 0 : GNUNET_JSON_spec_end ()
657 : };
658 :
659 0 : if (GNUNET_OK !=
660 0 : GNUNET_JSON_parse (transaction,
661 : spec,
662 : NULL, NULL))
663 : {
664 0 : GNUNET_break_op (0);
665 0 : return GNUNET_SYSERR;
666 : }
667 0 : if (GNUNET_OK !=
668 0 : TALER_exchange_online_purse_refund_verify (
669 : amount,
670 0 : &rh->details.purse_refund.refund_fee,
671 : pc->coin_pub,
672 0 : &rh->details.purse_refund.purse_pub,
673 0 : &rh->details.purse_refund.exchange_pub,
674 0 : &rh->details.purse_refund.exchange_sig))
675 : {
676 0 : GNUNET_break_op (0);
677 0 : return GNUNET_SYSERR;
678 : }
679 0 : if ( (GNUNET_YES !=
680 0 : TALER_amount_cmp_currency (&rh->details.purse_refund.refund_fee,
681 0 : &pc->dk->fees.refund)) ||
682 : (0 !=
683 0 : TALER_amount_cmp (&rh->details.purse_refund.refund_fee,
684 0 : &pc->dk->fees.refund)) )
685 : {
686 0 : GNUNET_break_op (0);
687 0 : return GNUNET_SYSERR;
688 : }
689 0 : return GNUNET_NO;
690 : }
691 :
692 :
693 : /**
694 : * Handle reserve deposit entry in the coin's history.
695 : *
696 : * @param[in,out] pc overall context
697 : * @param[out] rh history entry to initialize
698 : * @param amount main amount of this operation
699 : * @param transaction JSON details for the operation
700 : * @return #GNUNET_SYSERR on error,
701 : * #GNUNET_OK to add, #GNUNET_NO to subtract
702 : */
703 : static enum GNUNET_GenericReturnValue
704 0 : help_reserve_open_deposit (struct CoinHistoryParseContext *pc,
705 : struct TALER_EXCHANGE_CoinHistoryEntry *rh,
706 : const struct TALER_Amount *amount,
707 : json_t *transaction)
708 : {
709 : struct GNUNET_JSON_Specification spec[] = {
710 0 : GNUNET_JSON_spec_fixed_auto ("reserve_sig",
711 : &rh->details.reserve_open_deposit.reserve_sig),
712 0 : GNUNET_JSON_spec_fixed_auto ("coin_sig",
713 : &rh->details.reserve_open_deposit.coin_sig),
714 0 : GNUNET_JSON_spec_end ()
715 : };
716 :
717 0 : if (GNUNET_OK !=
718 0 : GNUNET_JSON_parse (transaction,
719 : spec,
720 : NULL, NULL))
721 : {
722 0 : GNUNET_break_op (0);
723 0 : return GNUNET_SYSERR;
724 : }
725 0 : if (GNUNET_OK !=
726 0 : TALER_wallet_reserve_open_deposit_verify (
727 : amount,
728 0 : &rh->details.reserve_open_deposit.reserve_sig,
729 : pc->coin_pub,
730 0 : &rh->details.reserve_open_deposit.coin_sig))
731 : {
732 0 : GNUNET_break_op (0);
733 0 : return GNUNET_SYSERR;
734 : }
735 0 : return GNUNET_YES;
736 : }
737 :
738 :
739 : enum GNUNET_GenericReturnValue
740 4 : TALER_EXCHANGE_parse_coin_history (
741 : const struct TALER_EXCHANGE_Keys *keys,
742 : const struct TALER_EXCHANGE_DenomPublicKey *dk,
743 : const json_t *history,
744 : const struct TALER_CoinSpendPublicKeyP *coin_pub,
745 : struct TALER_Amount *total_in,
746 : struct TALER_Amount *total_out,
747 : unsigned int rlen,
748 : struct TALER_EXCHANGE_CoinHistoryEntry rhistory[static rlen])
749 4 : {
750 : const struct
751 : {
752 : const char *type;
753 : CoinCheckHelper helper;
754 : enum TALER_EXCHANGE_CoinTransactionType ctt;
755 4 : } map[] = {
756 : { "DEPOSIT",
757 : &help_deposit,
758 : TALER_EXCHANGE_CTT_DEPOSIT },
759 : { "MELT",
760 : &help_melt,
761 : TALER_EXCHANGE_CTT_MELT },
762 : { "REFUND",
763 : &help_refund,
764 : TALER_EXCHANGE_CTT_REFUND },
765 : { "RECOUP",
766 : &help_recoup,
767 : TALER_EXCHANGE_CTT_RECOUP },
768 : { "RECOUP-REFRESH",
769 : &help_recoup_refresh,
770 : TALER_EXCHANGE_CTT_RECOUP_REFRESH },
771 : { "OLD-COIN-RECOUP",
772 : &help_old_coin_recoup,
773 : TALER_EXCHANGE_CTT_OLD_COIN_RECOUP },
774 : { "PURSE-DEPOSIT",
775 : &help_purse_deposit,
776 : TALER_EXCHANGE_CTT_PURSE_DEPOSIT },
777 : { "PURSE-REFUND",
778 : &help_purse_refund,
779 : TALER_EXCHANGE_CTT_PURSE_REFUND },
780 : { "RESERVE-OPEN-DEPOSIT",
781 : &help_reserve_open_deposit,
782 : TALER_EXCHANGE_CTT_RESERVE_OPEN_DEPOSIT },
783 : { NULL, NULL, TALER_EXCHANGE_CTT_NONE }
784 : };
785 4 : struct CoinHistoryParseContext pc = {
786 : .dk = dk,
787 : .coin_pub = coin_pub,
788 : .total_out = total_out,
789 : .total_in = total_in
790 : };
791 : size_t len;
792 :
793 4 : if (NULL == history)
794 : {
795 0 : GNUNET_break_op (0);
796 0 : return GNUNET_SYSERR;
797 : }
798 4 : len = json_array_size (history);
799 4 : if (0 == len)
800 : {
801 0 : GNUNET_break_op (0);
802 0 : return GNUNET_SYSERR;
803 : }
804 4 : *total_in = dk->value;
805 4 : GNUNET_assert (GNUNET_OK ==
806 : TALER_amount_set_zero (total_in->currency,
807 : total_out));
808 9 : for (size_t off = 0; off<len; off++)
809 : {
810 5 : struct TALER_EXCHANGE_CoinHistoryEntry *rh = &rhistory[off];
811 5 : json_t *transaction = json_array_get (history,
812 : off);
813 : enum GNUNET_GenericReturnValue add;
814 : const char *type;
815 : struct GNUNET_JSON_Specification spec_glob[] = {
816 5 : TALER_JSON_spec_amount_any ("amount",
817 : &rh->amount),
818 5 : GNUNET_JSON_spec_string ("type",
819 : &type),
820 5 : GNUNET_JSON_spec_end ()
821 : };
822 :
823 5 : if (GNUNET_OK !=
824 5 : GNUNET_JSON_parse (transaction,
825 : spec_glob,
826 : NULL, NULL))
827 : {
828 0 : GNUNET_break_op (0);
829 0 : return GNUNET_SYSERR;
830 : }
831 5 : if (GNUNET_YES !=
832 5 : TALER_amount_cmp_currency (&rh->amount,
833 : total_in))
834 : {
835 0 : GNUNET_break_op (0);
836 0 : return GNUNET_SYSERR;
837 : }
838 5 : GNUNET_log (GNUNET_ERROR_TYPE_INFO,
839 : "Operation of type %s with amount %s\n",
840 : type,
841 : TALER_amount2s (&rh->amount));
842 5 : add = GNUNET_SYSERR;
843 23 : for (unsigned int i = 0; NULL != map[i].type; i++)
844 : {
845 23 : if (0 == strcasecmp (type,
846 23 : map[i].type))
847 : {
848 5 : rh->type = map[i].ctt;
849 5 : add = map[i].helper (&pc,
850 : rh,
851 5 : &rh->amount,
852 : transaction);
853 5 : break;
854 : }
855 : }
856 5 : switch (add)
857 : {
858 0 : case GNUNET_SYSERR:
859 : /* entry type not supported, new version on server? */
860 0 : rh->type = TALER_EXCHANGE_CTT_NONE;
861 0 : GNUNET_break_op (0);
862 0 : GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
863 : "Unexpected type `%s' in response\n",
864 : type);
865 0 : return GNUNET_SYSERR;
866 5 : case GNUNET_YES:
867 : /* This amount should be debited from the coin */
868 5 : if (0 >
869 5 : TALER_amount_add (total_out,
870 : total_out,
871 5 : &rh->amount))
872 : {
873 : /* overflow in history already!? inconceivable! Bad exchange! */
874 0 : GNUNET_break_op (0);
875 0 : return GNUNET_SYSERR;
876 : }
877 5 : break;
878 0 : case GNUNET_NO:
879 : /* This amount should be credited to the coin. */
880 0 : if (0 >
881 0 : TALER_amount_add (total_in,
882 : total_in,
883 0 : &rh->amount))
884 : {
885 : /* overflow in refund history? inconceivable! Bad exchange! */
886 0 : GNUNET_break_op (0);
887 0 : return GNUNET_SYSERR;
888 : }
889 0 : break;
890 : } /* end of switch(add) */
891 : }
892 4 : return GNUNET_OK;
893 : }
894 :
895 :
896 : /**
897 : * We received an #MHD_HTTP_OK history code. Handle the JSON
898 : * response.
899 : *
900 : * @param rsh handle of the request
901 : * @param j JSON response
902 : * @return #GNUNET_OK on success
903 : */
904 : static enum GNUNET_GenericReturnValue
905 4 : handle_coins_history_ok (struct TALER_EXCHANGE_CoinsHistoryHandle *rsh,
906 : const json_t *j)
907 : {
908 4 : struct TALER_EXCHANGE_CoinHistory rs = {
909 : .hr.reply = j,
910 : .hr.http_status = MHD_HTTP_OK
911 : };
912 : struct GNUNET_JSON_Specification spec[] = {
913 4 : TALER_JSON_spec_amount_any ("balance",
914 : &rs.details.ok.balance),
915 4 : GNUNET_JSON_spec_fixed_auto ("h_denom_pub",
916 : &rs.details.ok.h_denom_pub),
917 4 : GNUNET_JSON_spec_array_const ("history",
918 : &rs.details.ok.history),
919 4 : GNUNET_JSON_spec_end ()
920 : };
921 :
922 4 : if (GNUNET_OK !=
923 4 : GNUNET_JSON_parse (j,
924 : spec,
925 : NULL,
926 : NULL))
927 : {
928 0 : GNUNET_break_op (0);
929 0 : return GNUNET_SYSERR;
930 : }
931 4 : if (NULL != rsh->cb)
932 : {
933 4 : rsh->cb (rsh->cb_cls,
934 : &rs);
935 4 : rsh->cb = NULL;
936 : }
937 4 : GNUNET_JSON_parse_free (spec);
938 4 : return GNUNET_OK;
939 : }
940 :
941 :
942 : /**
943 : * Function called when we're done processing the
944 : * HTTP /coins/$RID/history request.
945 : *
946 : * @param cls the `struct TALER_EXCHANGE_CoinsHistoryHandle`
947 : * @param response_code HTTP response code, 0 on error
948 : * @param response parsed JSON result, NULL on error
949 : */
950 : static void
951 4 : handle_coins_history_finished (void *cls,
952 : long response_code,
953 : const void *response)
954 : {
955 4 : struct TALER_EXCHANGE_CoinsHistoryHandle *rsh = cls;
956 4 : const json_t *j = response;
957 4 : struct TALER_EXCHANGE_CoinHistory rs = {
958 : .hr.reply = j,
959 4 : .hr.http_status = (unsigned int) response_code
960 : };
961 :
962 4 : rsh->job = NULL;
963 4 : switch (response_code)
964 : {
965 0 : case 0:
966 0 : rs.hr.ec = TALER_EC_GENERIC_INVALID_RESPONSE;
967 0 : break;
968 4 : case MHD_HTTP_OK:
969 4 : if (GNUNET_OK !=
970 4 : handle_coins_history_ok (rsh,
971 : j))
972 : {
973 0 : rs.hr.http_status = 0;
974 0 : rs.hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED;
975 : }
976 4 : break;
977 0 : case MHD_HTTP_BAD_REQUEST:
978 : /* This should never happen, either us or the exchange is buggy
979 : (or API version conflict); just pass JSON reply to the application */
980 0 : GNUNET_break (0);
981 0 : rs.hr.ec = TALER_JSON_get_error_code (j);
982 0 : rs.hr.hint = TALER_JSON_get_error_hint (j);
983 0 : break;
984 0 : case MHD_HTTP_FORBIDDEN:
985 : /* This should never happen, either us or the exchange is buggy
986 : (or API version conflict); just pass JSON reply to the application */
987 0 : GNUNET_break (0);
988 0 : rs.hr.ec = TALER_JSON_get_error_code (j);
989 0 : rs.hr.hint = TALER_JSON_get_error_hint (j);
990 0 : break;
991 0 : case MHD_HTTP_NOT_FOUND:
992 : /* Nothing really to verify, this should never
993 : happen, we should pass the JSON reply to the application */
994 0 : rs.hr.ec = TALER_JSON_get_error_code (j);
995 0 : rs.hr.hint = TALER_JSON_get_error_hint (j);
996 0 : break;
997 0 : case MHD_HTTP_INTERNAL_SERVER_ERROR:
998 : /* Server had an internal issue; we should retry, but this API
999 : leaves this to the application */
1000 0 : rs.hr.ec = TALER_JSON_get_error_code (j);
1001 0 : rs.hr.hint = TALER_JSON_get_error_hint (j);
1002 0 : break;
1003 0 : default:
1004 : /* unexpected response code */
1005 0 : GNUNET_break_op (0);
1006 0 : rs.hr.ec = TALER_JSON_get_error_code (j);
1007 0 : rs.hr.hint = TALER_JSON_get_error_hint (j);
1008 0 : GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
1009 : "Unexpected response code %u/%d for coins history\n",
1010 : (unsigned int) response_code,
1011 : (int) rs.hr.ec);
1012 0 : break;
1013 : }
1014 4 : if (NULL != rsh->cb)
1015 : {
1016 0 : rsh->cb (rsh->cb_cls,
1017 : &rs);
1018 0 : rsh->cb = NULL;
1019 : }
1020 4 : TALER_EXCHANGE_coins_history_cancel (rsh);
1021 4 : }
1022 :
1023 :
1024 : struct TALER_EXCHANGE_CoinsHistoryHandle *
1025 4 : TALER_EXCHANGE_coins_history (
1026 : struct GNUNET_CURL_Context *ctx,
1027 : const char *url,
1028 : const struct TALER_CoinSpendPrivateKeyP *coin_priv,
1029 : uint64_t start_off,
1030 : TALER_EXCHANGE_CoinsHistoryCallback cb,
1031 : void *cb_cls)
1032 : {
1033 : struct TALER_EXCHANGE_CoinsHistoryHandle *rsh;
1034 : CURL *eh;
1035 : char arg_str[sizeof (struct TALER_CoinSpendPublicKeyP) * 2 + 64];
1036 : struct curl_slist *job_headers;
1037 :
1038 4 : rsh = GNUNET_new (struct TALER_EXCHANGE_CoinsHistoryHandle);
1039 4 : rsh->cb = cb;
1040 4 : rsh->cb_cls = cb_cls;
1041 4 : GNUNET_CRYPTO_eddsa_key_get_public (&coin_priv->eddsa_priv,
1042 : &rsh->coin_pub.eddsa_pub);
1043 : {
1044 : char pub_str[sizeof (struct TALER_CoinSpendPublicKeyP) * 2];
1045 : char *end;
1046 :
1047 4 : end = GNUNET_STRINGS_data_to_string (
1048 4 : &rsh->coin_pub,
1049 : sizeof (rsh->coin_pub),
1050 : pub_str,
1051 : sizeof (pub_str));
1052 4 : *end = '\0';
1053 4 : if (0 != start_off)
1054 0 : GNUNET_snprintf (arg_str,
1055 : sizeof (arg_str),
1056 : "coins/%s/history?start=%llu",
1057 : pub_str,
1058 : (unsigned long long) start_off);
1059 : else
1060 4 : GNUNET_snprintf (arg_str,
1061 : sizeof (arg_str),
1062 : "coins/%s/history",
1063 : pub_str);
1064 : }
1065 4 : rsh->url = TALER_url_join (url,
1066 : arg_str,
1067 : NULL);
1068 4 : if (NULL == rsh->url)
1069 : {
1070 0 : GNUNET_free (rsh);
1071 0 : return NULL;
1072 : }
1073 4 : eh = TALER_EXCHANGE_curl_easy_get_ (rsh->url);
1074 4 : if (NULL == eh)
1075 : {
1076 0 : GNUNET_break (0);
1077 0 : GNUNET_free (rsh->url);
1078 0 : GNUNET_free (rsh);
1079 0 : return NULL;
1080 : }
1081 :
1082 : {
1083 : struct TALER_CoinSpendSignatureP coin_sig;
1084 : char *sig_hdr;
1085 : char *hdr;
1086 :
1087 4 : TALER_wallet_coin_history_sign (start_off,
1088 : coin_priv,
1089 : &coin_sig);
1090 :
1091 4 : sig_hdr = GNUNET_STRINGS_data_to_string_alloc (
1092 : &coin_sig,
1093 : sizeof (coin_sig));
1094 4 : GNUNET_asprintf (&hdr,
1095 : "%s: %s",
1096 : TALER_COIN_HISTORY_SIGNATURE_HEADER,
1097 : sig_hdr);
1098 4 : GNUNET_free (sig_hdr);
1099 4 : job_headers = curl_slist_append (NULL,
1100 : hdr);
1101 4 : GNUNET_free (hdr);
1102 4 : if (NULL == job_headers)
1103 : {
1104 0 : GNUNET_break (0);
1105 0 : curl_easy_cleanup (eh);
1106 0 : return NULL;
1107 : }
1108 : }
1109 :
1110 4 : rsh->job = GNUNET_CURL_job_add2 (ctx,
1111 : eh,
1112 : job_headers,
1113 : &handle_coins_history_finished,
1114 : rsh);
1115 4 : curl_slist_free_all (job_headers);
1116 4 : return rsh;
1117 : }
1118 :
1119 :
1120 : void
1121 4 : TALER_EXCHANGE_coins_history_cancel (
1122 : struct TALER_EXCHANGE_CoinsHistoryHandle *rsh)
1123 : {
1124 4 : if (NULL != rsh->job)
1125 : {
1126 0 : GNUNET_CURL_job_cancel (rsh->job);
1127 0 : rsh->job = NULL;
1128 : }
1129 4 : TALER_curl_easy_post_finished (&rsh->post_ctx);
1130 4 : GNUNET_free (rsh->url);
1131 4 : GNUNET_free (rsh);
1132 4 : }
1133 :
1134 :
1135 : /**
1136 : * Verify that @a coin_sig does NOT appear in the @a history of a coin's
1137 : * transactions and thus whatever transaction is authorized by @a coin_sig is
1138 : * a conflict with @a proof.
1139 : *
1140 : * @param history coin history to check
1141 : * @param coin_sig signature that must not be in @a history
1142 : * @return #GNUNET_OK if @a coin_sig is not in @a history
1143 : */
1144 : enum GNUNET_GenericReturnValue
1145 0 : TALER_EXCHANGE_check_coin_signature_conflict (
1146 : const json_t *history,
1147 : const struct TALER_CoinSpendSignatureP *coin_sig)
1148 : {
1149 : size_t off;
1150 : json_t *entry;
1151 :
1152 0 : json_array_foreach (history, off, entry)
1153 : {
1154 : struct TALER_CoinSpendSignatureP cs;
1155 : struct GNUNET_JSON_Specification spec[] = {
1156 0 : GNUNET_JSON_spec_fixed_auto ("coin_sig",
1157 : &cs),
1158 0 : GNUNET_JSON_spec_end ()
1159 : };
1160 :
1161 0 : if (GNUNET_OK !=
1162 0 : GNUNET_JSON_parse (entry,
1163 : spec,
1164 : NULL, NULL))
1165 0 : continue; /* entry without coin signature */
1166 0 : if (0 ==
1167 0 : GNUNET_memcmp (&cs,
1168 : coin_sig))
1169 : {
1170 0 : GNUNET_break_op (0);
1171 0 : return GNUNET_SYSERR;
1172 : }
1173 : }
1174 0 : return GNUNET_OK;
1175 : }
1176 :
1177 :
1178 : #if FIXME_IMPLEMENT /* #9422 */
1179 : /**
1180 : * FIXME-Oec-#9422: we need some specific routines that show
1181 : * that certain coin operations are indeed in conflict,
1182 : * for example that the coin is of a different denomination
1183 : * or different age restrictions.
1184 : * This relates to unimplemented error handling for
1185 : * coins in the exchange!
1186 : *
1187 : * Check that the provided @a proof indeeds indicates
1188 : * a conflict for @a coin_pub.
1189 : *
1190 : * @param keys exchange keys
1191 : * @param proof provided conflict proof
1192 : * @param dk denomination of @a coin_pub that the client
1193 : * used
1194 : * @param coin_pub public key of the coin
1195 : * @param required balance required on the coin for the operation
1196 : * @return #GNUNET_OK if @a proof holds
1197 : */
1198 : // FIXME-#9422: should be properly defined and implemented!
1199 : enum GNUNET_GenericReturnValue
1200 : TALER_EXCHANGE_check_coin_conflict_ (
1201 : const struct TALER_EXCHANGE_Keys *keys,
1202 : const json_t *proof,
1203 : const struct TALER_EXCHANGE_DenomPublicKey *dk,
1204 : const struct TALER_CoinSpendPublicKeyP *coin_pub,
1205 : const struct TALER_Amount *required)
1206 : {
1207 : enum TALER_ErrorCode ec;
1208 :
1209 : ec = TALER_JSON_get_error_code (proof);
1210 : switch (ec)
1211 : {
1212 : case TALER_EC_EXCHANGE_GENERIC_INSUFFICIENT_FUNDS:
1213 : /* Nothing to check anymore here, proof needs to be
1214 : checked in the GET /coins/$COIN_PUB handler */
1215 : break;
1216 : case TALER_EC_EXCHANGE_GENERIC_COIN_CONFLICTING_DENOMINATION_KEY:
1217 : // FIXME-#9422: write check!
1218 : break;
1219 : default:
1220 : GNUNET_break_op (0);
1221 : return GNUNET_SYSERR;
1222 : }
1223 : return GNUNET_OK;
1224 : }
1225 :
1226 :
1227 : #endif
1228 :
1229 :
1230 : /* end of exchange_api_coins_history.c */
|