-fix fieldnames, regenerated DBs

This commit is contained in:
Christian Grothoff 2022-04-24 20:49:11 +02:00
parent b671d6b25d
commit b4965db0d2
No known key found for this signature in database
GPG Key ID: 939E6BE1E29FC3CC
15 changed files with 2578 additions and 2261 deletions

@ -1 +1 @@
Subproject commit bffe32411e8ded537c5615ea054b43b3f7334bcd
Subproject commit fbd5974fba30cab15ef1b7454a5a609286c71508

View File

@ -1 +1 @@
1650014542
1650825040

View File

@ -113,7 +113,7 @@ currency = TESTKUDOS
[merchant-exchange-default]
CURRENCY = TESTKUDOS
EXCHANGE_BASE_URL = http://localhost:8081/
MASTER_KEY = SDXD4H9RYCX8GNPZG73KB21JB9QMSWHK6857SZNMTW0K159WFTRG
MASTER_KEY = C0KWBDJJ5Y0HQ5T0N5BXEDWWV10GM6GSQM8MN7M8B84QDRZDZ8QG
[merchant-account-merchant]
ACTIVE_default = YES
@ -157,7 +157,7 @@ CONFIG = postgres:///auditor-basedb
[exchange]
LOOKAHEAD_SIGN = 32 weeks 1 day
SIGNKEY_DURATION = 4 weeks
MASTER_PUBLIC_KEY = SDXD4H9RYCX8GNPZG73KB21JB9QMSWHK6857SZNMTW0K159WFTRG
MASTER_PUBLIC_KEY = C0KWBDJJ5Y0HQ5T0N5BXEDWWV10GM6GSQM8MN7M8B84QDRZDZ8QG
SIGNKEY_LEGAL_DURATION = 4 weeks
UNIXPATH = ${TALER_RUNTIME_DIR}/exchange.http
@ -175,7 +175,7 @@ DATABASE = postgres:///auditor-basedb
CONFIG = postgres:///auditor-basedb
[auditor]
PUBLIC_KEY = 7Z4C34S3G88KNDSEG9HF7HN6CAH5GNKES4KV8F2JKYV2CBPV4AG0
PUBLIC_KEY = NW60794YCXWFWJFKD7HK3GAQFNBH78T40BMQPTQP1XG9ZETH25SG
TINY_AMOUNT = TESTKUDOS:0.01
BASE_URL = http://localhost:8083/

View File

@ -1 +1 @@
SDXD4H9RYCX8GNPZG73KB21JB9QMSWHK6857SZNMTW0K159WFTRG
C0KWBDJJ5Y0HQ5T0N5BXEDWWV10GM6GSQM8MN7M8B84QDRZDZ8QG

File diff suppressed because it is too large Load Diff

View File

@ -186,7 +186,6 @@ for n in `seq 1 2`
do
echo -n "."
OK=0
# bank
wget --timeout=1 http://localhost:8081/keys -o /dev/null -O /dev/null >/dev/null || continue
OK=1
break
@ -197,6 +196,9 @@ then
exit_skip "Failed to setup keys"
fi
echo " DONE"
echo -n "Adding auditor signatures ..."
taler-auditor-offline -c $CONF \
download sign upload &> taler-auditor-offline.log

View File

@ -1 +1 @@
1650014960
1650825413

View File

@ -1 +1 @@
7VE6NGN2VG8623EW517VSC713RG14S9MCY95AB59S2W4QCEJGR5G
05TD8T49D8A9NXQ126MRBW5KQAR73RDBB4QC3QH14TZ0T9P8W4CG

File diff suppressed because it is too large Load Diff

View File

@ -45,6 +45,11 @@ struct ReservePurseContext
*/
const struct TALER_ReservePublicKeyP *reserve_pub;
/**
* Fees for the operation.
*/
const struct TEH_GlobalFee *gf;
/**
* Signature of the reserve affirming the merge.
*/
@ -194,151 +199,238 @@ purse_transaction (void *cls,
{
struct ReservePurseContext *rpc = cls;
enum GNUNET_DB_QueryStatus qs;
bool in_conflict = true;
/* 1) store purse */
qs = TEH_plugin->insert_purse_request (TEH_plugin->cls,
&rpc->purse_pub,
&rpc->merge_pub,
rpc->purse_expiration,
&rpc->h_contract_terms,
rpc->min_age,
&rpc->amount,
&rpc->purse_sig,
&in_conflict);
if (qs < 0)
{
if (GNUNET_DB_STATUS_SOFT_ERROR == qs)
return qs;
TALER_LOG_WARNING (
"Failed to store purse purse information in database\n");
*mhd_ret =
TALER_MHD_reply_with_error (connection,
MHD_HTTP_INTERNAL_SERVER_ERROR,
TALER_EC_GENERIC_DB_STORE_FAILED,
"insert purse request");
return qs;
}
if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs)
return qs;
if (in_conflict)
{
struct TALER_PurseMergePublicKeyP merge_pub;
struct GNUNET_TIME_Timestamp purse_expiration;
struct TALER_PrivateContractHashP h_contract_terms;
struct TALER_Amount target_amount;
struct TALER_Amount balance;
struct TALER_PurseContractSignatureP purse_sig;
uint32_t min_age;
TEH_plugin->rollback (TEH_plugin->cls);
qs = TEH_plugin->select_purse_request (TEH_plugin->cls,
bool in_conflict = true;
/* 1) store purse */
qs = TEH_plugin->insert_purse_request (TEH_plugin->cls,
&rpc->purse_pub,
&merge_pub,
&purse_expiration,
&h_contract_terms,
&min_age,
&target_amount,
&balance,
&purse_sig);
&rpc->merge_pub,
rpc->purse_expiration,
&rpc->h_contract_terms,
rpc->min_age,
&rpc->amount,
&rpc->purse_sig,
&in_conflict);
if (qs < 0)
{
GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR != qs);
TALER_LOG_WARNING ("Failed to fetch purse information from database\n");
*mhd_ret = TALER_MHD_reply_with_error (connection,
MHD_HTTP_INTERNAL_SERVER_ERROR,
TALER_EC_GENERIC_DB_FETCH_FAILED,
"select purse request");
return GNUNET_DB_STATUS_HARD_ERROR;
}
*mhd_ret
= TALER_MHD_REPLY_JSON_PACK (
connection,
MHD_HTTP_CONFLICT,
TALER_JSON_pack_ec (
TALER_EC_EXCHANGE_PURSE_CREATE_CONFLICTING_META_DATA),
TALER_JSON_pack_amount ("amount",
&target_amount),
GNUNET_JSON_pack_uint64 ("min_age",
min_age),
GNUNET_JSON_pack_timestamp ("purse_expiration",
purse_expiration),
GNUNET_JSON_pack_data_auto ("purse_sig",
&purse_sig),
GNUNET_JSON_pack_data_auto ("h_contract_terms",
&h_contract_terms),
GNUNET_JSON_pack_data_auto ("merge_pub",
&merge_pub));
return GNUNET_DB_STATUS_HARD_ERROR;
}
/* 2) FIXME: merge purse with reserve (and debit reserve for purse creation!) */
/* 3) if present, persist contract */
in_conflict = true;
qs = TEH_plugin->insert_contract (TEH_plugin->cls,
&rpc->purse_pub,
&rpc->contract_pub,
rpc->econtract_size,
rpc->econtract,
&rpc->econtract_sig,
&in_conflict);
if (qs < 0)
{
if (GNUNET_DB_STATUS_SOFT_ERROR == qs)
return qs;
TALER_LOG_WARNING ("Failed to store purse information in database\n");
*mhd_ret = TALER_MHD_reply_with_error (connection,
MHD_HTTP_INTERNAL_SERVER_ERROR,
TALER_EC_GENERIC_DB_STORE_FAILED,
"purse purse contract");
return qs;
}
if (in_conflict)
{
struct TALER_ContractDiffiePublicP pub_ckey;
struct TALER_PurseContractSignatureP econtract_sig;
size_t econtract_size;
void *econtract;
struct GNUNET_HashCode h_econtract;
qs = TEH_plugin->select_contract_by_purse (TEH_plugin->cls,
&rpc->purse_pub,
&pub_ckey,
&econtract_sig,
&econtract_size,
&econtract);
if (qs <= 0)
{
if (GNUNET_DB_STATUS_SOFT_ERROR == qs)
return qs;
TALER_LOG_WARNING (
"Failed to store fetch contract information from database\n");
*mhd_ret = TALER_MHD_reply_with_error (connection,
MHD_HTTP_INTERNAL_SERVER_ERROR,
TALER_EC_GENERIC_DB_FETCH_FAILED,
"select contract");
"Failed to store purse purse information in database\n");
*mhd_ret =
TALER_MHD_reply_with_error (connection,
MHD_HTTP_INTERNAL_SERVER_ERROR,
TALER_EC_GENERIC_DB_STORE_FAILED,
"insert purse request");
return qs;
}
GNUNET_CRYPTO_hash (econtract,
econtract_size,
&h_econtract);
*mhd_ret
= TALER_MHD_REPLY_JSON_PACK (
connection,
MHD_HTTP_CONFLICT,
TALER_JSON_pack_ec (
TALER_EC_EXCHANGE_PURSE_ECONTRACT_CONFLICTING_META_DATA),
GNUNET_JSON_pack_data_auto ("h_econtract",
&h_econtract),
GNUNET_JSON_pack_data_auto ("econtract_sig",
&econtract_sig),
GNUNET_JSON_pack_data_auto ("pub_ckey",
&pub_ckey));
return GNUNET_DB_STATUS_HARD_ERROR;
if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs)
return qs;
if (in_conflict)
{
struct TALER_PurseMergePublicKeyP merge_pub;
struct GNUNET_TIME_Timestamp purse_expiration;
struct TALER_PrivateContractHashP h_contract_terms;
struct TALER_Amount target_amount;
struct TALER_Amount balance;
struct TALER_PurseContractSignatureP purse_sig;
uint32_t min_age;
TEH_plugin->rollback (TEH_plugin->cls);
qs = TEH_plugin->select_purse_request (TEH_plugin->cls,
&rpc->purse_pub,
&merge_pub,
&purse_expiration,
&h_contract_terms,
&min_age,
&target_amount,
&balance,
&purse_sig);
if (qs <= 0)
{
GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR != qs);
TALER_LOG_WARNING ("Failed to fetch purse information from database\n");
*mhd_ret = TALER_MHD_reply_with_error (connection,
MHD_HTTP_INTERNAL_SERVER_ERROR,
TALER_EC_GENERIC_DB_FETCH_FAILED,
"select purse request");
return GNUNET_DB_STATUS_HARD_ERROR;
}
*mhd_ret
= TALER_MHD_REPLY_JSON_PACK (
connection,
MHD_HTTP_CONFLICT,
TALER_JSON_pack_ec (
TALER_EC_EXCHANGE_RESERVES_PURSE_CREATE_CONFLICTING_META_DATA),
TALER_JSON_pack_amount ("amount",
&target_amount),
GNUNET_JSON_pack_uint64 ("min_age",
min_age),
GNUNET_JSON_pack_timestamp ("purse_expiration",
purse_expiration),
GNUNET_JSON_pack_data_auto ("purse_sig",
&purse_sig),
GNUNET_JSON_pack_data_auto ("h_contract_terms",
&h_contract_terms),
GNUNET_JSON_pack_data_auto ("merge_pub",
&merge_pub));
return GNUNET_DB_STATUS_HARD_ERROR;
}
}
/* 2) create purse with reserve (and debit reserve for purse creation!) */
{
bool in_conflict = true;
bool insufficient_funds = true;
qs = TEH_plugin->do_reserve_purse (TEH_plugin->cls,
&rpc->purse_pub,
&rpc->merge_sig,
rpc->merge_timestamp,
&rpc->reserve_sig,
&rpc->gf->fees.purse,
rpc->reserve_pub,
&in_conflict,
&insufficient_funds);
if (qs < 0)
{
if (GNUNET_DB_STATUS_SOFT_ERROR == qs)
return qs;
TALER_LOG_WARNING (
"Failed to store purse merge information in database\n");
*mhd_ret =
TALER_MHD_reply_with_error (connection,
MHD_HTTP_INTERNAL_SERVER_ERROR,
TALER_EC_GENERIC_DB_STORE_FAILED,
"do reserve purse");
return qs;
}
if (in_conflict)
{
/* same purse already merged into a different reserve!? */
struct TALER_PurseContractPublicKeyP purse_pub;
struct TALER_PurseMergeSignatureP merge_sig;
struct GNUNET_TIME_Timestamp merge_timestamp;
char *partner_url;
struct TALER_ReservePublicKeyP reserve_pub;
TEH_plugin->rollback (TEH_plugin->cls);
qs = TEH_plugin->select_purse_merge (
TEH_plugin->cls,
&purse_pub,
&merge_sig,
&merge_timestamp,
&partner_url,
&reserve_pub);
if (qs <= 0)
{
GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR != qs);
TALER_LOG_WARNING (
"Failed to fetch purse merge information from database\n");
*mhd_ret = TALER_MHD_reply_with_error (connection,
MHD_HTTP_INTERNAL_SERVER_ERROR,
TALER_EC_GENERIC_DB_FETCH_FAILED,
"select purse merge");
return GNUNET_DB_STATUS_HARD_ERROR;
}
*mhd_ret
= TALER_MHD_REPLY_JSON_PACK (
connection,
MHD_HTTP_CONFLICT,
TALER_JSON_pack_ec (
TALER_EC_EXCHANGE_RESERVES_PURSE_MERGE_CONFLICTING_META_DATA),
GNUNET_JSON_pack_string ("partner_url",
NULL == partner_url
? TEH_base_url
: partner_url),
GNUNET_JSON_pack_timestamp ("merge_timestamp",
merge_timestamp),
GNUNET_JSON_pack_data_auto ("merge_sig",
&merge_sig),
GNUNET_JSON_pack_data_auto ("reserve_pub",
&reserve_pub));
GNUNET_free (partner_url);
return GNUNET_DB_STATUS_HARD_ERROR;
}
if (insufficient_funds)
{
*mhd_ret
= TALER_MHD_REPLY_JSON_PACK (
connection,
MHD_HTTP_CONFLICT,
TALER_JSON_pack_ec (
TALER_EC_EXCHANGE_RESERVES_PURSE_CREATE_INSUFFICIENT_FUNDS));
return GNUNET_DB_STATUS_HARD_ERROR;
}
}
/* 3) if present, persist contract */
if (NULL != rpc->econtract)
{
bool in_conflict = true;
qs = TEH_plugin->insert_contract (TEH_plugin->cls,
&rpc->purse_pub,
&rpc->contract_pub,
rpc->econtract_size,
rpc->econtract,
&rpc->econtract_sig,
&in_conflict);
if (qs < 0)
{
if (GNUNET_DB_STATUS_SOFT_ERROR == qs)
return qs;
TALER_LOG_WARNING ("Failed to store purse information in database\n");
*mhd_ret = TALER_MHD_reply_with_error (connection,
MHD_HTTP_INTERNAL_SERVER_ERROR,
TALER_EC_GENERIC_DB_STORE_FAILED,
"purse purse contract");
return qs;
}
if (in_conflict)
{
struct TALER_ContractDiffiePublicP pub_ckey;
struct TALER_PurseContractSignatureP econtract_sig;
size_t econtract_size;
void *econtract;
struct GNUNET_HashCode h_econtract;
qs = TEH_plugin->select_contract_by_purse (TEH_plugin->cls,
&rpc->purse_pub,
&pub_ckey,
&econtract_sig,
&econtract_size,
&econtract);
if (qs <= 0)
{
if (GNUNET_DB_STATUS_SOFT_ERROR == qs)
return qs;
TALER_LOG_WARNING (
"Failed to store fetch contract information from database\n");
*mhd_ret = TALER_MHD_reply_with_error (connection,
MHD_HTTP_INTERNAL_SERVER_ERROR,
TALER_EC_GENERIC_DB_FETCH_FAILED,
"select contract");
return qs;
}
GNUNET_CRYPTO_hash (econtract,
econtract_size,
&h_econtract);
*mhd_ret
= TALER_MHD_REPLY_JSON_PACK (
connection,
MHD_HTTP_CONFLICT,
TALER_JSON_pack_ec (
TALER_EC_EXCHANGE_PURSE_ECONTRACT_CONFLICTING_META_DATA),
GNUNET_JSON_pack_data_auto ("h_econtract",
&h_econtract),
GNUNET_JSON_pack_data_auto ("econtract_sig",
&econtract_sig),
GNUNET_JSON_pack_data_auto ("pub_ckey",
&pub_ckey));
return GNUNET_DB_STATUS_HARD_ERROR;
}
}
return qs;
}
@ -391,7 +483,6 @@ TEH_handler_reserves_purse (
&rpc.purse_expiration),
GNUNET_JSON_spec_end ()
};
const struct TEH_GlobalFee *gf;
{
enum GNUNET_GenericReturnValue res;
@ -433,9 +524,9 @@ TEH_handler_reserves_purse (
TALER_EC_EXCHANGE_RESERVES_PURSE_EXPIRATION_IS_NEVER,
NULL);
}
gf = TEH_keys_global_fee_by_time (TEH_keys_get_state (),
rpc.exchange_timestamp);
if (NULL == gf)
rpc.gf = TEH_keys_global_fee_by_time (TEH_keys_get_state (),
rpc.exchange_timestamp);
if (NULL == rpc.gf)
{
GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
"Cannot purse purse: global fees not configured!\n");

View File

@ -2710,6 +2710,27 @@ END $$;
-- IS 'Checks that the partner exists, the purse has not been merged with a different reserve and that the purse is full. If so, persists the merge data. Caller MUST abort the transaction on failures so as to not persist data by accident.';
CREATE OR REPLACE FUNCTION exchange_do_reserve_purse(
IN in_purse_pub BYTEA,
IN in_merge_sig BYTEA,
IN in_merge_timestamp INT8,
IN in_reserve_sig BYTEA,
IN in_purse_fee_val INT8,
IN in_purse_fee_frac INT8,
IN in_reserve_pub BYTEA,
OUT out_no_funds BOOLEAN,
OUT out_conflict BOOLEAN)
LANGUAGE plpgsql
AS $$
BEGIN
-- FIXME: implement!
out_conflict=TRUE;
out_no_funds=TRUE;
END $$;
CREATE OR REPLACE FUNCTION exchange_do_account_merge(
IN in_purse_pub BYTEA,
IN in_reserve_pub BYTEA,

View File

@ -3525,6 +3525,15 @@ prepare_statements (struct PostgresClosure *pg)
" FROM exchange_do_purse_merge"
" ($1, $2, $3, $4, $5, $6);",
6),
/* Used in #postgres_do_reserve_purse() */
GNUNET_PQ_make_prepare (
"call_reserve_purse",
"SELECT"
" out_no_funds AS insufficient_funds"
",out_conflict AS conflict"
" FROM exchange_do_reserve_purse"
" ($1, $2, $3, $4, $5, $6, $7);",
7),
/* Used in #postgres_select_purse_merge */
GNUNET_PQ_make_prepare (
"select_purse_merge",
@ -13561,6 +13570,59 @@ postgres_do_purse_merge (
}
/**
* Function called insert request to merge a purse into a reserve by the
* respective purse merge key. The purse must not have been merged into a
* different reserve.
*
* @param cls the @e cls of this struct with the plugin-specific state
* @param purse_pub purse to merge
* @param merge_sig signature affirming the merge
* @param merge_timestamp time of the merge
* @param reserve_sig signature of the reserve affirming the merge
* @param purse_fee amount to charge the reserve for the purse creation
* @param reserve_pub public key of the reserve to credit
* @param[out] in_conflict set to true if @a purse_pub was merged into a different reserve already
* @param[out] insufficient_funds set to true if @a reserve_pub has insufficient capacity to create another purse
* @return transaction status code
*/
static enum GNUNET_DB_QueryStatus
postgres_do_reserve_purse (
void *cls,
const struct TALER_PurseContractPublicKeyP *purse_pub,
const struct TALER_PurseMergeSignatureP *merge_sig,
const struct GNUNET_TIME_Timestamp merge_timestamp,
const struct TALER_ReserveSignatureP *reserve_sig,
const struct TALER_Amount *purse_fee,
const struct TALER_ReservePublicKeyP *reserve_pub,
bool *in_conflict,
bool *insufficient_funds)
{
struct PostgresClosure *pg = cls;
struct GNUNET_PQ_QueryParam params[] = {
GNUNET_PQ_query_param_auto_from_type (purse_pub),
GNUNET_PQ_query_param_auto_from_type (merge_sig),
GNUNET_PQ_query_param_timestamp (&merge_timestamp),
GNUNET_PQ_query_param_auto_from_type (reserve_sig),
TALER_PQ_query_param_amount (purse_fee),
GNUNET_PQ_query_param_auto_from_type (reserve_pub),
GNUNET_PQ_query_param_end
};
struct GNUNET_PQ_ResultSpec rs[] = {
GNUNET_PQ_result_spec_bool ("insufficient_funds",
insufficient_funds),
GNUNET_PQ_result_spec_bool ("conflict",
in_conflict),
GNUNET_PQ_result_spec_end
};
return GNUNET_PQ_eval_prepared_singleton_select (pg->conn,
"call_reserve_purse",
params,
rs);
}
/**
* Function called to approve merging of a purse with
* an account, made by the receiving account.
@ -13967,6 +14029,8 @@ libtaler_plugin_exchangedb_postgres_init (void *cls)
= &postgres_get_purse_deposit;
plugin->do_purse_merge
= &postgres_do_purse_merge;
plugin->do_reserve_purse
= &postgres_do_reserve_purse;
plugin->select_purse_merge
= &postgres_select_purse_merge;
plugin->do_account_merge

View File

@ -4689,6 +4689,35 @@ struct TALER_EXCHANGEDB_Plugin
bool *in_conflict);
/**
* Function called insert request to merge a purse into a reserve by the
* respective purse merge key. The purse must not have been merged into a
* different reserve.
*
* @param cls the @e cls of this struct with the plugin-specific state
* @param purse_pub purse to merge
* @param merge_sig signature affirming the merge
* @param merge_timestamp time of the merge
* @param reserve_sig signature of the reserve affirming the merge
* @param purse_fee amount to charge the reserve for the purse creation
* @param reserve_pub public key of the reserve to credit
* @param[out] in_conflict set to true if @a purse_pub was merged into a different reserve already
* @param[out] insufficient_funds set to true if @a reserve_pub has insufficient capacity to create another purse
* @return transaction status code
*/
enum GNUNET_DB_QueryStatus
(*do_reserve_purse)(
void *cls,
const struct TALER_PurseContractPublicKeyP *purse_pub,
const struct TALER_PurseMergeSignatureP *merge_sig,
const struct GNUNET_TIME_Timestamp merge_timestamp,
const struct TALER_ReserveSignatureP *reserve_sig,
const struct TALER_Amount *purse_fee,
const struct TALER_ReservePublicKeyP *reserve_pub,
bool *in_conflict,
bool *insufficient_funds);
/**
* Function called to approve merging of a purse with
* an account, made by the receiving account.

View File

@ -521,13 +521,13 @@ parse_global_fee (struct TALER_EXCHANGE_GlobalFee *gf,
const struct TALER_EXCHANGE_Keys *key_data)
{
struct GNUNET_JSON_Specification spec[] = {
GNUNET_JSON_spec_timestamp ("start_time",
GNUNET_JSON_spec_timestamp ("start_date",
&gf->start_date),
GNUNET_JSON_spec_timestamp ("end_time",
GNUNET_JSON_spec_timestamp ("end_date",
&gf->end_date),
GNUNET_JSON_spec_relative_time ("purse_timeout",
&gf->purse_timeout),
GNUNET_JSON_spec_relative_time ("kyc_timeout",
GNUNET_JSON_spec_relative_time ("account_kyc_timeout",
&gf->kyc_timeout),
GNUNET_JSON_spec_relative_time ("history_expiration",
&gf->history_expiration),

View File

@ -294,7 +294,6 @@ TALER_EXCHANGE_purse_create_with_merge (
&contract_pub.ecdhe_pub);
{
// FIXME: get purse expiration time from contract?
struct GNUNET_JSON_Specification spec[] = {
TALER_JSON_spec_amount_any ("amount",
&pcm->purse_value_after_fees),
@ -302,8 +301,7 @@ TALER_EXCHANGE_purse_create_with_merge (
GNUNET_JSON_spec_uint32 ("minimum_age",
&min_age),
NULL),
// FIXME: correct field name?
GNUNET_JSON_spec_timestamp ("payment_deadline",
GNUNET_JSON_spec_timestamp ("pay_deadline",
&pcm->purse_expiration),
GNUNET_JSON_spec_end ()
};