commit 6f62ce0a0602aa1ea0757638e096f10265a5fd56
parent 8cb95d87e29ba63d66314a9f933a75ff7c2bedb2
Author: Christian Grothoff <christian@grothoff.org>
Date: Wed, 22 Apr 2026 22:59:58 +0200
debug paivana
Diffstat:
9 files changed, 368 insertions(+), 142 deletions(-)
diff --git a/contrib/paywall.en.must b/contrib/paywall.en.must
@@ -2,6 +2,7 @@
<html lang="en">
<head>
<title>Payment Required</title>
+ <meta http-equiv="content-type" content="text/html;CHARSET=utf-8">
<!-- FIXME: probably should serve this ourselves... -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/qrcodejs/1.0.0/qrcode.min.js"
integrity="sha512-CNgIRecGo7nphbeZ04Sc13ka07paqdeTu0WR1IM4kNcpmBAUSHSQX0FslNhTDadL4O5SAGapGt4FodqL8My0mA=="
@@ -13,8 +14,7 @@
<a href="https://wallet.taler.net">GNU Taler</a> wallet.</p>
<div id="qrcode"></div>
- <p><a id="talerlink" href="#">Generating payment link…</a></p>
- <p id="statusmsg">Waiting for payment…</p>
+ <p><a id="talerlink" href="#">Generating payment link...</a></p>
<script>
(async () => {
@@ -23,35 +23,31 @@
return new Promise(resolve => setTimeout(resolve, ms));
}
- const CROCKFORD = '0123456789ABCDEFGHJKMNPQRSTVWXYZ';
-
- function crockford32(bytes) {
- // Encode a Uint8Array as Crockford base32 (no padding).
- let result = '', bits = 0, val = 0;
- for (const byte of bytes) {
- val = (val << 8) | byte;
- bits += 8;
- while (bits >= 5) {
- bits -= 5;
- result += CROCKFORD[(val >> bits) & 0x1f];
- }
- }
- if (bits > 0) result += CROCKFORD[(val << (5 - bits)) & 0x1f];
- return result;
+ // Note: good like this ONLY for short inputs...
+ function toBase64Url(bytes) {
+ let binary = String.fromCharCode(...bytes);
+ return btoa(binary)
+ .replace(/\+/g, '-')
+ .replace(/\//g, '_')
+ .replace(/=+$/, ''); // remove padding
}
- async function sha512b32(str) {
+ async function sha256b64(str) {
const buf = await crypto.subtle.digest(
- 'SHA-512', new TextEncoder().encode(str));
- return crockford32(new Uint8Array(buf));
+ 'SHA-256', new TextEncoder().encode(str));
+ return toBase64Url(new Uint8Array(buf));
}
async function makePaivanaId(curTime, nonce, website) {
- const hash = await sha512b32(nonce + website + String(curTime));
+ const hash = await sha256b64(nonce + website + String(curTime));
return `${curTime}-${hash}`;
}
- const website = `${window.location.protocol}//${window.location.host}`;
+ const href = window.location.href;
+ // This is a policy decision: we include the query parameters
+ // in the website to support sites that need them, say if
+ // "?page=42" is being used instead of "/pages/42".
+ const website = href.split('#')[0];
const metaMerchant = '{{ merchant_backend }}';
const metaTemplate = '{{ template_id }}';
const maxPickupDelay = {{ max_pickup_delay }}; // in seconds
@@ -59,9 +55,12 @@
const usePickupDelay = Math.min(maxPickupDelay, 60*60*24*356*100);
// Strip trailing slash so we can append /templateId cleanly.
- const merchantBase = metaMerchant.content.replace(/\/$/, '');
- const merchantProto = new URL(merchantBase).protocol;
+ const merchantBase = metaMerchant.replace(/\/$/, '');
+ const merchantUrl = new URL(merchantBase);
+ const merchantProto = merchantUrl.protocol;
const suffix = merchantProto === 'http:' ? '+http' : '';
+ const merchantHost = merchantUrl.host;
+ const merchantPath = merchantUrl.path;
const expTime = Math.floor(Date.now() / 1000) + maxPickupDelay;
const nonceArr = new Uint8Array(32);
@@ -73,10 +72,12 @@
const talerUri = [
`taler${suffix}://pay-template/`,
- merchantBase,
+ merchantHost,
+ merchantPath,
+ `/`,
metaTemplate,
`?session_id=${encodeURIComponent(paivanaId)}`,
- `&fulfillment_url=${encodeURIComponent(website)}`
+ `&fulfillment_url=${encodeURIComponent(href)}`
].join('');
const talerLink = document.getElementById('talerlink');
@@ -90,10 +91,9 @@
correctLevel: QRCode.CorrectLevel.M
});
- const statusMsg = document.getElementById('statusmsg');
-
async function confirmPayment(orderId) {
- statusMsg.textContent = 'Payment confirmed! Activating access…';
+ talerLink.textContent = 'Payment confirmed! Loading page...';
+ talerLink.href = '#';
try {
const res = await fetch(`${website}/.well-known/paivana`, {
method: 'POST',
@@ -106,7 +106,8 @@
location.href = dest;
} catch (e) {
console.warn('[paivana] confirm error:', e);
- statusMsg.textContent = 'Could not reach the server — please reload.';
+ talerLink.href = '#';
+ talerLink.textContent = 'Could not reach the server!';
}
}
@@ -122,7 +123,6 @@
if (orderId) {
await confirmPayment(orderId);
} else {
- statusMsg.textContent = 'Invalid response! Reloading anyway…';
// FIXME: for testing...
await waitMs(400);
location.href = website;
@@ -131,7 +131,8 @@
}
} catch (e) {
console.warn('[paivana] poll error:', e);
- statusMsg.textContent = 'Network error — retrying…';
+ talerLink.href = '#';
+ talerLink.textContent = 'Network error! Retrying...';
}
const remMs = Math.round(30_000 - (performance.now() - start));
if (remMs > 0) await waitMs(remMs);
diff --git a/src/backend/Makefile.am b/src/backend/Makefile.am
@@ -28,6 +28,7 @@ paivana_httpd_LDADD = \
-lgnunetcurl \
-lgnunetutil \
-lmicrohttpd \
+ -lgcrypt \
-lcurl \
-ljansson \
-lz
diff --git a/src/backend/paivana-httpd.c b/src/backend/paivana-httpd.c
@@ -255,7 +255,7 @@ run (void *cls,
return;
}
GNUNET_asprintf (&auth_header,
- "%s: %s",
+ "%s: Bearer %s",
MHD_HTTP_HEADER_AUTHORIZATION,
merchant_access_token);
GNUNET_free (merchant_access_token);
diff --git a/src/backend/paivana-httpd_cookie.c b/src/backend/paivana-httpd_cookie.c
@@ -25,6 +25,7 @@
*/
#include "platform.h"
#include <curl/curl.h>
+#include <gcrypt.h>
#include <gnunet/gnunet_util_lib.h>
#include <taler/taler_mhd_lib.h>
#include "paivana-httpd_cookie.h"
@@ -147,34 +148,38 @@ PAIVANA_HTTPD_compute_paivana_id (struct GNUNET_TIME_Timestamp cur_time,
const struct PAIVANA_Nonce *nonce)
{
struct GNUNET_TIME_AbsoluteNBO e;
- struct GNUNET_HashContext *hc;
- struct GNUNET_HashCode h;
- char *end;
- char cstr[128];
char *res;
+ gcry_md_hd_t hd;
+ const void *sha256;
+ char *cstr;
+ size_t clen;
e = GNUNET_TIME_absolute_hton (cur_time.abs_time);
- hc = GNUNET_CRYPTO_hash_context_start ();
- GNUNET_CRYPTO_hash_context_read (hc,
- nonce,
- sizeof (*nonce));
- GNUNET_CRYPTO_hash_context_read (hc,
- website,
- strlen (website) + 1);
- GNUNET_CRYPTO_hash_context_read (hc,
- &e,
- sizeof (e));
- GNUNET_CRYPTO_hash_context_finish (hc,
- &h);
- end = GNUNET_STRINGS_data_to_string (&h,
- sizeof (h),
- cstr,
- sizeof (cstr));
- *end = '\0';
+ GNUNET_assert (0 ==
+ gcry_md_open (&hd,
+ GCRY_MD_SHA256,
+ 0));
+ gcry_md_write (hd,
+ nonce,
+ sizeof (*nonce));
+ gcry_md_write (hd,
+ website,
+ strlen (website) + 1);
+ gcry_md_write (hd,
+ &e,
+ sizeof (e));
+ sha256 = gcry_md_read (hd,
+ 0);
+ cstr = NULL;
+ clen = GNUNET_STRINGS_base64url_encode (sha256,
+ 256 / 8,
+ &cstr);
GNUNET_asprintf (
&res,
- "%llu-%s",
+ "%llu-%.*s",
(unsigned long long) (cur_time.abs_time.abs_value_us / 1000LLU / 1000LLU),
+ (int) clen,
cstr);
+ GNUNET_free (cstr);
return res;
}
diff --git a/src/backend/paivana-httpd_daemon.c b/src/backend/paivana-httpd_daemon.c
@@ -114,8 +114,14 @@ create_response (void *cls,
{
struct RequestContext *rc = *con_cls;
const char *cookie;
+ bool ok = false;
+ struct GNUNET_Buffer buf;
+ char *website;
(void) cls;
+ memset (&buf,
+ 0,
+ sizeof (buf));
if ( (! rc->is_paivana) &&
(0 == strcmp (url,
".well-known/paivana")) &&
@@ -149,6 +155,20 @@ create_response (void *cls,
upload_data_size);
}
+ if (! PAIVANA_HTTPD_get_base_url (con,
+ &buf))
+ {
+ GNUNET_break (0);
+ return TALER_MHD_reply_with_error (
+ con,
+ MHD_HTTP_BAD_REQUEST,
+ TALER_EC_GENERIC_HTTP_HEADERS_MALFORMED,
+ "Host or X-Forwarded-Host required");
+ }
+ GNUNET_buffer_write_str (&buf,
+ url);
+ website = GNUNET_buffer_reap_str (&buf);
+
cookie = MHD_lookup_connection_value (con,
MHD_COOKIE_KIND,
"Paivana-Cookie");
@@ -156,50 +176,34 @@ create_response (void *cls,
{
void *ca;
size_t ca_len;
- struct GNUNET_Buffer buf;
- char *website;
- bool ok;
GNUNET_break (PAIVANA_HTTPD_get_client_address (con,
&ca,
&ca_len));
- if (! PAIVANA_HTTPD_get_base_url (con,
- &buf))
- {
- GNUNET_break (0);
- return TALER_MHD_reply_with_error (
- con,
- MHD_HTTP_BAD_REQUEST,
- TALER_EC_GENERIC_HTTP_HEADERS_MALFORMED,
- "Host or X-Forwarded-Host required");
- }
- GNUNET_buffer_write_str (&buf,
- url);
- website = GNUNET_buffer_reap_str (&buf);
ok = PAIVANA_HTTPD_check_cookie (cookie,
website,
ca_len,
ca);
GNUNET_free (ca);
- if (! ok)
+ }
+ if (! ok)
+ {
+ enum GNUNET_GenericReturnValue ret;
+
+ ret = PAIVANA_HTTPD_search_templates (con,
+ website);
+ GNUNET_free (website);
+ if (GNUNET_SYSERR != ret)
{
- struct MHD_Response *paywall;
-
- paywall = PAIVANA_HTTPD_search_templates (website);
- GNUNET_free (website);
- if (NULL != paywall)
- {
- GNUNET_log (GNUNET_ERROR_TYPE_INFO,
- "Request denied\n");
- return MHD_queue_response (con,
- MHD_HTTP_PAYMENT_REQUIRED,
- paywall);
- }
+ GNUNET_log (GNUNET_ERROR_TYPE_INFO,
+ "Payment required, sending paywall page %s\n",
+ (GNUNET_OK == ret) ? "ok" : "failed");
+ return (GNUNET_OK == ret) ? MHD_YES : MHD_NO;
}
- GNUNET_free (website);
}
+ GNUNET_free (website);
GNUNET_log (GNUNET_ERROR_TYPE_INFO,
- "Request ok!\n");
+ "Request OK, no paywall applies!\n");
rc->do_forward = true;
/* FIXME: code for 100 continue suppression should go here! */
return MHD_YES;
diff --git a/src/backend/paivana-httpd_templates.c b/src/backend/paivana-httpd_templates.c
@@ -41,6 +41,46 @@ struct Template;
#include <taler/merchant/get-private-templates-TEMPLATE_ID.h>
#include <taler/merchant/get-private-templates.h>
+
+/**
+ * Entry in the cache of responses for a given template.
+ */
+struct ResponseCacheEntry
+{
+
+ /**
+ * Kept in a DLL.
+ */
+ struct ResponseCacheEntry *next;
+
+ /**
+ * Kept in a DLL.
+ */
+ struct ResponseCacheEntry *prev;
+
+ /**
+ * Language of the response.
+ */
+ char *lang;
+
+ /**
+ * Accept-Encoding of the response.
+ */
+ char *ae;
+
+ /**
+ * Paywall response for these request parameters.
+ */
+ struct MHD_Response *paywall;
+
+ /**
+ * HTTP status to return with @e paywall.
+ */
+ unsigned int http_status;
+
+};
+
+
/**
* Information about a template in the merchant backend.
*/
@@ -83,9 +123,14 @@ struct Template
struct TALER_MERCHANT_GetPrivateTemplateHandle *gt;
/**
- * Paywall response for this template. NULL for no paywall.
+ * Kept in a DLL.
*/
- struct MHD_Response *paywall;
+ struct ResponseCacheEntry *rce_head;
+
+ /**
+ * Kept in a DLL.
+ */
+ struct ResponseCacheEntry *rce_tail;
};
@@ -100,7 +145,6 @@ static struct Template *t_head;
*/
static struct Template *t_tail;
-
/**
* Handle to get all the templates.
*/
@@ -108,62 +152,113 @@ static struct TALER_MERCHANT_GetPrivateTemplatesHandle *gpt;
/**
+ * Check if two strings are equal, including both being NULL
+ *
+ * @param s1 a string, possibly NULL
+ * @param s2 a string. possibly NULL
+ * @return true if both are equal
+ */
+static bool
+eq (const char *s1,
+ const char *s2)
+{
+ if (s1 == s2)
+ return true;
+ if (NULL == s1)
+ return false;
+ if (NULL == s2)
+ return false;
+ return (0 == strcmp (s1,
+ s2));
+}
+
+
+/**
* Try to initialize the paywall response.
*
- * @param template_id template to use for the response
- * @param max_pickup_delay how long does the user have to access the site
- * (relative expiration time of the paivana cookie they pay for)
- * @return HTTP response to return for matching websites
+ * @param conn connection to create the response for
+ * @param t template template to create the response for
+ * @return MHD status code to return
*/
-static struct MHD_Response *
-load_paywall (const char *template_id,
- struct GNUNET_TIME_Relative max_pickup_delay)
+static enum MHD_Result
+load_paywall (struct MHD_Connection *conn,
+ struct Template *t)
{
- void *result;
- size_t result_size;
+ struct MHD_Response *reply;
+ const char *lang;
+ const char *ae;
+ unsigned int http_status = MHD_HTTP_PAYMENT_REQUIRED;
+
+ lang = MHD_lookup_connection_value (conn,
+ MHD_HEADER_KIND,
+ MHD_HTTP_HEADER_ACCEPT_LANGUAGE);
+ ae = MHD_lookup_connection_value (conn,
+ MHD_HEADER_KIND,
+ MHD_HTTP_HEADER_ACCEPT_ENCODING);
+ for (struct ResponseCacheEntry *pos = t->rce_head;
+ NULL != pos;
+ pos = pos->next)
+ {
+ if ( (eq (lang,
+ pos->lang)) &&
+ (eq (ae,
+ pos->ae) ) )
+ return MHD_queue_response (conn,
+ pos->http_status,
+ pos->paywall);
+ }
{
+ enum GNUNET_GenericReturnValue ret;
json_t *data;
data = GNUNET_JSON_PACK (
GNUNET_JSON_pack_string (
"template_id",
- template_id),
+ t->template_id),
GNUNET_JSON_pack_uint64 (
"max_pickup_delay",
- max_pickup_delay.rel_value_us / 1000LLU / 1000LLU),
+ t->max_pickup_delay.rel_value_us / 1000LLU / 1000LLU),
GNUNET_JSON_pack_string (
"merchant_backend",
PH_merchant_base_url));
- if (0 !=
- TALER_TEMPLATING_fill ("paywall",
- data,
- &result,
- &result_size))
+ ret = TALER_TEMPLATING_build (conn,
+ &http_status,
+ "paywall",
+ NULL /* no instance */,
+ NULL, /* FIXME: no Taler URI!? */
+ data,
+ &reply);
+ if (GNUNET_OK != ret)
{
GNUNET_break (0);
json_decref (data);
- return NULL;
+ return (GNUNET_NO == ret) ? MHD_YES : MHD_NO;
}
json_decref (data);
}
+ GNUNET_break (MHD_YES ==
+ MHD_add_response_header (reply,
+ MHD_HTTP_HEADER_CONTENT_TYPE,
+ "text/html"));
+ // FIXME: set Vary and other cache control headers!
{
- struct MHD_Response *paywall;
-
- paywall = MHD_create_response_from_buffer_copy (result_size,
- result);
- if (NULL == paywall)
- {
- GNUNET_free (result);
- return NULL;
- }
- GNUNET_free (result);
- GNUNET_break (MHD_YES ==
- MHD_add_response_header (paywall,
- MHD_HTTP_HEADER_CONTENT_TYPE,
- "text/html"));
- return paywall;
+ struct ResponseCacheEntry *rce;
+
+ rce = GNUNET_new (struct ResponseCacheEntry);
+ if (NULL != lang)
+ rce->lang = GNUNET_strdup (lang);
+ if (NULL != ae)
+ rce->ae = GNUNET_strdup (ae);
+ rce->paywall = reply;
+ rce->http_status = http_status;
+ GNUNET_CONTAINER_DLL_insert (t->rce_head,
+ t->rce_tail,
+ rce);
+ return MHD_queue_response (conn,
+ rce->http_status,
+ reply);
}
}
@@ -213,6 +308,10 @@ parse_template (struct Template *t,
return;
}
t->regex = GNUNET_strdup (regex);
+ GNUNET_log (GNUNET_ERROR_TYPE_INFO,
+ "Using payment template %s for `%s'\n",
+ t->template_id,
+ regex);
}
@@ -238,11 +337,6 @@ setup_template (
GNUNET_break (0);
break;
}
- if (t->regex)
- {
- t->paywall = load_paywall (t->template_id,
- t->max_pickup_delay);
- }
for (struct Template *p = t_head; NULL != p; p = p->next)
if (NULL != p->gt)
return;
@@ -274,8 +368,18 @@ check_templates (
"No templates found, starting to serve requests\n");
PAIVANA_HTTPD_serve_requests ();
return;
+ case MHD_HTTP_UNAUTHORIZED:
+ GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
+ "Access to templates unauthorized: %s\n",
+ TALER_ErrorCode_get_hint (tgr->hr.ec));
+ PH_global_ret = EXIT_FAILURE;
+ GNUNET_SCHEDULER_shutdown ();
+ return;
default:
- GNUNET_break (0);
+ GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
+ "Unexpected HTTP status code %u on GET /private/templates (%d)\n",
+ tgr->hr.http_status,
+ (int) tgr->hr.ec);
PH_global_ret = EXIT_FAILURE;
GNUNET_SCHEDULER_shutdown ();
return;
@@ -325,9 +429,13 @@ PAIVANA_HTTPD_load_templates ()
}
-struct MHD_Response *
-PAIVANA_HTTPD_search_templates (const char *website)
+enum GNUNET_GenericReturnValue
+PAIVANA_HTTPD_search_templates (struct MHD_Connection *connection,
+ const char *website)
{
+ GNUNET_log (GNUNET_ERROR_TYPE_INFO,
+ "Searching templates for `%s'\n",
+ website);
for (struct Template *t = t_head; NULL != t; t = t->next)
{
if (NULL == t->regex)
@@ -336,9 +444,19 @@ PAIVANA_HTTPD_search_templates (const char *website)
website,
0, NULL,
0))
- return t->paywall;
+ {
+ enum MHD_Result ret;
+
+ ret = load_paywall (connection,
+ t);
+ return (MHD_YES == ret) ? GNUNET_OK : GNUNET_NO;
+ }
+ GNUNET_log (GNUNET_ERROR_TYPE_INFO,
+ "Request for %s did not match template %s\n",
+ website,
+ t->template_id);
}
- return NULL;
+ return GNUNET_SYSERR;
}
@@ -352,13 +470,23 @@ PAIVANA_HTTPD_unload_templates ()
{
struct Template *t = t_head;
+ while (NULL != t->rce_head)
+ {
+ struct ResponseCacheEntry *rce = t->rce_head;
+
+ GNUNET_CONTAINER_DLL_remove (t->rce_head,
+ t->rce_tail,
+ rce);
+ MHD_destroy_response (rce->paywall);
+ GNUNET_free (rce->ae);
+ GNUNET_free (rce->lang);
+ GNUNET_free (rce);
+ }
GNUNET_CONTAINER_DLL_remove (t_head,
t_tail,
t);
if (NULL != t->gt)
TALER_MERCHANT_get_private_template_cancel (t->gt);
- if (NULL != t->paywall)
- MHD_destroy_response (t->paywall);
if (NULL != t->regex)
{
regfree (&t->ex);
diff --git a/src/backend/paivana-httpd_templates.h b/src/backend/paivana-httpd_templates.h
@@ -42,12 +42,15 @@ PAIVANA_HTTPD_load_templates (void);
/**
* Return the paywall page for the given @a website.
*
+ * @param connection request to search paywall response for
* @param website site to look for paywall templates for
- * @return NULL if the @a url is not protected by the paywall,
- * otherwise paywall response to return (do NOT free!)
+ * @return #GNUNET_OK on paywall returned,
+ * #GNUNET_NO to close the connection with error
+ * #GNUNET_SYSERR if there is no paywall for @a website
*/
-struct MHD_Response *
-PAIVANA_HTTPD_search_templates (const char *website);
+enum GNUNET_GenericReturnValue
+PAIVANA_HTTPD_search_templates (struct MHD_Connection *connection,
+ const char *website);
/**
diff --git a/src/backend/test.conf b/src/backend/test.conf
@@ -1,9 +1,25 @@
[paivana]
DESTINATION_BASE_URL = https://grothoff.org/
SERVE = tcp
-HTTP_PORT = 8888
+PORT = 8888
-MERCHANT_BACKEND_URL = https://backend.demo.taler.net/instances/sandbox/
-MERCHANT_ACCESS_TOKEN = secret-token:sandbox
+BASE_URL = http://localhost:80/
+MERCHANT_BACKEND_URL = http://localhost:9966/
+MERCHANT_ACCESS_TOKEN = "secret-token:sandbox"
-SECRET = 42
-\ No newline at end of file
+SECRET = 42
+
+[merchant]
+CURRENCY = KUDOS
+BASE_URL = http://localhost:8080/
+PORT = 9966
+SERVE = TCP
+DEFAULT_REFUND_DELAY = 1s
+DEFAULT_WIRE_TRANSFER_ROUNDING_INTERVAL = day
+
+
+[merchantdb-postgres]
+CONFIG = postgres:///talercheck
+
+[merchant-exchange-chf]
+DISABLED = YES
diff --git a/src/backend/test.sh b/src/backend/test.sh
@@ -0,0 +1,69 @@
+#!/bin/bash
+# Minimal script for testing, creates a first template
+# that would work in combination with 'test.conf'.
+# Run after starting taler-merchant-httpd and
+# *before* starting paivana-httpd.
+
+# Exit, with status code "skip" (no 'real' failure)
+function exit_fail() {
+ echo "$@" >&2
+ exit 1
+}
+
+LAST_RESPONSE=$(mktemp /tmp/last-response-XXXXXX.json)
+
+echo -n "Configuring merchant instance ..."
+
+STATUS=$(curl -H "Content-Type: application/json" -X POST \
+ -H 'Authorization: Bearer secret-token:sandbox' \
+ http://localhost:9966/management/instances \
+ -d '{"auth":{"method":"external"},"id":"admin","name":"default","user_type":"business","address":{},"jurisdiction":{},"use_stefan":true,"default_wire_transfer_delay":{"d_us" : 50000000000},"default_pay_delay":{"d_us": 60000000000}}' \
+ -w "%{http_code}" \
+ -s \
+ -o "$LAST_RESPONSE")
+
+if [ "$STATUS" != "204" ]
+then
+ jq < "$LAST_RESPONSE"
+ exit_fail "Expected '204 No content' response. Got instead $STATUS"
+fi
+echo "OK"
+
+echo -n "Configuring merchant bank account ..."
+
+PAYTO="payto://x-taler-bank/bank.demo.taler.net/fortythree?receiver-name=43"
+# add bank account address
+STATUS=$(curl -H "Content-Type: application/json" \
+ -X POST \
+ -H 'Authorization: Bearer secret-token:sandbox' \
+ http://localhost:9966/private/accounts \
+ -d '{"payto_uri":"'"$PAYTO"'"}' \
+ -w "%{http_code}" \
+ -s \
+ -o "$LAST_RESPONSE")
+
+if [ "$STATUS" != "200" ]
+then
+ jq < "$LAST_RESPONSE"
+ exit_fail "Expected '200 OK' response. Got instead $STATUS"
+fi
+echo "Ok"
+
+echo -n "Creating Paivana template..."
+TID="paivana"
+STATUS=$(curl -H "Content-Type: application/json" \
+ -X POST \
+ 'http://localhost:9966/private/templates' \
+ -H 'Authorization: Bearer secret-token:sandbox' \
+ -d '{"template_id":"paivana","template_description":"A Paivana template","template_contract":{"template_type":"paivana","summary":"The summary","website_regex":".*","choices":[{"amount":"KUDOS:1"}]}}' \
+ -w "%{http_code}" \
+ -s \
+ -o "$LAST_RESPONSE")
+
+if [ "$STATUS" != "204" ]
+then
+ jq < "$LAST_RESPONSE"
+ exit_fail "Expected 204, template created. got: $STATUS"
+fi
+
+echo "OK"