/*
This file is part of TALER
Copyright (C) 2014-2017 Taler Systems SA
TALER is free software; you can redistribute it and/or modify it under the
terms of the GNU Lesser General Public License as published by the Free Software
Foundation; either version 2.1, or (at your option) any later version.
TALER is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License along with
TALER; see the file COPYING.LGPL. If not, see
*/
/**
* @file merchant/test_merchant_api.c
* @brief testcase to test merchant's HTTP API interface
* @author Christian Grothoff
* @author Marcello Stanisci
*/
#include "platform.h"
#include
#include
#include
#include
#include
#include
#include "taler_merchant_service.h"
#include "taler_merchantdb_lib.h"
#include
#include
#include
/**
* URL under which the merchant is reachable during the testcase.
*/
#define MERCHANT_URL "http://localhost:8082"
/**
* URL under which the exchange is reachable during the testcase.
*/
#define EXCHANGE_URL "http://localhost:8081/"
/**
* Account number of the exchange at the bank.
*/
#define EXCHANGE_ACCOUNT_NO 2
/**
* URL of the bank.
*/
#define BANK_URL "http://localhost:8083/"
/**
* On which port do we run the (fake) bank?
*/
#define BANK_PORT 8083
/**
* Max size allowed for an order.
*/
#define ORDER_MAX_SIZE 1000
#define RND_BLK(ptr) \
GNUNET_CRYPTO_random_block (GNUNET_CRYPTO_QUALITY_WEAK, ptr, sizeof (*ptr))
/**
* Handle to database.
*/
static struct TALER_MERCHANTDB_Plugin *db;
/**
* Configuration handle.
*/
struct GNUNET_CONFIGURATION_Handle *cfg;
/**
* Handle to access the exchange.
*/
static struct TALER_EXCHANGE_Handle *exchange;
/**
* Main execution context for the main loop of the exchange.
*/
static struct GNUNET_CURL_Context *ctx;
/**
* Array of instances to test against
*/
static char **instances;
/**
* How many merchant instances this test runs
*/
static unsigned int ninstances = 0;
/**
* Current instance
*/
static char *instance;
/**
* Current instance key
*/
static struct GNUNET_CRYPTO_EddsaPrivateKey *instance_priv;
/**
* Current instance being tested
*/
static unsigned int instance_idx = 0;
/**
* Task run on timeout.
*/
static struct GNUNET_SCHEDULER_Task *timeout_task;
/**
* Context for running the #ctx's event loop.
*/
static struct GNUNET_CURL_RescheduleContext *rc;
/**
* Handle to the fake bank service we run for the
* aggregator.
*/
static struct TALER_FAKEBANK_Handle *fakebank;
/**
* Result of the testcases, #GNUNET_OK on success
*/
static int result;
/**
* Opcodes for the interpreter.
*/
enum OpCode
{
/**
* Termination code, stops the interpreter loop (with success).
*/
OC_END = 0,
/**
* Issue a GET /proposal to the backend.
*/
OC_PROPOSAL_LOOKUP,
/**
* Add funds to a reserve by (faking) incoming wire transfer.
*/
OC_ADMIN_ADD_INCOMING,
/**
* Check status of a reserve.
*/
OC_WITHDRAW_STATUS,
/**
* Withdraw a coin from a reserve.
*/
OC_WITHDRAW_SIGN,
/**
* Issue a PUT /proposal to the backend.
*/
OC_PROPOSAL,
/**
* Pay with coins.
*/
OC_PAY,
/**
* Resume pay operation with additional coins.
*/
OC_PAY_AGAIN,
/**
* Abort payment with coins, requesting refund.
*/
OC_PAY_ABORT,
/**
* Abort payment with coins, executing refund.
*/
OC_PAY_ABORT_REFUND,
/**
* Run the aggregator to execute deposits.
*/
OC_RUN_AGGREGATOR,
/**
* Run the wirewatcher to check for incoming transactions.
*/
OC_RUN_WIREWATCH,
/**
* Check that the fakebank has received a certain transaction.
*/
OC_CHECK_BANK_TRANSFER,
/**
* Check that the fakebank has not received any other transactions.
*/
OC_CHECK_BANK_TRANSFERS_EMPTY,
/**
* Retrieve deposit details for a given wire transfer
*/
OC_TRACK_TRANSFER,
/**
* Retrieve wire transfer details for a given transaction
*/
OC_TRACK_TRANSACTION,
/**
* Test getting transactions based on timestamp
*/
OC_HISTORY,
/**
* Test the increase of a order refund
*/
OC_REFUND_INCREASE,
/**
* Test refund lookup
*/
OC_REFUND_LOOKUP,
/**
* Authorize a tip.
*/
OC_TIP_AUTHORIZE,
/**
* Pickup a tip.
*/
OC_TIP_PICKUP,
/**
* Check pay status.
*/
OC_CHECK_PAYMENT,
/**
* Query tip stats.
*/
OC_TIP_QUERY,
};
/**
* Structure specifying details about a coin to be melted.
* Used in a NULL-terminated array as part of command
* specification.
*/
struct MeltDetails
{
/**
* Amount to melt (including fee).
*/
const char *amount;
/**
* Reference to reserve_withdraw operations for coin to
* be used for the /refresh/melt operation.
*/
const char *coin_ref;
};
/**
* State of the interpreter loop.
*/
struct InterpreterState;
/**
* Internal withdraw handle used when withdrawing tips.
*/
struct WithdrawHandle
{
/**
* Withdraw operation this handle represents.
*/
struct TALER_EXCHANGE_ReserveWithdrawHandle *wsh;
/**
* Interpreter state we are part of.
*/
struct InterpreterState *is;
/**
* Offset of this withdraw operation in the current @e is command.
*/
unsigned int off;
};
/**
* Details for a exchange operation to execute.
*/
struct Command
{
/**
* Opcode of the command.
*/
enum OpCode oc;
/**
* Label for the command, can be NULL.
*/
const char *label;
/**
* Which response code do we expect for this command?
*/
unsigned int expected_response_code;
/**
* Details about the command.
*/
union
{
/**
* Information for a #OC_ADMIN_ADD_INCOMING command.
*/
struct
{
/**
* Label to another admin_add_incoming command if we
* should deposit into an existing reserve, NULL if
* a fresh reserve should be created.
*/
const char *reserve_reference;
/**
* Instance to use if we are filling a tipping-reserve. In this
* case, @e reserve_priv is filled from the configuration instead
* of at random. Usually NULL (for random @e reserve_priv).
*/
const char *instance;
/**
* String describing the amount to add to the reserve.
*/
const char *amount;
/**
* Wire transfer subject. NULL to use public key corresponding
* to @e reserve_priv or @e reserve_reference. Should only be
* set manually to test invalid wire transfer subjects.
*/
const char *subject;
/**
* Sender (debit) account number.
*/
uint64_t debit_account_no;
/**
* Receiver (credit) account number.
*/
uint64_t credit_account_no;
/**
* Username to use for authentication.
*/
const char *auth_username;
/**
* Password to use for authentication.
*/
const char *auth_password;
/**
* Set (by the interpreter) to the reserve's private key
* we used to fill the reserve.
*/
struct TALER_ReservePrivateKeyP reserve_priv;
/**
* Set to the API's handle during the operation.
*/
struct TALER_BANK_AdminAddIncomingHandle *aih;
/**
* Set to the wire transfer's unique ID.
*/
uint64_t serial_id;
} admin_add_incoming;
/**
* Information for OC_PROPOSAL_LOOKUP command.
*/
struct
{
/**
* Reference to the proposal we want to lookup.
*/
const char *proposal_reference;
struct TALER_MERCHANT_ProposalLookupOperation *plo;
} proposal_lookup;
/**
* Information for a #OC_WITHDRAW_STATUS command.
*/
struct
{
/**
* Label to the #OC_ADMIN_ADD_INCOMING command which
* created the reserve.
*/
const char *reserve_reference;
/**
* Set to the API's handle during the operation.
*/
struct TALER_EXCHANGE_ReserveStatusHandle *wsh;
/**
* Expected reserve balance.
*/
const char *expected_balance;
} reserve_status;
/**
* Information for a #OC_WITHDRAW_SIGN command.
*/
struct
{
/**
* Which reserve should we withdraw from?
*/
const char *reserve_reference;
/**
* String describing the denomination value we should withdraw.
* A corresponding denomination key must exist in the exchange's
* offerings. Can be NULL if @e pk is set instead.
*/
const char *amount;
/**
* If @e amount is NULL, this specifies the denomination key to
* use. Otherwise, this will be set (by the interpreter) to the
* denomination PK matching @e amount.
*/
const struct TALER_EXCHANGE_DenomPublicKey *pk;
/**
* Set (by the interpreter) to the exchange's signature over the
* coin's public key.
*/
struct TALER_DenominationSignature sig;
/**
* Set (by the interpreter) to the planchet's secrets.
*/
struct TALER_PlanchetSecretsP ps;
/**
* Withdraw handle (while operation is running).
*/
struct TALER_EXCHANGE_ReserveWithdrawHandle *wsh;
} reserve_withdraw;
/**
* Information for an #OC_PROPOSAL command.
*/
struct
{
/**
* The order.
* It's dynamically generated because we need different transaction_id
* for different merchant instances.
*/
char order[ORDER_MAX_SIZE];
/**
* Handle to the active PUT /proposal operation, or NULL.
*/
struct TALER_MERCHANT_ProposalOperation *po;
/**
* Handle to the active GET /proposal operation, or NULL.
*/
struct TALER_MERCHANT_ProposalLookupOperation *plo;
/**
* Full contract in JSON, set by the /contract operation.
* FIXME: verify in the code that this bit is actually proposal
* data and not the whole proposal.
*/
json_t *contract_terms;
/**
* Proposal's signature.
*/
struct TALER_MerchantSignatureP merchant_sig;
/**
* Proposal data's hashcode.
*/
struct GNUNET_HashCode hash;
/**
* The nonce set by the customer looking up the contract
* the first time.
*/
struct GNUNET_CRYPTO_EddsaPublicKey nonce;
} proposal;
/**
* Information for a #OC_PAY command.
*/
struct
{
/**
* Reference to the contract.
*/
const char *contract_ref;
/**
* ";"-separated list of references to withdrawn coins to be used
* in the payment. Each reference has the syntax "LABEL[/NUMBER]"
* where NUMBER refers to a particular coin (in case multiple coins
* were created in a step).
*/
char *coin_ref;
/**
* Amount to pay (from the coin, including fee).
*/
const char *amount_with_fee;
/**
* Amount to pay (from the coin, excluding fee). The sum of the
* deltas between all @e amount_with_fee and the @e
* amount_without_fee must be less than max_fee, and the sum of
* the @e amount_with_fee must be larger than the @e
* total_amount.
*/
const char *amount_without_fee;
/**
* Refund fee to use for each coin (only relevant if we
* exercise /pay's abort functionality).
*/
const char *refund_fee;
/**
* Pay handle while operation is running.
*/
struct TALER_MERCHANT_Pay *ph;
/**
* Hashcode of the proposal data associated to this payment.
*/
struct GNUNET_HashCode h_contract_terms;
/**
* Merchant's public key
*/
struct TALER_MerchantPublicKeyP merchant_pub;
} pay;
struct {
/**
* Reference to the (incomplete) pay operation that is to be
* resumed.
*/
char *pay_ref;
/**
* ";"-separated list of references to additional withdrawn
* coins to be used in the payment. Each reference has the
* syntax "LABEL[/NUMBER]" where NUMBER refers to a particular
* coin (in case multiple coins were created in a step).
*/
char *coin_ref;
/**
* Pay handle while operation is running.
*/
struct TALER_MERCHANT_Pay *ph;
} pay_again;
struct {
/**
* Reference to the pay operation that is to be aborted.
*/
char *pay_ref;
/**
* Pay handle while operation is running.
*/
struct TALER_MERCHANT_Pay *ph;
/**
* Set in #pay_refund_cb to number of refunds obtained.
*/
unsigned int num_refunds;
/**
* Array of @e num_refund refunds obtained.
*/
struct TALER_MERCHANT_RefundEntry *res;
/**
* Set to the hash of the original contract.
*/
struct GNUNET_HashCode h_contract;
/**
* Set to the merchant's public key.
*/
struct TALER_MerchantPublicKeyP merchant_pub;
} pay_abort;
struct {
/**
* Reference to the pay_abort command to be refunded.
*/
const char *abort_ref;
/**
* Number of the coin of @e abort_ref to be refunded.
*/
unsigned int num_coin;
/**
* Refund amount to use.
*/
const char *refund_amount;
/**
* Refund fee to expect.
*/
const char *refund_fee;
/**
* Handle to the refund operation.
*/
struct TALER_EXCHANGE_RefundHandle *rh;
} pay_abort_refund;
struct {
/**
* Process for the aggregator.
*/
struct GNUNET_OS_Process *aggregator_proc;
/**
* ID of task called whenever we get a SIGCHILD.
*/
struct GNUNET_SCHEDULER_Task *child_death_task;
} run_aggregator;
struct {
/**
* Process for the wirewatcher.
*/
struct GNUNET_OS_Process *wirewatch_proc;
/**
* ID of task called whenever we get a SIGCHILD.
*/
struct GNUNET_SCHEDULER_Task *child_death_task;
} run_wirewatch;
struct {
/**
* Which amount do we expect to see transferred?
*/
const char *amount;
/**
* Which account do we expect to be debited?
*/
uint64_t account_debit;
/**
* Which account do we expect to be credited?
*/
uint64_t account_credit;
/**
* Set (!) to the wire transfer subject observed.
*/
char *subject;
} check_bank_transfer;
struct {
/**
* #OC_CHECK_BANK_TRANSFER command from which we should grab
* the WTID.
*/
char *check_bank_ref;
/**
* #OC_PAY command which we expect in the result.
* Since we are tracking a bank transaction, we want to know
* which (Taler) deposit is associated with the bank
* transaction being tracked now.
*/
char *expected_pay_ref;
/**
* Handle to a /track/transfer operation
*/
struct TALER_MERCHANT_TrackTransferHandle *tdo;
} track_transfer;
struct {
/**
* #OC_PAY command from which we should grab
* the WTID.
*/
char *pay_ref;
/**
* #OC_CHECK_BANK_TRANSFER command which we expect in the result.
*/
char *expected_transfer_ref;
/**
* Wire fee we expect to pay for this transaction.
*/
const char *wire_fee;
/**
* Handle to a /track/transaction operation
*/
struct TALER_MERCHANT_TrackTransactionHandle *tth;
} track_transaction;
struct {
/**
* Date we want retrieved transactions younger than
*/
struct GNUNET_TIME_Absolute date;
/**
* How many "rows" we expect in the result
*/
unsigned int nresult;
/**
* Handle to /history request
*/
struct TALER_MERCHANT_HistoryOperation *ho;
/**
* The backend will return records with row_id
* less than this value.
*/
unsigned int start;
/**
* The backend will return at most `nrows` records.
*/
unsigned int nrows;
} history;
struct {
/**
* Reference to the order we want reimbursed
*/
char *order_id;
/**
* Handle to a refund increase operation
*/
struct TALER_MERCHANT_RefundIncreaseOperation *rio;
/**
* Amount to refund
*/
const char *refund_amount;
/**
* Reason for refunding
*/
const char *reason;
/**
* Refund fee (MUST match the value given in config)
*/
const char *refund_fee;
} refund_increase;
struct {
/**
* Reference to the order whose refund was increased
*/
char *order_id;
/**
* Handle to the operation
*/
struct TALER_MERCHANT_RefundLookupOperation *rlo;
/**
* Used to retrieve the asked refund amount.
* This information helps the callback to mock a GET /refund
* response and match it against what the backend actually
* responded.
*/
char *increase_ref;
/**
* Used to retrieve the number and denomination of coins
* used to pay for the related contract.
* This information helps the callback to mock a GET /refund
* response and match it against what the backend actually
* responded.
*/
char *pay_ref;
} refund_lookup;
struct {
/**
* Specify the instance (to succeed, this must match a prior
* enable action and the respective wire transfer's instance).
*/
const char *instance;
/**
* Reason to use for enabling the tip (required by the API, but not
* yet really useful as we do not have a way to read back the
* justifications stored in the merchant's DB).
*/
const char *justification;
/**
* How much should the tip be?
*/
const char *amount;
/**
* Handle for the ongoing operation.
*/
struct TALER_MERCHANT_TipAuthorizeOperation *tao;
/**
* Unique ID for the authorized tip, set by the interpreter.
*/
struct GNUNET_HashCode tip_id;
/**
* When does the authorization expire?
*/
struct GNUNET_TIME_Absolute tip_expiration;
/**
* EC expected for the operation.
*/
enum TALER_ErrorCode expected_ec;
} tip_authorize;
struct {
/**
* Reference to operation that authorized the tip. Used
* to obtain the `tip_id`.
*/
const char *authorize_ref;
/**
* Set to non-NULL to a label of another pick up operation
* that we should replay.
*/
const char *replay_ref;
/**
* Number of coins we pick up.
*/
unsigned int num_coins;
/**
* Array of @e num_coins denominations of the coins we pick up.
*/
const char **amounts;
/**
* Handle for the ongoing operation.
*/
struct TALER_MERCHANT_TipPickupOperation *tpo;
/**
* Temporary data structure to store the blinding keys while the
* pickup operation runs.
*/
struct TALER_PlanchetSecretsP *psa;
/**
* Array of denomination keys matching the @e amounts.
*/
const struct TALER_EXCHANGE_DenomPublicKey **dks;
/**
* Temporary data structure of @e num_coins entries for the
* withdraw operations.
*/
struct WithdrawHandle *withdraws;
/**
* Set (by the interpreter) to an array of @a num_coins signatures
* created from the (successful) tip operation.
*/
struct TALER_DenominationSignature *sigs;
/**
* EC expected for the operation.
*/
enum TALER_ErrorCode expected_ec;
} tip_pickup;
struct {
/**
* Expected available amount (in string format).
* NULL to skip check.
*/
char *expected_amount_available;
/**
* Expected picked up amount (in string format).
* NULL to skip check.
*/
char *expected_amount_picked_up;
/**
* Expected authorized amount (in string format).
* NULL to skip check.
*/
char *expected_amount_authorized;
/**
* Handle for the ongoing operation.
*/
struct TALER_MERCHANT_TipQueryOperation *tqo;
/**
* Merchant instance to use for tipping.
*/
char *instance;
} tip_query;
struct {
/**
* Reference for the contract we want to check.
*/
const char *contract_ref;
/**
* Whether to expect the payment to be settled or not.
*/
int expect_paid;
/**
* Operation handle for the /check-payment request,
* NULL if operation is not running.
*/
struct TALER_MERCHANT_CheckPaymentOperation *cpo;
} check_payment;
} details;
};
/**
* State of the interpreter loop.
*/
struct InterpreterState
{
/**
* Keys from the exchange.
*/
const struct TALER_EXCHANGE_Keys *keys;
/**
* Commands the interpreter will run.
*/
struct Command *commands;
/**
* Interpreter task (if one is scheduled).
*/
struct GNUNET_SCHEDULER_Task *task;
/**
* Instruction pointer. Tells #interpreter_run() which
* instruction to run next.
*/
unsigned int ip;
};
/**
* Pipe used to communicate child death via signal.
*/
static struct GNUNET_DISK_PipeHandle *sigpipe;
/**
* Return instance private key from config
*
* @param config configuration handle
* @param instance instance name
* @return pointer to private key, NULL on error
*/
struct GNUNET_CRYPTO_EddsaPrivateKey *
get_instance_priv (struct GNUNET_CONFIGURATION_Handle *config,
const char *instance)
{
char *config_section;
char *filename;
struct GNUNET_CRYPTO_EddsaPrivateKey *ret;
(void) GNUNET_asprintf (&config_section,
"merchant-instance-%s",
instance);
if (GNUNET_OK !=
GNUNET_CONFIGURATION_get_value_filename (config,
config_section,
"KEYFILE",
&filename))
{
GNUNET_break (0);
GNUNET_free (config_section);
return NULL;
}
GNUNET_free (config_section);
if (NULL ==
(ret = GNUNET_CRYPTO_eddsa_key_create_from_file (filename)))
GNUNET_break (0);
GNUNET_free (filename);
return ret;
}
/**
* The testcase failed, return with an error code.
*
* @param is interpreter state to clean up
*/
static void
fail (struct InterpreterState *is)
{
GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
"Interpreter failed at step %s (#%u)\n",
is->commands[is->ip].label,
is->ip);
result = GNUNET_SYSERR;
GNUNET_SCHEDULER_shutdown ();
}
/**
* Find a command by label.
*
* @param is interpreter state to search
* @param label label to look for
* @return NULL if command was not found
*/
static const struct Command *
find_command (const struct InterpreterState *is,
const char *label)
{
const struct Command *cmd;
if (NULL == label)
{
GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
"Attempt to lookup command for empty label\n");
return NULL;
}
for (unsigned int i=0;
OC_END != (cmd = &is->commands[i])->oc;
i++)
if ( (NULL != cmd->label) &&
(0 == strcmp (cmd->label,
label)) )
return cmd;
GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
"Command not found: %s\n",
label);
return NULL;
}
/**
* Run the main interpreter loop that performs exchange operations.
*
* @param cls contains the `struct InterpreterState`
*/
static void
interpreter_run (void *cls);
/**
* Run the next command with the interpreter.
*
* @param is current interpeter state.
*/
static void
next_command (struct InterpreterState *is)
{
is->ip++;
is->task = GNUNET_SCHEDULER_add_now (&interpreter_run,
is);
}
/**
* Function called upon completion of our /admin/add/incoming request.
*
* @param cls closure with the interpreter state
* @param http_status HTTP response code, #MHD_HTTP_OK (200) for successful status request
* 0 if the exchange's reply is bogus (fails to follow the protocol)
* @param ec taler-specific error code, #TALER_EC_NONE on success
* @param serial_id unique ID of the wire transfer
* @param full_response full response from the exchange (for logging, in case of errors)
*/
static void
add_incoming_cb (void *cls,
unsigned int http_status,
enum TALER_ErrorCode ec,
uint64_t serial_id,
const json_t *full_response)
{
struct InterpreterState *is = cls;
struct Command *cmd = &is->commands[is->ip];
cmd->details.admin_add_incoming.aih = NULL;
cmd->details.admin_add_incoming.serial_id = serial_id;
if (MHD_HTTP_OK != http_status)
{
GNUNET_break (0);
fail (is);
return;
}
next_command (is);
}
/**
* Parse given JSON object to absolute time.
*
* @param root the json object representing data
* @param[out] ret where to write the data
* @return #GNUNET_OK upon successful parsing; #GNUNET_SYSERR upon error
*/
static int
parse_abs_time (json_t *root,
struct GNUNET_TIME_Absolute *ret)
{
const char *val;
unsigned long long int tval;
val = json_string_value (root);
if (NULL == val)
{
GNUNET_break_op (0);
return GNUNET_SYSERR;
}
if ( (0 == strcasecmp (val,
"/forever/")) ||
(0 == strcasecmp (val,
"/end of time/")) ||
(0 == strcasecmp (val,
"/never/")) )
{
*ret = GNUNET_TIME_UNIT_FOREVER_ABS;
return GNUNET_OK;
}
if (1 != sscanf (val,
"/Date(%llu)/",
&tval))
{
GNUNET_break_op (0);
return GNUNET_SYSERR;
}
/* Time is in seconds in JSON, but in microseconds in GNUNET_TIME_Absolute */
ret->abs_value_us = tval * 1000LL * 1000LL;
if ( (ret->abs_value_us) / 1000LL / 1000LL != tval)
{
/* Integer overflow */
GNUNET_break_op (0);
return GNUNET_SYSERR;
}
return GNUNET_OK;
}
/**
* Callback for a /history request. It's up to this function how
* to render the array containing transactions details (FIXME link to
* documentation)
*
* @param cls closure
* @param http_status HTTP status returned by the merchant backend
* @param ec taler-specific error code
* @param json actual body containing history
*/
static void
history_cb (void *cls,
unsigned int http_status,
enum TALER_ErrorCode ec,
const json_t *json)
{
struct InterpreterState *is = cls;
struct Command *cmd = &is->commands[is->ip];
unsigned int nresult;
struct GNUNET_TIME_Absolute last_timestamp;
struct GNUNET_TIME_Absolute entry_timestamp;
cmd->details.history.ho = NULL;
if (MHD_HTTP_OK != http_status)
{
fail (is);
return;
}
nresult = json_array_size (json);
if (nresult != cmd->details.history.nresult)
{
GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
"Unexpected number of history entries. Got %d, expected %d\n",
nresult,
cmd->details.history.nresult);
fail (is);
return;
}
last_timestamp = GNUNET_TIME_absolute_get ();
last_timestamp = GNUNET_TIME_absolute_add (last_timestamp,
GNUNET_TIME_UNIT_DAYS);
json_t *entry;
json_t *timestamp;
size_t index;
json_array_foreach (json, index, entry)
{
timestamp = json_object_get (entry, "timestamp");
if (GNUNET_OK != parse_abs_time (timestamp, &entry_timestamp))
{
GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Bad timestamp given\n");
fail (is);
return;
}
/* entry_timestamp should always become last_timestamp */
entry_timestamp = GNUNET_TIME_absolute_max (last_timestamp, entry_timestamp);
if (last_timestamp.abs_value_us != entry_timestamp.abs_value_us)
{
GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
"History entries are NOT sorted from younger to older\n");
fail (is);
return;
}
}
next_command (is);
}
/**
* Check if the given historic event @a h corresponds to the given
* command @a cmd.
*
* @param h event in history
* @param cmd an #OC_ADMIN_ADD_INCOMING command
* @return #GNUNET_OK if they match, #GNUNET_SYSERR if not
*/
static int
compare_admin_add_incoming_history (const struct TALER_EXCHANGE_ReserveHistory *h,
const struct Command *cmd)
{
struct TALER_Amount amount;
if (TALER_EXCHANGE_RTT_DEPOSIT != h->type)
{
GNUNET_break_op (0);
return GNUNET_SYSERR;
}
GNUNET_assert (GNUNET_OK ==
TALER_string_to_amount (cmd->details.admin_add_incoming.amount,
&amount));
if (0 != TALER_amount_cmp (&amount,
&h->amount))
{
GNUNET_break_op (0);
return GNUNET_SYSERR;
}
return GNUNET_OK;
}
/**
* Check if the given historic event @a h corresponds to the given
* command @a cmd.
*
* @param h event in history
* @param cmd an #OC_WITHDRAW_SIGN command
* @return #GNUNET_OK if they match, #GNUNET_SYSERR if not
*/
static int
compare_reserve_withdraw_history (const struct TALER_EXCHANGE_ReserveHistory *h,
const struct Command *cmd)
{
struct TALER_Amount amount;
struct TALER_Amount amount_with_fee;
if (TALER_EXCHANGE_RTT_WITHDRAWAL != h->type)
{
GNUNET_break_op (0);
return GNUNET_SYSERR;
}
GNUNET_assert (GNUNET_OK ==
TALER_string_to_amount (cmd->details.reserve_withdraw.amount,
&amount));
GNUNET_assert (GNUNET_OK ==
TALER_amount_add (&amount_with_fee,
&amount,
&cmd->details.reserve_withdraw.pk->fee_withdraw));
if (0 != TALER_amount_cmp (&amount_with_fee,
&h->amount))
{
GNUNET_break_op (0);
return GNUNET_SYSERR;
}
return GNUNET_OK;
}
/**
* Function called with the result of a /reserve/status request.
*
* @param cls closure with the interpreter state
* @param http_status HTTP response code, #MHD_HTTP_OK (200) for successful status request
* 0 if the exchange's reply is bogus (fails to follow the protocol)
* @param[in] json original response in JSON format (useful only for diagnostics)
* @param balance current balance in the reserve, NULL on error
* @param history_length number of entries in the transaction history, 0 on error
* @param history detailed transaction history, NULL on error
*/
static void
reserve_status_cb (void *cls,
unsigned int http_status,
enum TALER_ErrorCode ec,
const json_t *json,
const struct TALER_Amount *balance,
unsigned int history_length,
const struct TALER_EXCHANGE_ReserveHistory *history)
{
struct InterpreterState *is = cls;
struct Command *cmd = &is->commands[is->ip];
struct Command *rel;
unsigned int j;
struct TALER_Amount amount;
cmd->details.reserve_status.wsh = NULL;
if (cmd->expected_response_code != http_status)
{
GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
"Unexpected response code %u (%d) to command %s\n",
http_status,
ec,
cmd->label);
GNUNET_break (0);
json_dumpf (json, stderr, 0);
fail (is);
return;
}
switch (http_status)
{
case MHD_HTTP_OK:
j = 0;
for (unsigned int i=0;iip;i++)
{
switch ((rel = &is->commands[i])->oc)
{
case OC_ADMIN_ADD_INCOMING:
/**
* If the command being iterated over filled a reserve AND
* it is the one referenced by the current "history command"
* ...
*/
if ( ( (NULL != rel->label) &&
(0 == strcmp (cmd->details.reserve_status.reserve_reference,
rel->label) ) ) ||
( (NULL != rel->details.admin_add_incoming.reserve_reference) &&
(0 == strcmp (cmd->details.reserve_status.reserve_reference,
rel->details.admin_add_incoming.reserve_reference) ) ) )
{
/**
* ... then make sure the history element mentions a "deposit
* operation" on that reserve.
*/
if (GNUNET_OK != compare_admin_add_incoming_history (&history[j],
rel))
{
GNUNET_break (0);
fail (is);
return;
}
j++;
}
break;
case OC_WITHDRAW_SIGN:
/**
* If the command being iterated over emptied a reserve AND
* it is the one referenced by the current "history command"
* ...
*/
if (0 == strcmp (cmd->details.reserve_status.reserve_reference,
rel->details.reserve_withdraw.reserve_reference))
{
/**
* ... then make sure the history element mentions a "withdraw
* operation" on that reserve.
*/
if (GNUNET_OK != compare_reserve_withdraw_history (&history[j],
rel))
{
GNUNET_break (0);
fail (is);
return;
}
j++;
}
break;
default:
/* unreleated, just skip */
break;
}
}
if (j != history_length)
{
GNUNET_break (0);
fail (is);
return;
}
if (NULL != cmd->details.reserve_status.expected_balance)
{
GNUNET_assert (GNUNET_OK ==
TALER_string_to_amount (cmd->details.reserve_status.expected_balance,
&amount));
if (0 != TALER_amount_cmp (&amount,
balance))
{
GNUNET_break (0);
fail (is);
return;
}
}
break;
default:
/* Unsupported status code (by test harness) */
GNUNET_break (0);
break;
}
next_command (is);
}
/**
* Function called upon completion of our /reserve/withdraw request.
*
* @param cls closure with the interpreter state
* @param http_status HTTP response code, #MHD_HTTP_OK (200) for successful status request
* 0 if the exchange's reply is bogus (fails to follow the protocol)
* @param ec taler-specific error code
* @param sig signature over the coin, NULL on error
* @param full_response full response from the exchange (for logging, in case of errors)
*/
static void
reserve_withdraw_cb (void *cls,
unsigned int http_status,
enum TALER_ErrorCode ec,
const struct TALER_DenominationSignature *sig,
const json_t *full_response)
{
struct InterpreterState *is = cls;
struct Command *cmd = &is->commands[is->ip];
cmd->details.reserve_withdraw.wsh = NULL;
if (cmd->expected_response_code != http_status)
{
GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
"Unexpected response code %u (%d) to command %s\n",
http_status,
ec,
cmd->label);
fail (is);
return;
}
switch (http_status)
{
case MHD_HTTP_OK:
if (NULL == sig)
{
GNUNET_break (0);
fail (is);
return;
}
/**
* NOTE: this assert is OK on the second instance run because the
* interpreter is "cleaned" by cleanup_state()
*/
GNUNET_assert (NULL == cmd->details.reserve_withdraw.sig.rsa_signature);
cmd->details.reserve_withdraw.sig.rsa_signature
= GNUNET_CRYPTO_rsa_signature_dup (sig->rsa_signature);
break;
case MHD_HTTP_PAYMENT_REQUIRED:
/* nothing to check */
break;
default:
/* Unsupported status code (by test harness) */
GNUNET_break (0);
break;
}
next_command (is);
}
/**
* Callback for GET /proposal issued at backend.
* Used to initialize the proposal after it was created.
*
* @param cls closure
* @param http_status HTTP status code we got
* @param json full response we got
*/
static void
proposal_lookup_initial_cb (void *cls,
unsigned int http_status,
const json_t *json,
const json_t *contract_terms,
const struct TALER_MerchantSignatureP *sig,
const struct GNUNET_HashCode *hash)
{
struct InterpreterState *is = cls;
struct Command *cmd = &is->commands[is->ip];
if (cmd->expected_response_code != http_status)
{
fail (is);
return;
}
cmd->details.proposal.hash = *hash;
cmd->details.proposal.merchant_sig = *sig;
cmd->details.proposal.contract_terms = json_deep_copy (contract_terms);
cmd->details.proposal.plo = NULL;
next_command (is);
}
/**
* Callback that works POST /proposal's output.
*
* @param cls closure
* @param http_status HTTP response code, 200 indicates success;
* 0 if the backend's reply is bogus (fails to follow the protocol)
* @param ec taler-specific error code
* @param obj the full received JSON reply, or
* error details if the request failed
* @param contract_terms the order + additional information provided by the
* backend, NULL on error.
* @param sig merchant's signature over the contract, NULL on error
* @param h_contract hash of the contract, NULL on error
*/
static void
proposal_cb (void *cls,
unsigned int http_status,
enum TALER_ErrorCode ec,
const json_t *obj,
const char *order_id)
{
struct InterpreterState *is = cls;
struct Command *cmd = &is->commands[is->ip];
cmd->details.proposal.po = NULL;
switch (http_status)
{
case MHD_HTTP_OK:
break;
default: {
char *s = json_dumps (obj, JSON_COMPACT);
GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
"Unexpected status code from /proposal: %u (%d). Step %u, response: %s\n",
http_status,
ec,
is->ip,
s);
GNUNET_free_non_null (s);
fail (is);
}
return;
}
if (NULL == (cmd->details.proposal.plo
= TALER_MERCHANT_proposal_lookup (ctx,
MERCHANT_URL,
order_id,
instance,
&cmd->details.proposal.nonce,
proposal_lookup_initial_cb,
is)))
{
GNUNET_break (0);
fail (is);
}
}
/**
* Process POST /refund (increase) response
*
* @param cls closure
* @param http_status HTTP status code
* @param ec taler-specific error object
* @param obj response body; is NULL on success.
*/
static void
refund_increase_cb (void *cls,
unsigned int http_status,
enum TALER_ErrorCode ec,
const json_t *obj)
{
struct InterpreterState *is = cls;
struct Command *cmd = &is->commands[is->ip];
if (MHD_HTTP_OK != http_status)
{
GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
"Refund increase failed\n");
fail (is);
return;
}
cmd->details.refund_increase.rio = NULL;
next_command (is);
}
/**
* Callback that frees all the elements in the hashmap
*
* @param cls closure, NULL
* @param key current key
* @param value a `struct TALER_Amount`
* @return always #GNUNET_YES (continue to iterate)
*/
static int
hashmap_free (void *cls,
const struct GNUNET_HashCode *key,
void *value)
{
struct TALER_Amount *refund_amount = value;
GNUNET_free (refund_amount);
return GNUNET_YES;
}
/**
* Process GET /refund (increase) response.
*
* @param cls closure
* @param http_status HTTP status code
* @param ec taler-specific error object
* @param obj response body; is NULL on error.
*/
static void
refund_lookup_cb (void *cls,
unsigned int http_status,
enum TALER_ErrorCode ec,
const json_t *obj)
{
struct GNUNET_CONTAINER_MultiHashMap *map;
struct InterpreterState *is = cls;
struct Command *cmd = &is->commands[is->ip];
size_t index;
json_t *elem;
const char *error_name;
unsigned int error_line;
struct GNUNET_HashCode h_coin_pub;
char *icoin_ref;
char *icoin_refs;
const struct Command *icoin;
const struct Command *pay;
struct TALER_CoinSpendPublicKeyP icoin_pub;
struct GNUNET_HashCode h_icoin_pub;
struct TALER_Amount *iamount;
struct TALER_Amount acc;
const struct Command *increase;
struct TALER_Amount refund_amount;
const json_t *arr;
cmd->details.refund_lookup.rlo = NULL;
if (MHD_HTTP_OK != http_status)
{
GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
"Refund lookup failed\n");
fail (is);
return;
}
map = GNUNET_CONTAINER_multihashmap_create (1, GNUNET_NO);
arr = json_object_get (obj,
"refund_permissions");
if (NULL == arr)
{
GNUNET_break (0);
fail (is);
return;
}
json_array_foreach (arr, index, elem)
{
struct TALER_CoinSpendPublicKeyP coin_pub;
struct TALER_Amount *irefund_amount
= GNUNET_new (struct TALER_Amount);
struct GNUNET_JSON_Specification spec[] = {
GNUNET_JSON_spec_fixed_auto ("coin_pub", &coin_pub),
TALER_JSON_spec_amount ("refund_amount", irefund_amount),
GNUNET_JSON_spec_end ()
};
GNUNET_assert (GNUNET_OK ==
GNUNET_JSON_parse (elem,
spec,
&error_name,
&error_line));
GNUNET_CRYPTO_hash (&coin_pub,
sizeof (struct TALER_CoinSpendPublicKeyP),
&h_coin_pub);
GNUNET_assert (GNUNET_OK ==
GNUNET_CONTAINER_multihashmap_put (map,
&h_coin_pub,
irefund_amount,
GNUNET_CONTAINER_MULTIHASHMAPOPTION_UNIQUE_ONLY));
};
/* Retrieve coins used to pay, from #OC_PAY command */
GNUNET_assert (NULL != (pay =
find_command (is,
cmd->details.refund_lookup.pay_ref)));
icoin_refs = GNUNET_strdup (pay->details.pay.coin_ref);
GNUNET_assert (GNUNET_OK ==
TALER_amount_get_zero ("EUR",
&acc));
for (icoin_ref = strtok (icoin_refs, ";");
NULL != icoin_ref;
icoin_ref = strtok (NULL, ";"))
{
GNUNET_assert (NULL != (icoin =
find_command (is,
icoin_ref)));
GNUNET_CRYPTO_eddsa_key_get_public (&icoin->details.reserve_withdraw.ps.coin_priv.eddsa_priv,
&icoin_pub.eddsa_pub);
GNUNET_CRYPTO_hash (&icoin_pub,
sizeof (struct TALER_CoinSpendPublicKeyP),
&h_icoin_pub);
/* Can be NULL: not all coins are involved in refund */
iamount = GNUNET_CONTAINER_multihashmap_get (map,
&h_icoin_pub);
if (NULL == iamount)
continue;
GNUNET_assert (GNUNET_OK ==
TALER_amount_add (&acc,
&acc,
iamount));
}
GNUNET_free (icoin_refs);
/* Check if refund has been 100% covered */
GNUNET_assert (increase =
find_command (is,
cmd->details.refund_lookup.increase_ref));
GNUNET_assert (GNUNET_OK ==
TALER_string_to_amount (increase->details.refund_increase.refund_amount,
&refund_amount));
GNUNET_CONTAINER_multihashmap_iterate (map,
&hashmap_free,
NULL);
GNUNET_CONTAINER_multihashmap_destroy (map);
if (0 != TALER_amount_cmp (&acc,
&refund_amount))
{
GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
"Incomplete refund: expected '%s', got '%s'\n",
TALER_amount_to_string (&refund_amount),
TALER_amount_to_string (&acc));
fail (is);
return;
}
next_command (is);
}
/**
* Function called with the result of a /pay operation.
*
* @param cls closure with the interpreter state
* @param http_status HTTP response code, #MHD_HTTP_OK (200) for successful deposit;
* 0 if the exchange's reply is bogus (fails to follow the protocol)
* @param ec taler-specific error object
* @param obj the received JSON reply, should be kept as proof (and, in case of errors,
* be forwarded to the customer)
*/
static void
pay_cb (void *cls,
unsigned int http_status,
enum TALER_ErrorCode ec,
const json_t *obj)
{
struct InterpreterState *is = cls;
struct Command *cmd = &is->commands[is->ip];
struct GNUNET_CRYPTO_EddsaSignature sig;
const char *error_name;
unsigned int error_line;
cmd->details.pay.ph = NULL;
if (cmd->expected_response_code != http_status)
{
GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
"Unexpected response code %u (%d) to command %s\n",
http_status,
ec,
cmd->label);
fail (is);
return;
}
if (MHD_HTTP_OK == http_status)
{
/* Check signature */
struct PaymentResponsePS mr;
struct GNUNET_JSON_Specification spec[] = {
GNUNET_JSON_spec_fixed_auto ("sig", &sig),
GNUNET_JSON_spec_fixed_auto ("h_contract_terms",
&cmd->details.pay.h_contract_terms),
GNUNET_JSON_spec_end ()
};
GNUNET_assert (GNUNET_OK ==
GNUNET_JSON_parse (obj,
spec,
&error_name,
&error_line));
mr.purpose.purpose = htonl (TALER_SIGNATURE_MERCHANT_PAYMENT_OK);
mr.purpose.size = htonl (sizeof (mr));
mr.h_contract_terms = cmd->details.pay.h_contract_terms;
if (GNUNET_OK !=
GNUNET_CRYPTO_eddsa_verify (TALER_SIGNATURE_MERCHANT_PAYMENT_OK,
&mr.purpose,
&sig,
&cmd->details.pay.merchant_pub.eddsa_pub))
{
GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
"Merchant signature given in response to /pay invalid\n");
fail (is);
return;
}
}
next_command (is);
}
/**
* Function called with the result of a /pay again operation.
*
* @param cls closure with the interpreter state
* @param http_status HTTP response code, #MHD_HTTP_OK (200) for successful deposit;
* 0 if the exchange's reply is bogus (fails to follow the protocol)
* @param ec taler-specific error object
* @param obj the received JSON reply, should be kept as proof (and, in case of errors,
* be forwarded to the customer)
*/
static void
pay_again_cb (void *cls,
unsigned int http_status,
enum TALER_ErrorCode ec,
const json_t *obj)
{
struct InterpreterState *is = cls;
struct Command *cmd = &is->commands[is->ip];
struct GNUNET_CRYPTO_EddsaSignature sig;
const char *error_name;
unsigned int error_line;
const struct Command *pref;
cmd->details.pay_again.ph = NULL;
if (cmd->expected_response_code != http_status)
{
GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
"Unexpected response code %u (%d) to command %s\n",
http_status,
ec,
cmd->label);
fail (is);
return;
}
GNUNET_assert (NULL != (pref = find_command
(is,
cmd->details.pay_again.pay_ref)));
if (MHD_HTTP_OK == http_status)
{
struct PaymentResponsePS mr;
/* Check signature */
struct GNUNET_JSON_Specification spec[] = {
GNUNET_JSON_spec_fixed_auto ("sig",
&sig),
GNUNET_JSON_spec_fixed_auto ("h_contract_terms",
&mr.h_contract_terms),
GNUNET_JSON_spec_end ()
};
GNUNET_assert (GNUNET_OK ==
GNUNET_JSON_parse (obj,
spec,
&error_name,
&error_line));
mr.purpose.purpose = htonl (TALER_SIGNATURE_MERCHANT_PAYMENT_OK);
mr.purpose.size = htonl (sizeof (mr));
if (GNUNET_OK !=
GNUNET_CRYPTO_eddsa_verify (TALER_SIGNATURE_MERCHANT_PAYMENT_OK,
&mr.purpose,
&sig,
&pref->details.pay.merchant_pub.eddsa_pub))
{
GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
"Merchant signature given in response to /pay invalid\n");
fail (is);
return;
}
}
next_command (is);
}
/**
* Task triggered whenever we receive a SIGCHLD (child
* process died).
*
* @param cls closure, NULL if we need to self-restart
*/
static void
maint_child_death (void *cls)
{
struct InterpreterState *is = cls;
struct Command *cmd = &is->commands[is->ip];
const struct GNUNET_DISK_FileHandle *pr;
char c[16];
switch (cmd->oc) {
case OC_RUN_AGGREGATOR:
cmd->details.run_aggregator.child_death_task = NULL;
pr = GNUNET_DISK_pipe_handle (sigpipe,
GNUNET_DISK_PIPE_END_READ);
GNUNET_break (0 < GNUNET_DISK_file_read (pr,
&c,
sizeof (c)));
GNUNET_OS_process_wait (cmd->details.run_aggregator.aggregator_proc);
GNUNET_OS_process_destroy (cmd->details.run_aggregator.aggregator_proc);
cmd->details.run_aggregator.aggregator_proc = NULL;
break;
case OC_RUN_WIREWATCH:
cmd->details.run_wirewatch.child_death_task = NULL;
pr = GNUNET_DISK_pipe_handle (sigpipe, GNUNET_DISK_PIPE_END_READ);
GNUNET_break (0 < GNUNET_DISK_file_read (pr, &c, sizeof (c)));
GNUNET_OS_process_wait (cmd->details.run_wirewatch.wirewatch_proc);
GNUNET_OS_process_destroy (cmd->details.run_wirewatch.wirewatch_proc);
cmd->details.run_wirewatch.wirewatch_proc = NULL;
break;
default:
GNUNET_break (0);
fail (is);
return;
}
next_command (is);
}
/**
* Callback for a /track/transfer operation
*
* @param cls closure for this function
* @param http_status HTTP response code returned by the server
* @param ec taler-specific error code
* @param sign_key exchange key used to sign @a json, or NULL
* @param json original json reply (may include signatures, those have then been
* validated already)
* @param h_wire hash of the wire transfer address the transfer went to, or NULL on error
* @param total_amount total amount of the wire transfer, or NULL if the exchange could
* not provide any @a wtid (set only if @a http_status is #MHD_HTTP_OK)
* @param details_length length of the @a details array
* @param details array with details about the combined transactions
*/
static void
track_transfer_cb (void *cls,
unsigned int http_status,
enum TALER_ErrorCode ec,
const struct TALER_ExchangePublicKeyP *sign_key,
const json_t *json,
const struct GNUNET_HashCode *h_wire,
const struct TALER_Amount *total_amount,
unsigned int details_length,
const struct TALER_MERCHANT_TrackTransferDetails *details)
{
struct InterpreterState *is = cls;
struct Command *cmd = &is->commands[is->ip];
cmd->details.track_transfer.tdo = NULL;
if (cmd->expected_response_code != http_status)
{
GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
"Unexpected response code %u (%d) to command %s\n",
http_status,
ec,
cmd->label);
fail (is);
return;
}
switch (http_status)
{
case MHD_HTTP_OK:
break;
default:
GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
"Unhandled HTTP status.\n");
}
next_command (is);
}
/**
* Callback for GET /proposal issued at backend. Just check
* whether response code is as expected.
*
* @param cls closure
* @param http_status HTTP status code we got
* @param json full response we got
*/
static void
proposal_lookup_cb (void *cls,
unsigned int http_status,
const json_t *json,
const json_t *contract_terms,
const struct TALER_MerchantSignatureP *sig,
const struct GNUNET_HashCode *hash)
{
struct InterpreterState *is = cls;
struct Command *cmd = &is->commands[is->ip];
cmd->details.proposal_lookup.plo = NULL;
if (cmd->expected_response_code != http_status)
fail (is);
next_command (is);
}
/**
* Callback for GET /proposal issued at backend. Just check
* whether response code is as expected.
*
* @param cls closure
* @param http_status HTTP status code we got
* @param json full response we got
*/
static void
check_payment_cb (void *cls,
unsigned int http_status,
const json_t *obj,
int paid,
int refunded,
struct TALER_Amount *refund_amount,
const char *payment_redirect_url)
{
struct InterpreterState *is = cls;
struct Command *cmd = &is->commands[is->ip];
GNUNET_log (GNUNET_ERROR_TYPE_INFO, "check payment: expected paid: %s: %d\n",
cmd->label,
cmd->details.check_payment.expect_paid);
GNUNET_log (GNUNET_ERROR_TYPE_INFO, "check payment: paid: %d\n",
paid);
GNUNET_log (GNUNET_ERROR_TYPE_INFO, "check payment: url: %s\n",
payment_redirect_url);
cmd->details.check_payment.cpo = NULL;
if (paid != cmd->details.check_payment.expect_paid)
{
GNUNET_break (0);
fail (is);
return;
}
if (cmd->expected_response_code != http_status)
fail (is);
next_command (is);
}
/**
* Callback to process a GET /tip-query request
*
* @param cls closure
* @param http_status HTTP status code for this request
* @param ec Taler-specific error code
* @param raw raw response body
*/
static void
tip_query_cb (void *cls,
unsigned int http_status,
enum TALER_ErrorCode ec,
const json_t *raw,
struct GNUNET_TIME_Absolute reserve_expiration,
struct TALER_ReservePublicKeyP *reserve_pub,
struct TALER_Amount *amount_authorized,
struct TALER_Amount *amount_available,
struct TALER_Amount *amount_picked_up)
{
struct InterpreterState *is = cls;
struct Command *cmd = &is->commands[is->ip];
struct TALER_Amount a;
cmd->details.tip_query.tqo = NULL;
GNUNET_log (GNUNET_ERROR_TYPE_INFO,
"Tip query callback at command %u/%s(%u)\n",
is->ip,
cmd->label,
cmd->oc);
GNUNET_assert (NULL != reserve_pub);
GNUNET_assert (NULL != amount_authorized);
GNUNET_assert (NULL != amount_available);
GNUNET_assert (NULL != amount_picked_up);
if (cmd->details.tip_query.expected_amount_available)
{
GNUNET_assert (GNUNET_OK ==
TALER_string_to_amount (cmd->details.tip_query.expected_amount_available, &a));
GNUNET_log (GNUNET_ERROR_TYPE_INFO, "expected available %s, actual %s\n",
TALER_amount_to_string (&a),
TALER_amount_to_string (amount_available));
GNUNET_assert (0 == TALER_amount_cmp (amount_available, &a));
}
if (cmd->details.tip_query.expected_amount_authorized)
{
GNUNET_assert (GNUNET_OK ==
TALER_string_to_amount (cmd->details.tip_query.expected_amount_authorized, &a));
GNUNET_log (GNUNET_ERROR_TYPE_INFO, "expected authorized %s, actual %s\n",
TALER_amount_to_string (&a),
TALER_amount_to_string (amount_authorized));
GNUNET_assert (0 == TALER_amount_cmp (amount_authorized, &a));
}
if (cmd->details.tip_query.expected_amount_picked_up)
{
GNUNET_assert (GNUNET_OK ==
TALER_string_to_amount (cmd->details.tip_query.expected_amount_picked_up, &a));
GNUNET_log (GNUNET_ERROR_TYPE_INFO, "expected picked_up %s, actual %s\n",
TALER_amount_to_string (&a),
TALER_amount_to_string (amount_picked_up));
GNUNET_assert (0 == TALER_amount_cmp (amount_picked_up, &a));
}
if (cmd->expected_response_code != http_status)
fail (is);
next_command (is);
}
/**
* Function called with detailed wire transfer data.
*
* @param cls closure
* @param http_status HTTP status code we got, 0 on exchange protocol violation
* @param ec taler-specific error code
* @param json original json reply
*/
static void
track_transaction_cb (void *cls,
unsigned int http_status,
enum TALER_ErrorCode ec,
const json_t *json)
{
struct InterpreterState *is = cls;
struct Command *cmd = &is->commands[is->ip];
cmd->details.track_transaction.tth = NULL;
if (cmd->expected_response_code != http_status)
{
GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
"Unexpected response code %u (%d) to command %s\n",
http_status,
ec,
cmd->label);
fail (is);
return;
}
if (MHD_HTTP_OK != http_status)
fail (is);
next_command (is);
}
/**
* Callback for a /tip-authorize request. Returns the result of
* the operation.
*
* @param cls closure
* @param http_status HTTP status returned by the merchant backend
* @param ec taler-specific error code
* @param tip_id which tip ID should be used to pickup the tip
* @param tip_expiration when does the tip expire (needs to be picked up before this time)
* @param exchange_url at what exchange can the tip be picked up
*/
static void
tip_authorize_cb (void *cls,
unsigned int http_status,
enum TALER_ErrorCode ec,
const struct GNUNET_HashCode *tip_id,
struct GNUNET_TIME_Absolute tip_expiration,
const char *exchange_url)
{
struct InterpreterState *is = cls;
struct Command *cmd = &is->commands[is->ip];
cmd->details.tip_authorize.tao = NULL;
if (cmd->expected_response_code != http_status)
{
GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
"Unexpected response code %u (%d) to command %s\n",
http_status,
ec,
cmd->label);
fail (is);
return;
}
if (cmd->details.tip_authorize.expected_ec != ec)
{
GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
"Unexpected error code %d (%u) to command %s\n",
ec,
http_status,
cmd->label);
fail (is);
return;
}
if ( (MHD_HTTP_OK == http_status) &&
(TALER_EC_NONE == ec) )
{
if (0 != strcmp (exchange_url,
EXCHANGE_URL))
{
GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
"Unexpected exchange URL %s to command %s\n",
exchange_url,
cmd->label);
fail (is);
return;
}
cmd->details.tip_authorize.tip_id = *tip_id;
cmd->details.tip_authorize.tip_expiration = tip_expiration;
}
next_command (is);
}
/**
* Callbacks of this type are used to serve the result of submitting a
* withdraw request to a exchange.
*
* @param cls closure, a `struct WithdrawHandle *`
* @param http_status HTTP response code, #MHD_HTTP_OK (200) for successful status request
* 0 if the exchange's reply is bogus (fails to follow the protocol)
* @param ec taler-specific error code, #TALER_EC_NONE on success
* @param sig signature over the coin, NULL on error
* @param full_response full response from the exchange (for logging, in case of errors)
*/
static void
pickup_withdraw_cb (void *cls,
unsigned int http_status,
enum TALER_ErrorCode ec,
const struct TALER_DenominationSignature *sig,
const json_t *full_response)
{
struct WithdrawHandle *wh = cls;
struct InterpreterState *is = wh->is;
struct Command *cmd = &is->commands[is->ip];
wh->wsh = NULL;
GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
"Withdraw operation %u completed with %u (%d)\n",
wh->off,
http_status,
ec);
GNUNET_assert (wh->off < cmd->details.tip_pickup.num_coins);
if ( (MHD_HTTP_OK != http_status) ||
(TALER_EC_NONE != ec) )
{
GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
"Unexpected response code %u (%d) to command %s when withdrawing\n",
http_status,
ec,
cmd->label);
fail (is);
return;
}
if (NULL == cmd->details.tip_pickup.sigs)
cmd->details.tip_pickup.sigs = GNUNET_new_array (cmd->details.tip_pickup.num_coins,
struct TALER_DenominationSignature);
GNUNET_assert (NULL == cmd->details.tip_pickup.sigs[wh->off].rsa_signature);
cmd->details.tip_pickup.sigs[wh->off].rsa_signature
= GNUNET_CRYPTO_rsa_signature_dup (sig->rsa_signature);
for (unsigned int i=0;idetails.tip_pickup.num_coins;i++)
if (NULL != cmd->details.tip_pickup.withdraws[wh->off].wsh)
return; /* still some ops ongoing */
GNUNET_free (cmd->details.tip_pickup.withdraws);
cmd->details.tip_pickup.withdraws = NULL;
next_command (is);
}
/**
* Callback for a /tip-pickup request. Returns the result of
* the operation.
*
* @param cls closure
* @param http_status HTTP status returned by the merchant backend, "200 OK" on success
* @param ec taler-specific error code
* @param reserve_pub public key of the reserve that made the @a reserve_sigs, NULL on error
* @param num_reserve_sigs length of the @a reserve_sigs array, 0 on error
* @param reserve_sigs array of signatures authorizing withdrawals, NULL on error
* @param json original json response
*/
static void
pickup_cb (void *cls,
unsigned int http_status,
enum TALER_ErrorCode ec,
const struct TALER_ReservePublicKeyP *reserve_pub,
unsigned int num_reserve_sigs,
const struct TALER_ReserveSignatureP *reserve_sigs,
const json_t *json)
{
struct InterpreterState *is = cls;
struct Command *cmd = &is->commands[is->ip];
cmd->details.tip_pickup.tpo = NULL;
if (http_status != cmd->expected_response_code)
{
GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
"Unexpected response code %u (%d) to command %s\n",
http_status,
ec,
cmd->label);
fail (is);
return;
}
if (ec != cmd->details.tip_pickup.expected_ec)
{
GNUNET_break (0);
fail (is);
return;
}
if ( (MHD_HTTP_OK != http_status) ||
(TALER_EC_NONE != ec) )
{
next_command (is);
return;
}
if (num_reserve_sigs != cmd->details.tip_pickup.num_coins)
{
GNUNET_break (0);
fail (is);
return;
}
/* pickup successful, now withdraw! */
GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
"Obtained %u signatures for withdrawal from picking up a tip\n",
num_reserve_sigs);
GNUNET_assert (NULL == cmd->details.tip_pickup.withdraws);
cmd->details.tip_pickup.withdraws
= GNUNET_new_array (num_reserve_sigs,
struct WithdrawHandle);
for (unsigned int i=0;idetails.tip_pickup.withdraws[i];
wh->off = i;
wh->is = is;
GNUNET_assert ( (NULL == wh->wsh) &&
( (NULL == cmd->details.tip_pickup.sigs) ||
(NULL == cmd->details.tip_pickup.sigs[wh->off].rsa_signature) ) );
wh->wsh = TALER_EXCHANGE_reserve_withdraw2 (exchange,
cmd->details.tip_pickup.dks[i],
&reserve_sigs[i],
reserve_pub,
&cmd->details.tip_pickup.psa[i],
&pickup_withdraw_cb,
wh);
if (NULL == wh->wsh)
{
GNUNET_break (0);
fail (is);
return;
}
}
if (0 == num_reserve_sigs)
next_command (is);
}
/**
* Find denomination key matching the given amount.
*
* @param keys array of keys to search
* @param amount coin value to look for
* @return NULL if no matching key was found
*/
static const struct TALER_EXCHANGE_DenomPublicKey *
find_pk (const struct TALER_EXCHANGE_Keys *keys,
const struct TALER_Amount *amount)
{
struct GNUNET_TIME_Absolute now;
char *str;
now = GNUNET_TIME_absolute_get ();
for (unsigned int i=0;inum_denom_keys;i++)
{
const struct TALER_EXCHANGE_DenomPublicKey *pk;
pk = &keys->denom_keys[i];
if ( (0 == TALER_amount_cmp (amount,
&pk->value)) &&
(now.abs_value_us >= pk->valid_from.abs_value_us) &&
(now.abs_value_us < pk->withdraw_valid_until.abs_value_us) )
return pk;
}
/* do 2nd pass to check if expiration times are to blame for failure */
str = TALER_amount_to_string (amount);
for (unsigned int i=0;inum_denom_keys;i++)
{
const struct TALER_EXCHANGE_DenomPublicKey *pk;
pk = &keys->denom_keys[i];
if ( (0 == TALER_amount_cmp (amount,
&pk->value)) &&
( (now.abs_value_us < pk->valid_from.abs_value_us) ||
(now.abs_value_us > pk->withdraw_valid_until.abs_value_us) ) )
{
GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
"Have denomination key for `%s', but with wrong expiration range %llu vs [%llu,%llu)\n",
str,
(unsigned long long) now.abs_value_us,
(unsigned long long) pk->valid_from.abs_value_us,
(unsigned long long) pk->withdraw_valid_until.abs_value_us);
GNUNET_free (str);
return NULL;
}
}
GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
"No denomination key for amount %s found\n",
str);
GNUNET_free (str);
return NULL;
}
/**
* Reset the interpreter's state.
*
* @param is interpreter to reset
*/
static void
cleanup_state (struct InterpreterState *is)
{
struct Command *cmd;
for (unsigned int i=0;OC_END != (cmd = &is->commands[i])->oc;i++)
{
switch (cmd->oc)
{
case OC_END:
GNUNET_assert (0);
break;
case OC_PROPOSAL_LOOKUP:
if (NULL != cmd->details.proposal_lookup.plo)
{
GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
"Command %u (%s) did not complete\n",
i,
cmd->label);
TALER_MERCHANT_proposal_lookup_cancel (cmd->details.proposal_lookup.plo);
}
break;
case OC_ADMIN_ADD_INCOMING:
if (NULL != cmd->details.admin_add_incoming.aih)
{
GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
"Command %u (%s) did not complete\n",
i,
cmd->label);
TALER_BANK_admin_add_incoming_cancel (cmd->details.admin_add_incoming.aih);
cmd->details.admin_add_incoming.aih = NULL;
}
break;
case OC_WITHDRAW_STATUS:
if (NULL != cmd->details.reserve_status.wsh)
{
GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
"Command %u (%s) did not complete\n",
i,
cmd->label);
TALER_EXCHANGE_reserve_status_cancel (cmd->details.reserve_status.wsh);
cmd->details.reserve_status.wsh = NULL;
}
break;
case OC_WITHDRAW_SIGN:
if (NULL != cmd->details.reserve_withdraw.wsh)
{
GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
"Command %u (%s) did not complete\n",
i,
cmd->label);
TALER_EXCHANGE_reserve_withdraw_cancel (cmd->details.reserve_withdraw.wsh);
cmd->details.reserve_withdraw.wsh = NULL;
}
if (NULL != cmd->details.reserve_withdraw.sig.rsa_signature)
{
GNUNET_CRYPTO_rsa_signature_free (cmd->details.reserve_withdraw.sig.rsa_signature);
cmd->details.reserve_withdraw.sig.rsa_signature = NULL;
}
break;
case OC_PROPOSAL:
if (NULL != cmd->details.proposal.po)
{
GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
"Command %u (%s) did not complete (proposal put)\n",
i,
cmd->label);
TALER_MERCHANT_proposal_cancel (cmd->details.proposal.po);
cmd->details.proposal.po = NULL;
}
if (NULL != cmd->details.proposal.plo)
{
GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
"Command %u (%s) did not complete (proposal lookup)\n",
i,
cmd->label);
TALER_MERCHANT_proposal_lookup_cancel (cmd->details.proposal.plo);
}
if (NULL != cmd->details.proposal.contract_terms)
{
json_decref (cmd->details.proposal.contract_terms);
cmd->details.proposal.contract_terms = NULL;
}
break;
case OC_PAY:
if (NULL != cmd->details.pay.ph)
{
GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
"Command %u (%s) did not complete\n",
i,
cmd->label);
TALER_MERCHANT_pay_cancel (cmd->details.pay.ph);
cmd->details.pay.ph = NULL;
}
break;
case OC_PAY_AGAIN:
if (NULL != cmd->details.pay_again.ph)
{
GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
"Command %u (%s) did not complete\n",
i,
cmd->label);
TALER_MERCHANT_pay_cancel (cmd->details.pay_again.ph);
cmd->details.pay_again.ph = NULL;
}
break;
case OC_PAY_ABORT:
if (NULL != cmd->details.pay_abort.ph)
{
GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
"Command %u (%s) did not complete\n",
i,
cmd->label);
TALER_MERCHANT_pay_cancel (cmd->details.pay_abort.ph);
cmd->details.pay_abort.ph = NULL;
}
GNUNET_array_grow (cmd->details.pay_abort.res,
cmd->details.pay_abort.num_refunds,
0);
break;
case OC_PAY_ABORT_REFUND:
if (NULL != cmd->details.pay_abort_refund.rh)
{
GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
"Command %u (%s) did not complete\n",
i,
cmd->label);
TALER_EXCHANGE_refund_cancel (cmd->details.pay_abort_refund.rh);
cmd->details.pay_abort_refund.rh = NULL;
}
break;
case OC_RUN_AGGREGATOR:
if (NULL != cmd->details.run_aggregator.aggregator_proc)
{
GNUNET_break (0 ==
GNUNET_OS_process_kill (cmd->details.run_aggregator.aggregator_proc,
SIGKILL));
GNUNET_OS_process_wait (cmd->details.run_aggregator.aggregator_proc);
GNUNET_OS_process_destroy (cmd->details.run_aggregator.aggregator_proc);
cmd->details.run_aggregator.aggregator_proc = NULL;
}
if (NULL != cmd->details.run_aggregator.child_death_task)
{
GNUNET_SCHEDULER_cancel (cmd->details.run_aggregator.child_death_task);
cmd->details.run_aggregator.child_death_task = NULL;
}
break;
case OC_RUN_WIREWATCH:
if (NULL != cmd->details.run_wirewatch.wirewatch_proc)
{
GNUNET_break (0 ==
GNUNET_OS_process_kill (cmd->details.run_wirewatch.wirewatch_proc,
SIGKILL));
GNUNET_OS_process_wait (cmd->details.run_wirewatch.wirewatch_proc);
GNUNET_OS_process_destroy (cmd->details.run_wirewatch.wirewatch_proc);
cmd->details.run_wirewatch.wirewatch_proc = NULL;
}
if (NULL != cmd->details.run_wirewatch.child_death_task)
{
GNUNET_SCHEDULER_cancel (cmd->details.run_wirewatch.child_death_task);
cmd->details.run_wirewatch.child_death_task = NULL;
}
break;
case OC_CHECK_BANK_TRANSFER:
GNUNET_free_non_null (cmd->details.check_bank_transfer.subject);
cmd->details.check_bank_transfer.subject = NULL;
break;
case OC_CHECK_BANK_TRANSFERS_EMPTY:
break;
case OC_TRACK_TRANSFER:
if (NULL != cmd->details.track_transfer.tdo)
{
TALER_MERCHANT_track_transfer_cancel (cmd->details.track_transfer.tdo);
cmd->details.track_transfer.tdo = NULL;
}
break;
case OC_TRACK_TRANSACTION:
if (NULL != cmd->details.track_transaction.tth)
{
TALER_MERCHANT_track_transaction_cancel (cmd->details.track_transaction.tth);
cmd->details.track_transaction.tth = NULL;
}
break;
case OC_HISTORY:
if (NULL != cmd->details.history.ho)
{
TALER_MERCHANT_history_cancel (cmd->details.history.ho);
cmd->details.history.ho = NULL;
}
break;
case OC_REFUND_INCREASE:
if (NULL != cmd->details.refund_increase.rio)
{
TALER_MERCHANT_refund_increase_cancel (cmd->details.refund_increase.rio);
cmd->details.refund_increase.rio = NULL;
}
break;
case OC_REFUND_LOOKUP:
if (NULL != cmd->details.refund_lookup.rlo)
{
TALER_MERCHANT_refund_lookup_cancel (cmd->details.refund_lookup.rlo);
cmd->details.refund_lookup.rlo = NULL;
}
break;
case OC_TIP_AUTHORIZE:
if (NULL != cmd->details.tip_authorize.tao)
{
TALER_MERCHANT_tip_authorize_cancel (cmd->details.tip_authorize.tao);
cmd->details.tip_authorize.tao = NULL;
}
break;
case OC_TIP_PICKUP:
if (NULL != cmd->details.tip_pickup.tpo)
{
TALER_MERCHANT_tip_pickup_cancel (cmd->details.tip_pickup.tpo);
cmd->details.tip_pickup.tpo = NULL;
}
if (NULL != cmd->details.tip_pickup.psa)
{
GNUNET_free (cmd->details.tip_pickup.psa);
cmd->details.tip_pickup.psa = NULL;
}
if (NULL != cmd->details.tip_pickup.dks)
{
GNUNET_free (cmd->details.tip_pickup.dks);
cmd->details.tip_pickup.dks = NULL;
}
if (NULL != cmd->details.tip_pickup.withdraws)
{
for (unsigned int j=0;jdetails.tip_pickup.num_coins;j++)
{
struct WithdrawHandle *wh = &cmd->details.tip_pickup.withdraws[j];
if (NULL != wh->wsh)
{
TALER_EXCHANGE_reserve_withdraw_cancel (wh->wsh);
wh->wsh = NULL;
}
}
GNUNET_free (cmd->details.tip_pickup.withdraws);
cmd->details.tip_pickup.withdraws = NULL;
}
if (NULL != cmd->details.tip_pickup.sigs)
{
for (unsigned int j=0;jdetails.tip_pickup.num_coins;j++)
{
if (NULL != cmd->details.tip_pickup.sigs[j].rsa_signature)
{
GNUNET_CRYPTO_rsa_signature_free (cmd->details.tip_pickup.sigs[j].rsa_signature);
cmd->details.tip_pickup.sigs[j].rsa_signature = NULL;
}
}
GNUNET_free (cmd->details.tip_pickup.sigs);
cmd->details.tip_pickup.sigs = NULL;
}
break;
case OC_CHECK_PAYMENT:
if (NULL != cmd->details.check_payment.cpo)
{
GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
"Command %u (%s) did not complete\n",
i,
cmd->label);
TALER_MERCHANT_check_payment_cancel (cmd->details.check_payment.cpo);
cmd->details.check_payment.cpo = NULL;
}
break;
case OC_TIP_QUERY:
if (NULL != cmd->details.tip_query.tqo)
{
GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
"Command %u (%s) did not complete\n",
i,
cmd->label);
TALER_MERCHANT_tip_query_cancel (cmd->details.tip_query.tqo);
cmd->details.tip_query.tqo = NULL;
}
break;
default:
GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
"Shutdown: unknown instruction %d at %u (%s)\n",
cmd->oc,
i,
cmd->label);
break;
}
}
}
/**
* Parse the @a coins specification and grow the @a pc
* array with the coins found, updating @a npc.
*
* @param[in,out] pc pointer to array of coins found
* @param[in,out] npc length of array at @a pc
* @param[in] coins string specifying coins to add to @a pc,
* clobbered in the process
* @param is interpreter state
* @param pref reference to the #OC_PAY command
* @return #GNUNET_OK on success
*/
static int
build_coins (struct TALER_MERCHANT_PayCoin **pc,
unsigned int *npc,
char *coins,
struct InterpreterState *is,
const struct Command *pref)
{
char *token;
for (token = strtok (coins, ";");
NULL != token;
token = strtok (NULL, ";"))
{
const struct Command *coin_ref;
char *ctok;
unsigned int ci;
struct TALER_MERCHANT_PayCoin *icoin;
/* Token syntax is "LABEL[/NUMBER]" */
ctok = strchr (token, '/');
ci = 0;
if (NULL != ctok)
{
*ctok = '\0';
ctok++;
if (1 != sscanf (ctok,
"%u",
&ci))
{
GNUNET_break (0);
return GNUNET_SYSERR;
}
}
GNUNET_assert (coin_ref = find_command (is,
token));
GNUNET_array_grow (*pc,
*npc,
(*npc) + 1);
icoin = &(*pc)[(*npc)-1];
switch (coin_ref->oc)
{
case OC_WITHDRAW_SIGN:
icoin->coin_priv = coin_ref->details.reserve_withdraw.ps.coin_priv;
icoin->denom_pub = coin_ref->details.reserve_withdraw.pk->key;
icoin->denom_sig = coin_ref->details.reserve_withdraw.sig;
icoin->denom_value = coin_ref->details.reserve_withdraw.pk->value;
icoin->exchange_url = EXCHANGE_URL;
break;
case OC_TIP_PICKUP:
icoin->coin_priv = coin_ref->details.tip_pickup.psa[ci].coin_priv;
icoin->denom_pub = coin_ref->details.tip_pickup.dks[ci]->key;
icoin->denom_sig = coin_ref->details.tip_pickup.sigs[ci];
icoin->denom_value = coin_ref->details.tip_pickup.dks[ci]->value;
icoin->exchange_url = EXCHANGE_URL;
break;
default:
GNUNET_assert (0);
}
GNUNET_assert (GNUNET_OK ==
TALER_string_to_amount (pref->details.pay.amount_with_fee,
&icoin->amount_with_fee));
GNUNET_assert (GNUNET_OK ==
TALER_string_to_amount (pref->details.pay.amount_without_fee,
&icoin->amount_without_fee));
GNUNET_assert (GNUNET_OK ==
TALER_string_to_amount (pref->details.pay.refund_fee,
&icoin->refund_fee));
}
return GNUNET_OK;
}
/**
* Callbacks of this type are used to serve the result of submitting a
* /pay request to a merchant.
*
* @param cls closure
* @param http_status HTTP response code, 200 or 300-level response codes
* can indicate success, depending on whether the interaction
* was with a merchant frontend or backend;
* 0 if the merchant's reply is bogus (fails to follow the protocol)
* @param ec taler-specific error code
* @param merchant_pub public key of the merchant
* @param h_contract hash of the contract
* @param num_refunds size of the @a merchant_sigs array, 0 on errors
* @param res merchant signatures refunding coins, NULL on errors
* @param obj the received JSON reply, with error details if the request failed
*/
static void
pay_refund_cb (void *cls,
unsigned int http_status,
enum TALER_ErrorCode ec,
const struct TALER_MerchantPublicKeyP *merchant_pub,
const struct GNUNET_HashCode *h_contract,
unsigned int num_refunds,
const struct TALER_MERCHANT_RefundEntry *res,
const json_t *obj)
{
struct InterpreterState *is = cls;
struct Command *cmd = &is->commands[is->ip];
cmd->details.pay_abort.ph = NULL;
if (cmd->expected_response_code != http_status)
{
GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
"Unexpected response code %u (%d) to command %s\n",
http_status,
ec,
cmd->label);
fail (is);
return;
}
if ( (MHD_HTTP_OK == http_status) &&
(TALER_EC_NONE == ec) )
{
GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
"Received %u refunds\n",
num_refunds);
cmd->details.pay_abort.num_refunds = num_refunds;
cmd->details.pay_abort.res
= GNUNET_new_array (num_refunds,
struct TALER_MERCHANT_RefundEntry);
memcpy (cmd->details.pay_abort.res,
res,
num_refunds * sizeof (struct TALER_MERCHANT_RefundEntry));
cmd->details.pay_abort.h_contract = *h_contract;
cmd->details.pay_abort.merchant_pub = *merchant_pub;
}
next_command (is);
}
/**
* Callbacks of this type are used to serve the result of submitting a
* refund request to an exchange.
*
* @param cls closure
* @param http_status HTTP response code, #MHD_HTTP_OK (200) for successful deposit;
* 0 if the exchange's reply is bogus (fails to follow the protocol)
* @param ec taler-specific error code, #TALER_EC_NONE on success
* @param sign_key exchange key used to sign @a obj, or NULL
* @param obj the received JSON reply, should be kept as proof (and, in particular,
* be forwarded to the customer)
*/
static void
abort_refund_cb (void *cls,
unsigned int http_status,
enum TALER_ErrorCode ec,
const struct TALER_ExchangePublicKeyP *sign_key,
const json_t *obj)
{
struct InterpreterState *is = cls;
struct Command *cmd = &is->commands[is->ip];
cmd->details.pay_abort_refund.rh = NULL;
if (cmd->expected_response_code != http_status)
{
GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
"Unexpected response code %u (%d) to command %s\n",
http_status,
ec,
cmd->label);
fail (is);
return;
}
next_command (is);
}
/**
* Run the main interpreter loop that performs exchange operations.
*
* @param cls contains the `struct InterpreterState`
*/
static void
interpreter_run (void *cls)
{
const struct GNUNET_SCHEDULER_TaskContext *tc;
struct InterpreterState *is = cls;
struct Command *cmd = &is->commands[is->ip];
const struct Command *ref;
struct TALER_ReservePublicKeyP reserve_pub;
struct TALER_Amount amount;
is->task = NULL;
tc = GNUNET_SCHEDULER_get_task_context ();
if (0 != (tc->reason & GNUNET_SCHEDULER_REASON_SHUTDOWN))
{
GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
"Test aborted by shutdown request\n");
fail (is);
return;
}
GNUNET_log (GNUNET_ERROR_TYPE_INFO,
"Interpreter runs command %u/%s(%u)\n",
is->ip,
cmd->label,
cmd->oc);
switch (cmd->oc)
{
case OC_END:
result = GNUNET_OK;
if (instance_idx + 1 == ninstances)
{
GNUNET_SCHEDULER_shutdown ();
return;
}
cleanup_state (is);
is->ip = 0;
instance_idx++;
instance = instances[instance_idx];
GNUNET_free_non_null (instance_priv);
instance_priv = get_instance_priv (cfg, instance);
GNUNET_log (GNUNET_ERROR_TYPE_INFO,
"Switching instance: `%s'\n",
instance);
is->task = GNUNET_SCHEDULER_add_now (interpreter_run,
is);
break;
case OC_PROPOSAL_LOOKUP:
{
const char *order_id;
GNUNET_assert (NULL != cmd->details.proposal_lookup.proposal_reference);
GNUNET_assert (NULL != (ref =
find_command (is,
cmd->details.proposal_lookup.proposal_reference)));
order_id = json_string_value (json_object_get (ref->details.proposal.contract_terms,
"order_id"));
if (NULL == (cmd->details.proposal_lookup.plo
= TALER_MERCHANT_proposal_lookup (ctx,
MERCHANT_URL,
order_id,
instance,
&ref->details.proposal.nonce,
proposal_lookup_cb,
is)))
{
GNUNET_break (0);
fail (is);
}
}
break;
case OC_CHECK_PAYMENT:
{
const char *order_id;
GNUNET_assert (NULL != cmd->details.check_payment.contract_ref);
GNUNET_assert (NULL != (ref =
find_command (is,
cmd->details.check_payment.contract_ref)));
order_id = json_string_value (json_object_get (ref->details.proposal.contract_terms,
"order_id"));
GNUNET_assert (NULL != order_id);
if (NULL == (cmd->details.check_payment.cpo
= TALER_MERCHANT_check_payment (ctx,
MERCHANT_URL,
instance,
order_id,
NULL,
NULL,
NULL,
check_payment_cb,
is)))
{
GNUNET_break (0);
fail (is);
}
}
break;
case OC_TIP_QUERY:
{
if (instance_idx != 0)
{
// We check /tip-query only for the first instance,
// since for tipping we use a dedicated instance.
// On repeated runs, the expected authorized amounts wouldn't
// match up (they would all accumulate!)
next_command (is);
break;
}
if (NULL == (cmd->details.tip_query.tqo
= TALER_MERCHANT_tip_query (ctx,
MERCHANT_URL,
cmd->details.tip_query.instance,
tip_query_cb,
is)))
{
GNUNET_break (0);
fail (is);
}
}
break;
case OC_ADMIN_ADD_INCOMING:
{
char *subject;
struct TALER_BANK_AuthenticationData auth;
if (NULL !=
cmd->details.admin_add_incoming.subject)
{
subject = GNUNET_strdup (cmd->details.admin_add_incoming.subject);
}
else
{
/* Use reserve public key as subject */
if (NULL !=
cmd->details.admin_add_incoming.reserve_reference)
{
GNUNET_assert (NULL != (ref
= find_command (is,
cmd->details.admin_add_incoming.reserve_reference)));
GNUNET_assert (OC_ADMIN_ADD_INCOMING == ref->oc);
cmd->details.admin_add_incoming.reserve_priv
= ref->details.admin_add_incoming.reserve_priv;
}
else if (NULL !=
cmd->details.admin_add_incoming.instance)
{
char *section;
char *keys;
struct GNUNET_CRYPTO_EddsaPrivateKey *pk;
GNUNET_asprintf (§ion,
"merchant-instance-%s",
cmd->details.admin_add_incoming.instance);
if (GNUNET_OK !=
GNUNET_CONFIGURATION_get_value_string (cfg,
section,
"TIP_RESERVE_PRIV_FILENAME",
&keys))
{
GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
"Configuration fails to specify reserve private key filename in section %s\n",
section);
GNUNET_free (section);
fail (is);
return;
}
pk = GNUNET_CRYPTO_eddsa_key_create_from_file (keys);
if (NULL == pk)
{
GNUNET_log_config_invalid (GNUNET_ERROR_TYPE_ERROR,
section,
"TIP_RESERVE_PRIV_FILENAME",
"Failed to read private key");
GNUNET_free (keys);
GNUNET_free (section);
fail (is);
return;
}
cmd->details.admin_add_incoming.reserve_priv.eddsa_priv = *pk;
GNUNET_free (pk);
GNUNET_free (keys);
GNUNET_free (section);
}
else
{
struct GNUNET_CRYPTO_EddsaPrivateKey *priv;
priv = GNUNET_CRYPTO_eddsa_key_create ();
cmd->details.admin_add_incoming.reserve_priv.eddsa_priv = *priv;
GNUNET_free (priv);
}
GNUNET_CRYPTO_eddsa_key_get_public (&cmd->details.admin_add_incoming.reserve_priv.eddsa_priv,
&reserve_pub.eddsa_pub);
subject = GNUNET_STRINGS_data_to_string_alloc (&reserve_pub,
sizeof (reserve_pub));
}
GNUNET_CRYPTO_eddsa_key_get_public (&cmd->details.admin_add_incoming.reserve_priv.eddsa_priv,
&reserve_pub.eddsa_pub);
GNUNET_assert (GNUNET_OK ==
TALER_string_to_amount (cmd->details.admin_add_incoming.amount,
&amount));
auth.method = TALER_BANK_AUTH_BASIC;
auth.details.basic.username = (char *) cmd->details.admin_add_incoming.auth_username;
auth.details.basic.password = (char *) cmd->details.admin_add_incoming.auth_password;
cmd->details.admin_add_incoming.aih
= TALER_BANK_admin_add_incoming (ctx,
BANK_URL,
&auth,
EXCHANGE_URL,
subject,
&amount,
cmd->details.admin_add_incoming.debit_account_no,
cmd->details.admin_add_incoming.credit_account_no,
&add_incoming_cb,
is);
GNUNET_free (subject);
if (NULL == cmd->details.admin_add_incoming.aih)
{
GNUNET_break (0);
fail (is);
}
break;
}
case OC_WITHDRAW_STATUS:
GNUNET_assert (NULL !=
cmd->details.reserve_status.reserve_reference);
GNUNET_assert (NULL != (ref = find_command
(is,
cmd->details.reserve_status.reserve_reference)));
GNUNET_assert (OC_ADMIN_ADD_INCOMING == ref->oc);
GNUNET_CRYPTO_eddsa_key_get_public (&ref->details.admin_add_incoming.reserve_priv.eddsa_priv,
&reserve_pub.eddsa_pub);
if (NULL == (cmd->details.reserve_status.wsh
= TALER_EXCHANGE_reserve_status (exchange,
&reserve_pub,
&reserve_status_cb,
is)))
{
GNUNET_break (0);
fail (is);
}
break;
case OC_WITHDRAW_SIGN:
GNUNET_assert (NULL != cmd->details.reserve_withdraw.reserve_reference);
GNUNET_assert (NULL != (ref = find_command
(is,
cmd->details.reserve_withdraw.reserve_reference)));
GNUNET_assert (OC_ADMIN_ADD_INCOMING == ref->oc);
GNUNET_assert (NULL != cmd->details.reserve_withdraw.amount);
GNUNET_assert (GNUNET_OK ==
TALER_string_to_amount (cmd->details.reserve_withdraw.amount,
&amount));
GNUNET_assert (NULL != (cmd->details.reserve_withdraw.pk
= find_pk (is->keys,
&amount)));
TALER_planchet_setup_random (&cmd->details.reserve_withdraw.ps);
cmd->details.reserve_withdraw.wsh
= TALER_EXCHANGE_reserve_withdraw
(exchange,
cmd->details.reserve_withdraw.pk,
&ref->details.admin_add_incoming.reserve_priv,
&cmd->details.reserve_withdraw.ps,
&reserve_withdraw_cb,
is);
if (NULL == cmd->details.reserve_withdraw.wsh)
{
GNUNET_break (0);
fail (is);
}
break;
case OC_PROPOSAL:
{
json_t *order;
json_error_t error;
GNUNET_assert (NULL != (order = json_loads (cmd->details.proposal.order,
JSON_REJECT_DUPLICATES,
&error)));
GNUNET_CRYPTO_random_block (GNUNET_CRYPTO_QUALITY_WEAK,
&cmd->details.proposal.nonce,
sizeof (struct GNUNET_CRYPTO_EddsaPublicKey));
if (NULL != instance)
{
json_t *merchant;
merchant = json_object ();
json_object_set_new (merchant,
"instance",
json_string (instance));
json_object_set_new (order,
"merchant",
merchant);
}
cmd->details.proposal.po = TALER_MERCHANT_order_put (ctx,
MERCHANT_URL,
order,
&proposal_cb,
is);
json_decref (order);
if (NULL == cmd->details.proposal.po)
{
GNUNET_break (0);
fail (is);
}
break;
}
case OC_PAY:
{
struct TALER_MERCHANT_PayCoin *pc;
unsigned int npc;
char *coins;
const char *order_id;
struct GNUNET_TIME_Absolute refund_deadline;
struct GNUNET_TIME_Absolute pay_deadline;
struct GNUNET_TIME_Absolute timestamp;
struct GNUNET_HashCode h_wire;
struct TALER_MerchantPublicKeyP merchant_pub;
struct TALER_MerchantSignatureP merchant_sig;
struct TALER_Amount total_amount;
struct TALER_Amount max_fee;
const char *error_name;
unsigned int error_line;
/* get proposal */
GNUNET_assert (NULL != (ref = find_command
(is,
cmd->details.pay.contract_ref)));
merchant_sig = ref->details.proposal.merchant_sig;
GNUNET_assert (NULL != ref->details.proposal.contract_terms);
{
/* Get information that needs to be replied in the deposit permission */
struct GNUNET_JSON_Specification spec[] = {
GNUNET_JSON_spec_string ("order_id",
&order_id),
GNUNET_JSON_spec_absolute_time ("refund_deadline",
&refund_deadline),
GNUNET_JSON_spec_absolute_time ("pay_deadline",
&pay_deadline),
GNUNET_JSON_spec_absolute_time ("timestamp",
×tamp),
GNUNET_JSON_spec_fixed_auto ("merchant_pub",
&merchant_pub),
GNUNET_JSON_spec_fixed_auto ("H_wire",
&h_wire),
TALER_JSON_spec_amount ("amount",
&total_amount),
TALER_JSON_spec_amount ("max_fee",
&max_fee),
GNUNET_JSON_spec_end()
};
if (GNUNET_OK !=
GNUNET_JSON_parse (ref->details.proposal.contract_terms,
spec,
&error_name,
&error_line))
{
GNUNET_break_op (0);
GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
"Parser failed on %s:%u\n",
error_name,
error_line);
/**
* Let's use fail() here, as the proposal might be broken
* because of backend's fault.
*/
fail (is);
return;
}
cmd->details.pay.merchant_pub = merchant_pub;
}
/* strtok loop here */
coins = GNUNET_strdup (cmd->details.pay.coin_ref);
pc = NULL;
npc = 0;
if (GNUNET_OK !=
build_coins (&pc,
&npc,
coins,
is,
cmd))
{
fail (is);
GNUNET_array_grow (pc,
npc,
0);
GNUNET_free (coins);
return;
}
GNUNET_free (coins);
cmd->details.pay.ph = TALER_MERCHANT_pay_wallet
(ctx,
MERCHANT_URL,
instance,
&ref->details.proposal.hash,
&total_amount,
&max_fee,
&merchant_pub,
&merchant_sig,
timestamp,
refund_deadline,
pay_deadline,
&h_wire,
order_id,
npc /* num_coins */,
pc /* coins */,
&pay_cb,
is);
GNUNET_array_grow (pc,
npc,
0);
}
if (NULL == cmd->details.pay.ph)
{
GNUNET_break (0);
fail (is);
}
break;
case OC_PAY_AGAIN:
{
struct TALER_MERCHANT_PayCoin *pc;
const struct Command *pref;
unsigned int npc;
char *coins;
const char *order_id;
struct GNUNET_TIME_Absolute refund_deadline;
struct GNUNET_TIME_Absolute pay_deadline;
struct GNUNET_TIME_Absolute timestamp;
struct GNUNET_HashCode h_wire;
struct TALER_MerchantPublicKeyP merchant_pub;
struct TALER_MerchantSignatureP merchant_sig;
struct TALER_Amount total_amount;
struct TALER_Amount max_fee;
const char *error_name;
unsigned int error_line;
/* Get original /pay command */
GNUNET_assert (NULL != (pref = find_command
(is,
cmd->details.pay_again.pay_ref)));
/* get proposal */
GNUNET_assert (NULL != (ref = find_command
(is,
pref->details.pay.contract_ref)));
merchant_sig = ref->details.proposal.merchant_sig;
GNUNET_assert (NULL != ref->details.proposal.contract_terms);
{
/* Get information that needs to be replied in the deposit permission */
struct GNUNET_JSON_Specification spec[] = {
GNUNET_JSON_spec_string ("order_id",
&order_id),
GNUNET_JSON_spec_absolute_time ("refund_deadline",
&refund_deadline),
GNUNET_JSON_spec_absolute_time ("pay_deadline",
&pay_deadline),
GNUNET_JSON_spec_absolute_time ("timestamp",
×tamp),
GNUNET_JSON_spec_fixed_auto ("merchant_pub",
&merchant_pub),
GNUNET_JSON_spec_fixed_auto ("H_wire",
&h_wire),
TALER_JSON_spec_amount ("amount",
&total_amount),
TALER_JSON_spec_amount ("max_fee",
&max_fee),
GNUNET_JSON_spec_end()
};
if (GNUNET_OK !=
GNUNET_JSON_parse (ref->details.proposal.contract_terms,
spec,
&error_name,
&error_line))
{
GNUNET_break_op (0);
GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
"Parser failed on %s:%u\n",
error_name,
error_line);
/**
* Let's use fail() here, as the proposal might be broken
* because of backend's fault.
*/
fail (is);
return;
}
}
pc = NULL;
npc = 0;
/* Loop over coins from pay again */
coins = GNUNET_strdup (cmd->details.pay_again.coin_ref);
if (GNUNET_OK !=
build_coins (&pc,
&npc,
coins,
is,
pref))
{
fail (is);
GNUNET_array_grow (pc,
npc,
0);
GNUNET_free (coins);
return;
}
GNUNET_free (coins);
/* then repeat payment attempt */
cmd->details.pay_again.ph = TALER_MERCHANT_pay_wallet
(ctx,
MERCHANT_URL,
instance,
&ref->details.proposal.hash,
&total_amount,
&max_fee,
&merchant_pub,
&merchant_sig,
timestamp,
refund_deadline,
pay_deadline,
&h_wire,
order_id,
npc /* num_coins */,
pc /* coins */,
&pay_again_cb,
is);
GNUNET_array_grow (pc,
npc,
0);
}
if (NULL == cmd->details.pay_again.ph)
{
GNUNET_break (0);
fail (is);
}
break;
case OC_PAY_ABORT:
{
struct TALER_MERCHANT_PayCoin *pc;
const struct Command *pref;
unsigned int npc;
char *coins;
const char *order_id;
struct GNUNET_TIME_Absolute refund_deadline;
struct GNUNET_TIME_Absolute pay_deadline;
struct GNUNET_TIME_Absolute timestamp;
struct GNUNET_HashCode h_wire;
struct TALER_MerchantPublicKeyP merchant_pub;
struct TALER_MerchantSignatureP merchant_sig;
struct TALER_Amount total_amount;
struct TALER_Amount max_fee;
const char *error_name;
unsigned int error_line;
/* Get original /pay command */
GNUNET_assert (NULL != (pref = find_command
(is,
cmd->details.pay_abort.pay_ref)));
/* get proposal */
GNUNET_assert (NULL != (ref = find_command
(is,
pref->details.pay.contract_ref)));
merchant_sig = ref->details.proposal.merchant_sig;
GNUNET_assert (NULL != ref->details.proposal.contract_terms);
{
/* Get information that needs to be replied in the deposit permission */
struct GNUNET_JSON_Specification spec[] = {
GNUNET_JSON_spec_string ("order_id",
&order_id),
GNUNET_JSON_spec_absolute_time ("refund_deadline",
&refund_deadline),
GNUNET_JSON_spec_absolute_time ("pay_deadline",
&pay_deadline),
GNUNET_JSON_spec_absolute_time ("timestamp",
×tamp),
GNUNET_JSON_spec_fixed_auto ("merchant_pub",
&merchant_pub),
GNUNET_JSON_spec_fixed_auto ("H_wire",
&h_wire),
TALER_JSON_spec_amount ("amount",
&total_amount),
TALER_JSON_spec_amount ("max_fee",
&max_fee),
GNUNET_JSON_spec_end()
};
if (GNUNET_OK !=
GNUNET_JSON_parse (ref->details.proposal.contract_terms,
spec,
&error_name,
&error_line))
{
GNUNET_break_op (0);
GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
"Parser failed on %s:%u\n",
error_name,
error_line);
/**
* Let's use fail() here, as the proposal might be broken
* because of backend's fault.
*/
fail (is);
return;
}
}
/* strtok loop over original coins here */
pc = NULL;
npc = 0;
coins = GNUNET_strdup (pref->details.pay.coin_ref);
if (GNUNET_OK !=
build_coins (&pc,
&npc,
coins,
is,
pref))
{
fail (is);
GNUNET_array_grow (pc,
npc,
0);
GNUNET_free (coins);
return;
}
GNUNET_free (coins);
/* then trigger abort-refund operation */
cmd->details.pay_abort.ph = TALER_MERCHANT_pay_abort
(ctx,
MERCHANT_URL,
instance,
&ref->details.proposal.hash,
&total_amount,
&max_fee,
&merchant_pub,
&merchant_sig,
timestamp,
refund_deadline,
pay_deadline,
&h_wire,
order_id,
npc /* num_coins */,
pc /* coins */,
&pay_refund_cb,
is);
GNUNET_array_grow (pc,
npc,
0);
}
if (NULL == cmd->details.pay_abort.ph)
{
GNUNET_break (0);
fail (is);
}
break;
case OC_PAY_ABORT_REFUND:
{
struct TALER_Amount refund_fee;
const struct TALER_MERCHANT_RefundEntry *re;
/* Get original /pay command */
GNUNET_assert (NULL != (ref = find_command
(is,
cmd->details.pay_abort_refund.abort_ref)));
GNUNET_assert (OC_PAY_ABORT == ref->oc);
GNUNET_assert (ref->details.pay_abort.num_refunds >
cmd->details.pay_abort_refund.num_coin);
re = &ref->details.pay_abort.res[cmd->details.pay_abort_refund.num_coin];
GNUNET_assert (GNUNET_OK ==
TALER_string_to_amount
(cmd->details.pay_abort_refund.refund_amount,
&amount));
GNUNET_assert (GNUNET_OK ==
TALER_string_to_amount
(cmd->details.pay_abort_refund.refund_fee,
&refund_fee));
cmd->details.pay_abort_refund.rh
= TALER_EXCHANGE_refund2 (exchange,
&amount,
&refund_fee,
&ref->details.pay_abort.h_contract,
&re->coin_pub,
re->rtransaction_id,
&ref->details.pay_abort.merchant_pub,
&re->merchant_sig,
&abort_refund_cb,
is);
if (NULL == cmd->details.pay_abort_refund.rh)
{
GNUNET_break (0);
fail (is);
}
}
break;
case OC_RUN_AGGREGATOR:
{
const struct GNUNET_DISK_FileHandle *pr;
GNUNET_assert (NULL != (cmd->details.run_aggregator.aggregator_proc
= GNUNET_OS_start_process (GNUNET_NO,
GNUNET_OS_INHERIT_STD_ALL,
NULL, NULL, NULL,
"taler-exchange-aggregator",
"taler-exchange-aggregator",
"-c", "test_merchant_api.conf",
"-t", /* exit when done */
NULL)));
pr = GNUNET_DISK_pipe_handle (sigpipe, GNUNET_DISK_PIPE_END_READ);
cmd->details.run_aggregator.child_death_task
= GNUNET_SCHEDULER_add_read_file (GNUNET_TIME_UNIT_FOREVER_REL,
pr,
&maint_child_death, is);
}
break;
case OC_RUN_WIREWATCH:
{
const struct GNUNET_DISK_FileHandle *pr;
static int once;
cmd->details.run_wirewatch.wirewatch_proc
= GNUNET_OS_start_process (GNUNET_NO,
GNUNET_OS_INHERIT_STD_ALL,
NULL, NULL, NULL,
"taler-exchange-wirewatch",
"taler-exchange-wirewatch",
"-c", "test_merchant_api.conf",
"-t", "test", /* use Taler's bank/fakebank */
"-T", /* exit when done */
(0 == once ? "-r" : NULL),
NULL);
if (NULL == cmd->details.run_wirewatch.wirewatch_proc)
{
GNUNET_break (0);
fail (is);
return;
}
once = 1;
pr = GNUNET_DISK_pipe_handle (sigpipe,
GNUNET_DISK_PIPE_END_READ);
cmd->details.run_wirewatch.child_death_task
= GNUNET_SCHEDULER_add_read_file (GNUNET_TIME_UNIT_FOREVER_REL,
pr,
&maint_child_death, is);
return;
}
case OC_CHECK_BANK_TRANSFER:
{
GNUNET_assert (GNUNET_OK ==
TALER_string_to_amount
(cmd->details.check_bank_transfer.amount,
&amount));
if (GNUNET_OK != TALER_FAKEBANK_check
(fakebank,
&amount,
cmd->details.check_bank_transfer.account_debit,
cmd->details.check_bank_transfer.account_credit,
EXCHANGE_URL,
&cmd->details.check_bank_transfer.subject))
{
GNUNET_break (0);
fail (is);
return;
}
next_command (is);
return;
}
case OC_CHECK_BANK_TRANSFERS_EMPTY:
{
if (GNUNET_OK !=
TALER_FAKEBANK_check_empty (fakebank))
{
GNUNET_break (0);
fail (is);
return;
}
next_command (is);
return;
}
case OC_TRACK_TRANSFER:
{
struct TALER_WireTransferIdentifierRawP wtid;
const char *subject;
GNUNET_assert (NULL != ( ref = find_command
(is,
cmd->details.track_transfer.check_bank_ref)));
subject = ref->details.check_bank_transfer.subject;
GNUNET_assert (GNUNET_OK ==
GNUNET_STRINGS_string_to_data (subject,
strlen (subject),
&wtid,
sizeof (wtid)));
if (NULL == (cmd->details.track_transfer.tdo
= TALER_MERCHANT_track_transfer (ctx,
MERCHANT_URL,
instance,
"test",
&wtid,
EXCHANGE_URL,
&track_transfer_cb,
is)))
{
GNUNET_break (0);
fail (is);
}
}
return;
case OC_TRACK_TRANSACTION:
{
const struct Command *proposal_ref;
const char *order_id;
/* get proposal reference, and order_id from it */
GNUNET_assert(NULL != (ref = find_command
(is,
cmd->details.track_transaction.pay_ref)));
GNUNET_assert (NULL != (proposal_ref = find_command
(is,
ref->details.pay.contract_ref)));
order_id = json_string_value
(json_object_get (proposal_ref->details.proposal.contract_terms,
"order_id"));
if (NULL == (cmd->details.track_transaction.tth
= TALER_MERCHANT_track_transaction (ctx,
MERCHANT_URL,
instance,
order_id,
&track_transaction_cb,
is)))
{
GNUNET_break (0);
fail (is);
}
return;
}
case OC_HISTORY:
if (0 == cmd->details.history.date.abs_value_us)
{
cmd->details.history.date = GNUNET_TIME_absolute_add
(GNUNET_TIME_absolute_get (),
GNUNET_TIME_UNIT_HOURS);
GNUNET_TIME_round_abs (&cmd->details.history.date);
}
if (NULL == (cmd->details.history.ho
= TALER_MERCHANT_history (ctx,
MERCHANT_URL,
instance,
cmd->details.history.start,
cmd->details.history.nrows,
cmd->details.history.date,
&history_cb,
is)))
{
GNUNET_break (0);
fail (is);
}
break;
case OC_REFUND_INCREASE:
{
struct TALER_Amount refund_amount;
GNUNET_assert (GNUNET_OK ==
TALER_string_to_amount
(cmd->details.refund_increase.refund_amount,
&refund_amount));
if (NULL == (cmd->details.refund_increase.rio
= TALER_MERCHANT_refund_increase
(ctx,
MERCHANT_URL,
cmd->details.refund_increase.order_id,
&refund_amount,
cmd->details.refund_increase.reason,
instance,
refund_increase_cb,
is)))
{
GNUNET_break (0);
fail (is);
}
break;
}
case OC_REFUND_LOOKUP:
{
if (NULL == (cmd->details.refund_lookup.rlo
= TALER_MERCHANT_refund_lookup
(ctx,
MERCHANT_URL,
cmd->details.refund_lookup.order_id,
instance,
refund_lookup_cb,
is)))
{
GNUNET_break (0);
fail (is);
}
break;
}
case OC_TIP_AUTHORIZE:
{
GNUNET_assert (NULL != cmd->details.tip_authorize.amount);
GNUNET_assert (GNUNET_OK ==
TALER_string_to_amount (cmd->details.tip_authorize.amount,
&amount));
if (NULL == (cmd->details.tip_authorize.tao
= TALER_MERCHANT_tip_authorize
(ctx,
MERCHANT_URL,
"http://merchant.com/pickup",
"http://merchant.com/continue",
&amount,
cmd->details.tip_authorize.instance,
cmd->details.tip_authorize.justification,
&tip_authorize_cb,
is)))
{
GNUNET_break (0);
fail (is);
}
break;
}
case OC_TIP_PICKUP:
{
unsigned int num_planchets;
const struct Command *rr;
if (NULL == cmd->details.tip_pickup.replay_ref)
{
rr = NULL;
for (num_planchets=0;
NULL != cmd->details.tip_pickup.amounts[num_planchets];
num_planchets++);
ref = find_command (is,
cmd->details.tip_pickup.authorize_ref);
GNUNET_assert (NULL != ref);
GNUNET_assert (OC_TIP_AUTHORIZE == ref->oc);
}
else
{
rr = find_command (is,
cmd->details.tip_pickup.replay_ref);
GNUNET_assert (NULL != rr);
GNUNET_assert (OC_TIP_PICKUP == rr->oc);
num_planchets = rr->details.tip_pickup.num_coins;
ref = find_command (is,
rr->details.tip_pickup.authorize_ref);
GNUNET_assert (NULL != ref);
GNUNET_assert (OC_TIP_AUTHORIZE == ref->oc);
}
cmd->details.tip_pickup.num_coins = num_planchets;
{
struct TALER_PlanchetDetail planchets[num_planchets];
cmd->details.tip_pickup.psa = GNUNET_new_array (num_planchets,
struct TALER_PlanchetSecretsP);
cmd->details.tip_pickup.dks = GNUNET_new_array (num_planchets,
const struct TALER_EXCHANGE_DenomPublicKey *);
for (unsigned int i=0;idetails.tip_pickup.amounts[i],
&amount));
cmd->details.tip_pickup.dks[i]
= find_pk (is->keys,
&amount);
if (NULL == cmd->details.tip_pickup.dks[i])
{
GNUNET_break (0);
fail (is);
return;
}
TALER_planchet_setup_random (&cmd->details.tip_pickup.psa[i]);
}
else
{
cmd->details.tip_pickup.dks[i]
= rr->details.tip_pickup.dks[i];
cmd->details.tip_pickup.psa[i]
= rr->details.tip_pickup.psa[i];
}
if (GNUNET_OK !=
TALER_planchet_prepare (&cmd->details.tip_pickup.dks[i]->key,
&cmd->details.tip_pickup.psa[i],
&planchets[i]))
{
GNUNET_break (0);
fail (is);
return;
}
}
if (NULL == (cmd->details.tip_pickup.tpo
= TALER_MERCHANT_tip_pickup (ctx,
MERCHANT_URL,
&ref->details.tip_authorize.tip_id,
num_planchets,
planchets,
&pickup_cb,
is)))
{
GNUNET_break (0);
fail (is);
}
for (unsigned int i=0;ioc,
is->ip,
cmd->label);
fail (is);
return;
}
}
/**
* Function run when the test times out.
*
* @param cls NULL
*/
static void
do_timeout (void *cls)
{
timeout_task = NULL;
GNUNET_SCHEDULER_shutdown ();
}
/**
* Function run when the test terminates (good or bad).
* Cleans up our state.
*
* @param cls the interpreter state.
*/
static void
do_shutdown (void *cls)
{
struct InterpreterState *is = cls;
if (NULL != timeout_task)
{
GNUNET_SCHEDULER_cancel (timeout_task);
timeout_task = NULL;
}
GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
"Shutdown executing\n");
cleanup_state (is);
if (NULL != instance_priv)
{
GNUNET_free (instance_priv);
instance_priv = NULL;
}
if (NULL != is->task)
{
GNUNET_SCHEDULER_cancel (is->task);
is->task = NULL;
}
GNUNET_free (is);
for (unsigned int i=0;idrop_tables (db->cls);
TALER_MERCHANTDB_plugin_unload (db);
GNUNET_CONFIGURATION_destroy (cfg);
}
/**
* Functions of this type are called to provide the retrieved signing and
* denomination keys of the exchange. No TALER_EXCHANGE_*() functions should be called
* in this callback.
*
* @param cls closure
* @param keys information about keys of the exchange
* @param vc compatibility information
*/
static void
cert_cb (void *cls,
const struct TALER_EXCHANGE_Keys *keys,
enum TALER_EXCHANGE_VersionCompatibility vc)
{
struct InterpreterState *is = cls;
/* check that keys is OK */
#define ERR(cond) do { if(!(cond)) break; GNUNET_break (0); GNUNET_SCHEDULER_shutdown(); return; } while (0)
ERR (NULL == keys);
ERR (0 == keys->num_sign_keys);
GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
"Read %u signing keys\n",
keys->num_sign_keys);
ERR (0 == keys->num_denom_keys);
GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
"Read %u denomination keys\n",
keys->num_denom_keys);
#undef ERR
/* run actual tests via interpreter-loop */
GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
"Certificate callback invoked, starting interpreter\n");
is->keys = keys;
is->task = GNUNET_SCHEDULER_add_now (&interpreter_run,
is);
}
/**
* Signal handler called for SIGCHLD. Triggers the
* respective handler by writing to the trigger pipe.
*/
static void
sighandler_child_death ()
{
static char c;
int old_errno = errno; /* back-up errno */
GNUNET_break (1 == GNUNET_DISK_file_write
(GNUNET_DISK_pipe_handle (sigpipe,
GNUNET_DISK_PIPE_END_WRITE),
&c,
sizeof (c)));
errno = old_errno; /* restore errno */
}
/**
* Main function that will be run by the scheduler.
*
* @param cls closure
*/
static void
run (void *cls)
{
struct InterpreterState *is;
static const char *pickup_amounts_1[] = {
"EUR:5",
NULL
};
static struct Command commands[] =
{
/* Fill reserve with EUR:5.01, as withdraw fee is 1 ct per
config */
{ .oc = OC_ADMIN_ADD_INCOMING,
.label = "create-reserve-1",
.expected_response_code = MHD_HTTP_OK,
.details.admin_add_incoming.debit_account_no = 62,
.details.admin_add_incoming.credit_account_no = EXCHANGE_ACCOUNT_NO,
.details.admin_add_incoming.auth_username = "user62",
.details.admin_add_incoming.auth_password = "pass62",
.details.admin_add_incoming.amount = "EUR:10.02" },
/* Run wirewatch to observe /admin/add/incoming */
{ .oc = OC_RUN_WIREWATCH,
.label = "wirewatch-1" },
{ .oc = OC_CHECK_BANK_TRANSFER,
.label = "check_bank_transfer-1",
.details.check_bank_transfer.amount = "EUR:10.02",
.details.check_bank_transfer.account_debit = 62,
.details.check_bank_transfer.account_credit = EXCHANGE_ACCOUNT_NO
},
/* Withdraw a 5 EUR coin, at fee of 1 ct */
{ .oc = OC_WITHDRAW_SIGN,
.label = "withdraw-coin-1",
.expected_response_code = MHD_HTTP_OK,
.details.reserve_withdraw.reserve_reference
= "create-reserve-1",
.details.reserve_withdraw.amount = "EUR:5" },
/* Withdraw a 5 EUR coin, at fee of 1 ct */
{ .oc = OC_WITHDRAW_SIGN,
.label = "withdraw-coin-2",
.expected_response_code = MHD_HTTP_OK,
.details.reserve_withdraw.reserve_reference
= "create-reserve-1",
.details.reserve_withdraw.amount = "EUR:5" },
/* Check that deposit and withdraw operation are in history,
and that the balance is now at zero */
{ .oc = OC_WITHDRAW_STATUS,
.label = "withdraw-status-1",
.expected_response_code = MHD_HTTP_OK,
.details.reserve_status.reserve_reference
= "create-reserve-1",
.details.reserve_status.expected_balance = "EUR:0" },
/* Create proposal */
{ .oc = OC_PROPOSAL,
.label = "create-proposal-1",
.expected_response_code = MHD_HTTP_OK,
.details.proposal.order = "{\
\"max_fee\":\
{\"currency\":\"EUR\",\
\"value\":0,\
\"fraction\":50000000},\
\"order_id\":\"1\",\
\"refund_deadline\":\"\\/Date(0)\\/\",\
\"pay_deadline\":\"\\/Date(99999999999)\\/\",\
\"amount\":\
{\"currency\":\"EUR\",\
\"value\":5,\
\"fraction\":0},\
\"summary\": \"merchant-lib testcase\",\
\"products\":\
[ {\"description\":\"ice cream\",\
\"value\":\"{EUR:5}\"} ] }"},
/* check payment before we pay */
{ .oc = OC_CHECK_PAYMENT,
.label = "check-payment-1",
.expected_response_code = MHD_HTTP_OK,
.details.check_payment.contract_ref = "create-proposal-1",
.details.check_payment.expect_paid = GNUNET_NO },
/* execute simple payment */
{ .oc = OC_PAY,
.label = "deposit-simple",
.expected_response_code = MHD_HTTP_OK,
.details.pay.contract_ref = "create-proposal-1",
.details.pay.coin_ref = "withdraw-coin-1",
.details.pay.refund_fee = "EUR:0.01",
.details.pay.amount_with_fee = "EUR:5",
.details.pay.amount_without_fee = "EUR:4.99" },
/* check payment after we've paid */
{ .oc = OC_CHECK_PAYMENT,
.label = "check-payment-2",
.expected_response_code = MHD_HTTP_OK,
.details.check_payment.contract_ref = "create-proposal-1",
.details.check_payment.expect_paid = GNUNET_YES },
/* Test for #5262: abort after full payment */
{ .oc = OC_PAY_ABORT,
.label = "pay-abort-2",
.expected_response_code = MHD_HTTP_FORBIDDEN,
.details.pay_abort.pay_ref = "deposit-simple",
},
/* Try to replay payment reusing coin */
{ .oc = OC_PAY,
.label = "replay-simple",
.expected_response_code = MHD_HTTP_OK,
.details.pay.contract_ref = "create-proposal-1",
.details.pay.coin_ref = "withdraw-coin-1",
.details.pay.refund_fee = "EUR:0.01",
.details.pay.amount_with_fee = "EUR:5",
.details.pay.amount_without_fee = "EUR:4.99" },
/* Create another contract */
{ .oc = OC_PROPOSAL,
.label = "create-proposal-2",
.expected_response_code = MHD_HTTP_OK,
.details.proposal.order = "{\
\"max_fee\":\
{\"currency\":\"EUR\",\
\"value\":0,\
\"fraction\":50000000},\
\"order_id\":\"2\",\
\"refund_deadline\":\"\\/Date(0)\\/\",\
\"pay_deadline\":\"\\/Date(9999999999)\\/\",\
\"amount\":\
{\"currency\":\"EUR\",\
\"value\":5,\
\"fraction\":0},\
\"summary\":\"useful product\",\
\"products\":\
[ {\"description\":\"ice cream\",\
\"value\":\"{EUR:5}\"} ] }" },
/**
* Try to double-spend the 5 EUR coin at the same
* merchant (but different transaction ID)
*/
{ .oc = OC_PAY,
.label = "deposit-double-2",
.expected_response_code = MHD_HTTP_FORBIDDEN,
.details.pay.contract_ref = "create-proposal-2",
.details.pay.coin_ref = "withdraw-coin-1",
.details.pay.refund_fee = "EUR:0.01",
.details.pay.amount_with_fee = "EUR:5",
.details.pay.amount_without_fee = "EUR:4.99" },
{ .oc = OC_HISTORY,
.label = "history-0",
.expected_response_code = MHD_HTTP_OK,
/**
* all records to be returned; setting date as 0 lets the
* interpreter set it as 'now' + one hour delta, just to
* make sure it surpasses the proposal's timestamp.
*/
.details.history.date.abs_value_us = 0,
/**
* We only expect ONE result (create-proposal-1) to be
* included in /history response, because create-proposal-3
* did NOT go through because of double spending.
*/
.details.history.nresult = 1,
.details.history.start = 10,
.details.history.nrows = 10
},
/* Fill second reserve with EUR:1 */
{ .oc = OC_ADMIN_ADD_INCOMING,
.label = "create-reserve-2",
.expected_response_code = MHD_HTTP_OK,
.details.admin_add_incoming.debit_account_no = 63,
.details.admin_add_incoming.credit_account_no = EXCHANGE_ACCOUNT_NO,
.details.admin_add_incoming.auth_username = "user63",
.details.admin_add_incoming.auth_password = "pass63",
.details.admin_add_incoming.amount = "EUR:1" },
/* Add another 4.01 EUR to reserve #2 */
{ .oc = OC_ADMIN_ADD_INCOMING,
.label = "create-reserve-2b",
.expected_response_code = MHD_HTTP_OK,
.details.admin_add_incoming.reserve_reference = "create-reserve-2",
.details.admin_add_incoming.debit_account_no = 63,
.details.admin_add_incoming.credit_account_no = EXCHANGE_ACCOUNT_NO,
.details.admin_add_incoming.auth_username = "user63",
.details.admin_add_incoming.auth_password = "pass63",
.details.admin_add_incoming.amount = "EUR:4.01" },
/* Run wirewatch to observe /admin/add/incoming */
{ .oc = OC_RUN_WIREWATCH,
.label = "wirewatch-2" },
{ .oc = OC_CHECK_BANK_TRANSFER,
.label = "check_bank_transfer-2",
.details.check_bank_transfer.amount = "EUR:1",
.details.check_bank_transfer.account_debit = 63,
.details.check_bank_transfer.account_credit = EXCHANGE_ACCOUNT_NO
},
{ .oc = OC_CHECK_BANK_TRANSFER,
.label = "check_bank_transfer-2",
.details.check_bank_transfer.amount = "EUR:4.01",
.details.check_bank_transfer.account_debit = 63,
.details.check_bank_transfer.account_credit = EXCHANGE_ACCOUNT_NO
},
/* Withdraw a 5 EUR coin, at fee of 1 ct */
{ .oc = OC_WITHDRAW_SIGN,
.label = "withdraw-coin-2",
.expected_response_code = MHD_HTTP_OK,
.details.reserve_withdraw.reserve_reference
= "create-reserve-2",
.details.reserve_withdraw.amount = "EUR:5" },
/* Proposal lookup */
{
.oc = OC_PROPOSAL_LOOKUP,
.label = "fetch-proposal-2",
.expected_response_code = MHD_HTTP_OK,
.details.proposal_lookup.proposal_reference
= "create-proposal-2" },
/* Check nothing happened on the bank side so far */
{ .oc = OC_CHECK_BANK_TRANSFERS_EMPTY,
.label = "check_bank_empty" },
/* Run transfers. */
{ .oc = OC_RUN_AGGREGATOR,
.label = "run-aggregator" },
/* Obtain WTID of the transfer generated by "deposit-simple" */
{ .oc = OC_CHECK_BANK_TRANSFER,
.label = "check_bank_transfer-498c",
.details.check_bank_transfer.amount = "EUR:4.98",
/* exchange-outgoing */
.details.check_bank_transfer.account_debit = 2,
/* merchant */
.details.check_bank_transfer.account_credit = 62
},
/* Check that there are no other unusual transfers */
{ .oc = OC_CHECK_BANK_TRANSFERS_EMPTY,
.label = "check_bank_empty" },
{ .oc = OC_TRACK_TRANSACTION,
.label = "track-transaction-1",
.expected_response_code = MHD_HTTP_OK,
.details.track_transaction.expected_transfer_ref
= "check_bank_transfer-498c",
.details.track_transaction.pay_ref = "deposit-simple",
.details.track_transaction.wire_fee = "EUR:0.01"
},
/* Trace the WTID back to the original transaction */
{ .oc = OC_TRACK_TRANSFER,
.label = "track-transfer-1",
.expected_response_code = MHD_HTTP_OK,
.details.track_transfer.check_bank_ref
= "check_bank_transfer-498c",
.details.track_transfer.expected_pay_ref = "deposit-simple"
},
{ .oc = OC_TRACK_TRANSFER,
.label = "track-transfer-1-again",
.expected_response_code = MHD_HTTP_OK,
.details.track_transfer.check_bank_ref
= "check_bank_transfer-498c",
.details.track_transfer.expected_pay_ref = "deposit-simple"
},
/* Pay again successfully on 2nd contract */
{ .oc = OC_PAY,
.label = "deposit-simple-2",
.expected_response_code = MHD_HTTP_OK,
.details.pay.contract_ref = "create-proposal-2",
.details.pay.coin_ref = "withdraw-coin-2",
.details.pay.refund_fee = "EUR:0.01",
.details.pay.amount_with_fee = "EUR:5",
.details.pay.amount_without_fee = "EUR:4.99" },
/* Run transfers. */
{ .oc = OC_RUN_AGGREGATOR,
.label = "run-aggregator-2" },
/* Obtain WTID of the transfer */
{ .oc = OC_CHECK_BANK_TRANSFER,
.label = "check_bank_transfer-498c-2",
.details.check_bank_transfer.amount = "EUR:4.98",
/* exchange-outgoing */
.details.check_bank_transfer.account_debit = 2,
/* merchant */
.details.check_bank_transfer.account_credit = 62
},
/* Check that there are no other unusual transfers */
{ .oc = OC_CHECK_BANK_TRANSFERS_EMPTY,
.label = "check_bank_empty" },
/* Trace the WTID back to the original transaction */
{ .oc = OC_TRACK_TRANSFER,
.label = "track-transfer-2",
.expected_response_code = MHD_HTTP_OK,
.details.track_transfer.check_bank_ref
= "check_bank_transfer-498c-2",
.details.track_transfer.expected_pay_ref
= "deposit-simple-2"
},
{ .oc = OC_TRACK_TRANSFER,
.label = "track-transfer-2-again",
.expected_response_code = MHD_HTTP_OK,
.details.track_transfer.check_bank_ref
= "check_bank_transfer-498c-2",
.details.track_transfer.expected_pay_ref
= "deposit-simple-2"
},
{ .oc = OC_TRACK_TRANSACTION,
.label = "track-transaction-2",
.expected_response_code = MHD_HTTP_OK,
.details.track_transaction.expected_transfer_ref
= "check_bank_transfer-498c-2",
.details.track_transaction.wire_fee = "EUR:0.01",
.details.track_transaction.pay_ref = "deposit-simple-2"
},
{ .oc = OC_HISTORY,
.label = "history-1",
.expected_response_code = MHD_HTTP_OK,
.details.history.date.abs_value_us = 0,
/**
* Now we expect BOTH contracts (create-proposal-{1,2})
* to be included in /history response, because
* create-proposal-2 has now been correctly paid.
*/
.details.history.nresult = 2,
.details.history.start = 10,
.details.history.nrows = 10
},
{ .oc = OC_HISTORY,
.label = "history-2",
.expected_response_code = MHD_HTTP_OK,
/*no records returned, time limit too ancient*/
.details.history.date.abs_value_us = 1,
.details.history.nresult = 0,
.details.history.start = 10,
.details.history.nrows = 10
},
{ .oc = OC_REFUND_INCREASE,
.label = "refund-increase-1",
.details.refund_increase.refund_amount = "EUR:0.1",
.details.refund_increase.refund_fee = "EUR:0.01",
.details.refund_increase.reason = "refund test",
.details.refund_increase.order_id = "1"
},
{ .oc = OC_REFUND_LOOKUP,
.label = "refund-lookup-1",
.details.refund_lookup.order_id = "1",
.details.refund_lookup.increase_ref = "refund-increase-1",
.details.refund_lookup.pay_ref = "deposit-simple"
},
/* Test tipping */
{ .oc = OC_ADMIN_ADD_INCOMING,
.label = "create-reserve-tip-1",
.expected_response_code = MHD_HTTP_OK,
.details.admin_add_incoming.instance = "tip",
.details.admin_add_incoming.debit_account_no = 62,
.details.admin_add_incoming.credit_account_no = EXCHANGE_ACCOUNT_NO,
.details.admin_add_incoming.auth_username = "user62",
.details.admin_add_incoming.auth_password = "pass62",
/* we run *two* instances, but only this first call will
actually fill the reserve, as the second one will be seen as
a duplicate. Hence fill with twice the require amount per
round. */
.details.admin_add_incoming.amount = "EUR:20.04" },
/* Run wirewatch to observe /admin/add/incoming */
{ .oc = OC_RUN_WIREWATCH,
.label = "wirewatch-3" },
{ .oc = OC_CHECK_BANK_TRANSFER,
.label = "check_bank_transfer-tip-1",
.details.check_bank_transfer.amount = "EUR:20.04",
.details.check_bank_transfer.account_debit = 62,
.details.check_bank_transfer.account_credit = EXCHANGE_ACCOUNT_NO
},
/* Authorize two tips */
{ .oc = OC_TIP_AUTHORIZE,
.label = "authorize-tip-1",
.expected_response_code = MHD_HTTP_OK,
.details.tip_authorize.instance = "tip",
.details.tip_authorize.justification = "tip 1",
.details.tip_authorize.amount = "EUR:5.01" },
{ .oc = OC_TIP_AUTHORIZE,
.label = "authorize-tip-2",
.expected_response_code = MHD_HTTP_OK,
.details.tip_authorize.instance = "tip",
.details.tip_authorize.justification = "tip 2",
.details.tip_authorize.amount = "EUR:5.01" },
/* Check tip status */
{ .oc = OC_TIP_QUERY,
.label = "query-tip-1",
.expected_response_code = MHD_HTTP_OK,
.details.tip_query.instance = "tip" },
{ .oc = OC_TIP_QUERY,
.label = "query-tip-2",
.expected_response_code = MHD_HTTP_OK,
.details.tip_query.instance = "tip",
.details.tip_query.expected_amount_authorized = "EUR:10.02",
.details.tip_query.expected_amount_picked_up = "EUR:0.0",
.details.tip_query.expected_amount_available = "EUR:20.04" },
/* Withdraw tip */
{ .oc = OC_TIP_PICKUP,
.label = "pickup-tip-1",
.expected_response_code = MHD_HTTP_OK,
.details.tip_pickup.authorize_ref = "authorize-tip-1",
.details.tip_pickup.amounts = pickup_amounts_1 },
/* Check tip status again */
{ .oc = OC_TIP_QUERY,
.label = "query-tip-3",
.expected_response_code = MHD_HTTP_OK,
.details.tip_query.instance = "tip",
.details.tip_query.expected_amount_picked_up = "EUR:5.01",
.details.tip_query.expected_amount_available = "EUR:15.03" },
{ .oc = OC_TIP_PICKUP,
.label = "pickup-tip-2",
.expected_response_code = MHD_HTTP_OK,
.details.tip_pickup.authorize_ref = "authorize-tip-2",
.details.tip_pickup.amounts = pickup_amounts_1 },
{ .oc = OC_TIP_QUERY,
.label = "query-tip-4",
.expected_response_code = MHD_HTTP_OK,
.details.tip_query.instance = "tip",
.details.tip_query.expected_amount_picked_up = "EUR:10.02",
.details.tip_query.expected_amount_available = "EUR:10.02",
.details.tip_query.expected_amount_authorized = "EUR:10.02" },
{ .oc = OC_TIP_PICKUP,
.label = "pickup-tip-2b",
.expected_response_code = MHD_HTTP_OK,
.details.tip_pickup.replay_ref = "pickup-tip-2",
.details.tip_pickup.authorize_ref = "authorize-tip-2",
.details.tip_pickup.amounts = pickup_amounts_1 },
/* Test authorization failure modes */
{ .oc = OC_TIP_AUTHORIZE,
.label = "authorize-tip-3-insufficient-funds",
.expected_response_code = MHD_HTTP_PRECONDITION_FAILED,
.details.tip_authorize.instance = "dtip",
.details.tip_authorize.justification = "tip 3",
.details.tip_authorize.amount = "EUR:5.01",
.details.tip_authorize.expected_ec = TALER_EC_TIP_AUTHORIZE_INSUFFICIENT_FUNDS },
{ .oc = OC_TIP_AUTHORIZE,
.label = "authorize-tip-4-unknown-instance",
.expected_response_code = MHD_HTTP_NOT_FOUND,
.details.tip_authorize.instance = "unknown",
.details.tip_authorize.justification = "tip 4",
.details.tip_authorize.amount = "EUR:5.01",
.details.tip_authorize.expected_ec = TALER_EC_TIP_AUTHORIZE_INSTANCE_UNKNOWN },
{ .oc = OC_TIP_AUTHORIZE,
.label = "authorize-tip-5-notip-instance",
.expected_response_code = MHD_HTTP_NOT_FOUND,
.details.tip_authorize.instance = "default",
.details.tip_authorize.justification = "tip 5",
.details.tip_authorize.amount = "EUR:5.01",
.details.tip_authorize.expected_ec = TALER_EC_TIP_AUTHORIZE_INSTANCE_DOES_NOT_TIP },
{ .oc = OC_TIP_PICKUP,
.label = "pickup-tip-3-too-much",
.expected_response_code = MHD_HTTP_CONFLICT,
.details.tip_pickup.expected_ec = TALER_EC_TIP_PICKUP_NO_FUNDS,
.details.tip_pickup.authorize_ref = "authorize-tip-1",
.details.tip_pickup.amounts = pickup_amounts_1 },
/* Spend tip (just to be sure...) */
{ .oc = OC_PROPOSAL,
.label = "create-proposal-tip-1",
.expected_response_code = MHD_HTTP_OK,
.details.proposal.order = "{\
\"max_fee\":\
{\"currency\":\"EUR\",\
\"value\":0,\
\"fraction\":50000000},\
\"order_id\":\"1-tip\",\
\"refund_deadline\":\"\\/Date(0)\\/\",\
\"pay_deadline\":\"\\/Date(99999999999)\\/\",\
\"amount\":\
{\"currency\":\"EUR\",\
\"value\":5,\
\"fraction\":0},\
\"summary\": \"merchant-lib testcase\",\
\"products\":\
[ {\"description\":\"ice cream tip\",\
\"value\":\"{EUR:5}\"} ] }"},
{ .oc = OC_PAY,
.label = "deposit-tip-simple",
.expected_response_code = MHD_HTTP_OK,
.details.pay.contract_ref = "create-proposal-tip-1",
.details.pay.coin_ref = "pickup-tip-1",
.details.pay.refund_fee = "EUR:0.01",
.details.pay.amount_with_fee = "EUR:5",
.details.pay.amount_without_fee = "EUR:4.99" },
/* Run transfers. */
{ .oc = OC_RUN_AGGREGATOR,
.label = "run-aggregator-tip-1" },
{ .oc = OC_CHECK_BANK_TRANSFER,
.label = "check_bank_transfer-tip-498c",
.details.check_bank_transfer.amount = "EUR:4.98",
/* exchange-outgoing */
.details.check_bank_transfer.account_debit = 2,
/* merchant */
.details.check_bank_transfer.account_credit = 62
},
/* Check that there are no other unusual transfers */
{ .oc = OC_CHECK_BANK_TRANSFERS_EMPTY,
.label = "check_bank_empty" },
/* ****************** /pay again logic ************* */
/* Fill reserve with EUR:10.02, as withdraw fee is 1 ct per
config */
{ .oc = OC_ADMIN_ADD_INCOMING,
.label = "create-reserve-10",
.expected_response_code = MHD_HTTP_OK,
.details.admin_add_incoming.debit_account_no = 62,
.details.admin_add_incoming.credit_account_no = EXCHANGE_ACCOUNT_NO,
.details.admin_add_incoming.auth_username = "user62",
.details.admin_add_incoming.auth_password = "pass62",
.details.admin_add_incoming.amount = "EUR:10.02" },
/* Run wirewatch to observe /admin/add/incoming */
{ .oc = OC_RUN_WIREWATCH,
.label = "wirewatch-10" },
{ .oc = OC_CHECK_BANK_TRANSFER,
.label = "check_bank_transfer-10",
.details.check_bank_transfer.amount = "EUR:10.02",
.details.check_bank_transfer.account_debit = 62,
.details.check_bank_transfer.account_credit = EXCHANGE_ACCOUNT_NO
},
/* Withdraw a 5 EUR coin, at fee of 1 ct */
{ .oc = OC_WITHDRAW_SIGN,
.label = "withdraw-coin-10a",
.expected_response_code = MHD_HTTP_OK,
.details.reserve_withdraw.reserve_reference
= "create-reserve-10",
.details.reserve_withdraw.amount = "EUR:5" },
/* Withdraw a 5 EUR coin, at fee of 1 ct */
{ .oc = OC_WITHDRAW_SIGN,
.label = "withdraw-coin-10b",
.expected_response_code = MHD_HTTP_OK,
.details.reserve_withdraw.reserve_reference
= "create-reserve-10",
.details.reserve_withdraw.amount = "EUR:5" },
/* Check that deposit and withdraw operation are in history,
and that the balance is now at zero */
{ .oc = OC_WITHDRAW_STATUS,
.label = "withdraw-status-10",
.expected_response_code = MHD_HTTP_OK,
.details.reserve_status.reserve_reference
= "create-reserve-10",
.details.reserve_status.expected_balance = "EUR:0" },
/* Create proposal */
{ .oc = OC_PROPOSAL,
.label = "create-proposal-10",
.expected_response_code = MHD_HTTP_OK,
.details.proposal.order = "{\
\"max_fee\":\
{\"currency\":\"EUR\",\
\"value\":0,\
\"fraction\":50000000},\
\"order_id\":\"10\",\
\"refund_deadline\":\"\\/Date(0)\\/\",\
\"pay_deadline\":\"\\/Date(99999999999)\\/\",\
\"amount\":\
{\"currency\":\"EUR\",\
\"value\":10,\
\"fraction\":0},\
\"summary\": \"merchant-lib testcase\",\
\"products\":\
[ {\"description\":\"ice cream\",\
\"value\":\"{EUR:10}\"} ] }"},
/* execute simple payment, re-using one ancient coin */
{ .oc = OC_PAY,
.label = "pay-fail-partial-double-10",
.expected_response_code = MHD_HTTP_FORBIDDEN,
.details.pay.contract_ref = "create-proposal-10",
.details.pay.coin_ref = "withdraw-coin-10a;withdraw-coin-1",
/* These amounts are given per coin! */
.details.pay.refund_fee = "EUR:0.01",
.details.pay.amount_with_fee = "EUR:5",
.details.pay.amount_without_fee = "EUR:4.99" },
/* Try to replay payment reusing coin */
{ .oc = OC_PAY_AGAIN,
.label = "pay-again-10",
.expected_response_code = MHD_HTTP_OK,
.details.pay.refund_fee = "EUR:0.01",
.details.pay_again.pay_ref = "pay-fail-partial-double-10",
.details.pay_again.coin_ref = "withdraw-coin-10a;withdraw-coin-10b" },
/* Run transfers. */
{ .oc = OC_RUN_AGGREGATOR,
.label = "run-aggregator-10" },
/* Obtain WTID of the transfer */
{ .oc = OC_CHECK_BANK_TRANSFER,
.label = "check_bank_transfer-9.97-10",
.details.check_bank_transfer.amount = "EUR:9.97",
/* exchange-outgoing */
.details.check_bank_transfer.account_debit = 2,
/* merchant */
.details.check_bank_transfer.account_credit = 62
},
/* Check that there are no other unusual transfers */
{ .oc = OC_CHECK_BANK_TRANSFERS_EMPTY,
.label = "check_bank_empty-10" },
/* ****************** /pay abort logic ************* */
/* Fill reserve with EUR:10.02, as withdraw fee is 1 ct per
config */
{ .oc = OC_ADMIN_ADD_INCOMING,
.label = "create-reserve-11",
.expected_response_code = MHD_HTTP_OK,
.details.admin_add_incoming.debit_account_no = 62,
.details.admin_add_incoming.credit_account_no = EXCHANGE_ACCOUNT_NO,
.details.admin_add_incoming.auth_username = "user62",
.details.admin_add_incoming.auth_password = "pass62",
.details.admin_add_incoming.amount = "EUR:10.02" },
/* Run wirewatch to observe /admin/add/incoming */
{ .oc = OC_RUN_WIREWATCH,
.label = "wirewatch-11" },
{ .oc = OC_CHECK_BANK_TRANSFER,
.label = "check_bank_transfer-11",
.details.check_bank_transfer.amount = "EUR:10.02",
.details.check_bank_transfer.account_debit = 62,
.details.check_bank_transfer.account_credit = EXCHANGE_ACCOUNT_NO
},
/* Withdraw a 5 EUR coin, at fee of 1 ct */
{ .oc = OC_WITHDRAW_SIGN,
.label = "withdraw-coin-11a",
.expected_response_code = MHD_HTTP_OK,
.details.reserve_withdraw.reserve_reference
= "create-reserve-11",
.details.reserve_withdraw.amount = "EUR:5" },
/* Withdraw a 5 EUR coin, at fee of 1 ct */
{ .oc = OC_WITHDRAW_SIGN,
.label = "withdraw-coin-11b",
.expected_response_code = MHD_HTTP_OK,
.details.reserve_withdraw.reserve_reference
= "create-reserve-11",
.details.reserve_withdraw.amount = "EUR:5" },
/* Check that deposit and withdraw operation are in history,
and that the balance is now at zero */
{ .oc = OC_WITHDRAW_STATUS,
.label = "withdraw-status-11",
.expected_response_code = MHD_HTTP_OK,
.details.reserve_status.reserve_reference
= "create-reserve-11",
.details.reserve_status.expected_balance = "EUR:0" },
/* Create proposal */
{ .oc = OC_PROPOSAL,
.label = "create-proposal-11",
.expected_response_code = MHD_HTTP_OK,
.details.proposal.order = "{\
\"max_fee\":\
{\"currency\":\"EUR\",\
\"value\":0,\
\"fraction\":50000000},\
\"order_id\":\"11\",\
\"refund_deadline\":\"\\/Date(0)\\/\",\
\"pay_deadline\":\"\\/Date(99999999999)\\/\",\
\"amount\":\
{\"currency\":\"EUR\",\
\"value\":10,\
\"fraction\":0},\
\"summary\": \"merchant-lib testcase\",\
\"products\":\
[ {\"description\":\"ice cream\",\
\"value\":\"{EUR:10}\"} ] }"},
/* execute simple payment, re-using one ancient coin */
{ .oc = OC_PAY,
.label = "pay-fail-partial-double-11",
.expected_response_code = MHD_HTTP_FORBIDDEN,
.details.pay.contract_ref = "create-proposal-11",
.details.pay.coin_ref = "withdraw-coin-11a;withdraw-coin-1",
/* These amounts are given per coin! */
.details.pay.refund_fee = "EUR:0.01",
.details.pay.amount_with_fee = "EUR:5",
.details.pay.amount_without_fee = "EUR:4.99" },
/* Try to replay payment reusing coin */
{ .oc = OC_PAY_ABORT,
.label = "pay-abort-11",
.expected_response_code = MHD_HTTP_OK,
.details.pay_abort.pay_ref = "pay-fail-partial-double-11",
},
{ .oc = OC_PAY_ABORT_REFUND,
.label = "pay-abort-refund-11",
.expected_response_code = MHD_HTTP_OK,
.details.pay_abort_refund.abort_ref = "pay-abort-11",
.details.pay_abort_refund.num_coin = 0,
.details.pay_abort_refund.refund_amount = "EUR:5",
.details.pay_abort_refund.refund_fee = "EUR:0.01" },
/* Run transfers. */
{ .oc = OC_RUN_AGGREGATOR,
.label = "run-aggregator-11" },
/* Check that there are no other unusual transfers */
{ .oc = OC_CHECK_BANK_TRANSFERS_EMPTY,
.label = "check_bank_empty-11" },
/* end of testcase */
{ .oc = OC_END }
};
GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
"Interpreter initializing\n");
fakebank = TALER_FAKEBANK_start (BANK_PORT);
if (NULL == fakebank)
{
fprintf (stderr,
"\nFailed to start fake bank service\n");
result = 77;
return;
}
is = GNUNET_new (struct InterpreterState);
is->commands = commands;
GNUNET_assert (ctx = GNUNET_CURL_init
(&GNUNET_CURL_gnunet_scheduler_reschedule,
&rc));
rc = GNUNET_CURL_gnunet_rc_create (ctx);
GNUNET_assert (NULL != (exchange
= TALER_EXCHANGE_connect (ctx,
EXCHANGE_URL,
&cert_cb, is,
TALER_EXCHANGE_OPTION_END)));
timeout_task
= GNUNET_SCHEDULER_add_delayed (GNUNET_TIME_relative_multiply
(GNUNET_TIME_UNIT_SECONDS, 150),
&do_timeout, NULL);
GNUNET_SCHEDULER_add_shutdown (&do_shutdown, is);
}
/**
* Main function for the testcase for the exchange API.
*
* @param argc expected to be 1
* @param argv expected to only contain the program name
*/
int
main (int argc,
char * const *argv)
{
char *_instances;
char *token;
struct GNUNET_OS_Process *proc;
struct GNUNET_OS_Process *exchanged;
struct GNUNET_OS_Process *merchantd;
unsigned int cnt;
struct GNUNET_SIGNAL_Context *shc_chld;
unsetenv ("XDG_DATA_HOME");
unsetenv ("XDG_CONFIG_HOME");
GNUNET_log_setup ("test-merchant-api",
"DEBUG",
NULL);
cfg = GNUNET_CONFIGURATION_create ();
GNUNET_assert (GNUNET_OK ==
GNUNET_CONFIGURATION_load (cfg,
"test_merchant_api.conf"));
GNUNET_assert (GNUNET_OK ==
GNUNET_CONFIGURATION_get_value_string (cfg,
"merchant",
"INSTANCES",
&_instances));
GNUNET_log (GNUNET_ERROR_TYPE_INFO,
"Found instances `%s'\n",
_instances);
GNUNET_break (NULL != (token = strtok (_instances, " ")));
GNUNET_array_append (instances,
ninstances,
GNUNET_strdup (token));
while (NULL != (token = strtok (NULL, " ")))
GNUNET_array_append (instances,
ninstances,
GNUNET_strdup (token));
GNUNET_free (_instances);
instance = instances[instance_idx];
instance_priv = get_instance_priv (cfg, instance);
db = TALER_MERCHANTDB_plugin_load (cfg);
if (NULL == db)
{
GNUNET_CONFIGURATION_destroy (cfg);
return 77;
}
(void) db->drop_tables (db->cls);
if (GNUNET_OK != db->initialize (db->cls))
{
TALER_MERCHANTDB_plugin_unload (db);
GNUNET_CONFIGURATION_destroy (cfg);
return 77;
}
if (NULL == (proc = GNUNET_OS_start_process
(GNUNET_NO,
GNUNET_OS_INHERIT_STD_ALL,
NULL, NULL, NULL,
"taler-exchange-keyup",
"taler-exchange-keyup",
"-c", "test_merchant_api.conf",
NULL)))
{
GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
"Failed to run taler-exchange-keyup. Check your PATH.\n");
return 77;
}
GNUNET_OS_process_wait (proc);
GNUNET_OS_process_destroy (proc);
if (NULL == (proc = GNUNET_OS_start_process
(GNUNET_NO,
GNUNET_OS_INHERIT_STD_ALL,
NULL, NULL, NULL,
"taler-exchange-dbinit",
"taler-exchange-dbinit",
"-c", "test_merchant_api.conf",
"-r",
NULL)))
{
GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
"Failed to run taler-exchange-dbinit. Check your PATH.\n");
return 77;
}
GNUNET_OS_process_wait (proc);
GNUNET_OS_process_destroy (proc);
if (NULL == (exchanged = GNUNET_OS_start_process
(GNUNET_NO,
GNUNET_OS_INHERIT_STD_ALL,
NULL, NULL, NULL,
"taler-exchange-httpd",
"taler-exchange-httpd",
"-c", "test_merchant_api.conf",
NULL)))
{
GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
"Failed to run taler-exchange-httpd. Check your PATH.\n");
return 77;
}
/* give child time to start and bind against the socket */
GNUNET_log (GNUNET_ERROR_TYPE_INFO,
"Waiting for taler-exchange-httpd to be ready\n");
cnt = 0;
do
{
fprintf (stderr, ".");
sleep (1);
cnt++;
if (cnt > 60)
{
GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
"\nFailed to start taler-exchange-httpd\n");
GNUNET_OS_process_kill (exchanged,
SIGKILL);
GNUNET_OS_process_wait (exchanged);
GNUNET_OS_process_destroy (exchanged);
return 77;
}
}
while (0 != system ("wget -q -t 1 -T 1 " EXCHANGE_URL "keys -o /dev/null -O /dev/null"));
fprintf (stderr, "\n");
if (NULL == (merchantd = GNUNET_OS_start_process
(GNUNET_NO,
GNUNET_OS_INHERIT_STD_ALL,
NULL, NULL, NULL,
"taler-merchant-httpd",
"taler-merchant-httpd",
"-c", "test_merchant_api.conf",
"-L", "DEBUG",
NULL)))
{
GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
"Failed to run taler-merchant-httpd. Check your PATH.\n");
GNUNET_OS_process_kill (exchanged,
SIGKILL);
GNUNET_OS_process_wait (exchanged);
GNUNET_OS_process_destroy (exchanged);
return 77;
}
/* give child time to start and bind against the socket */
GNUNET_log (GNUNET_ERROR_TYPE_INFO,
"Waiting for taler-merchant-httpd to be ready\n");
cnt = 0;
do
{
fprintf (stderr, ".");
sleep (1);
cnt++;
if (cnt > 60)
{
GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
"\nFailed to start taler-merchant-httpd\n");
GNUNET_OS_process_kill (merchantd,
SIGKILL);
GNUNET_OS_process_wait (merchantd);
GNUNET_OS_process_destroy (merchantd);
GNUNET_OS_process_kill (exchanged,
SIGKILL);
GNUNET_OS_process_wait (exchanged);
GNUNET_OS_process_destroy (exchanged);
return 77;
}
}
while (0 != system ("wget -q -t 1 -T 1 " MERCHANT_URL " -o /dev/null -O /dev/null"));
fprintf (stderr, "\n");
result = GNUNET_SYSERR;
GNUNET_assert (NULL != (sigpipe = GNUNET_DISK_pipe
(GNUNET_NO, GNUNET_NO, GNUNET_NO, GNUNET_NO)));
shc_chld = GNUNET_SIGNAL_handler_install
(GNUNET_SIGCHLD,
&sighandler_child_death);
GNUNET_SCHEDULER_run (&run, NULL);
GNUNET_SIGNAL_handler_uninstall (shc_chld);
shc_chld = NULL;
GNUNET_DISK_pipe_close (sigpipe);
GNUNET_OS_process_kill (merchantd,
SIGTERM);
GNUNET_OS_process_wait (merchantd);
GNUNET_OS_process_destroy (merchantd);
GNUNET_OS_process_kill (exchanged,
SIGTERM);
GNUNET_OS_process_wait (exchanged);
GNUNET_OS_process_destroy (exchanged);
if (77 == result)
return 77;
return (GNUNET_OK == result) ? 0 : 1;
}