wallet: towards db-less benchmarking, some refactoring

This commit is contained in:
Florian Dold 2022-03-14 18:31:30 +01:00
parent 9e7ee06ad1
commit 332745862e
No known key found for this signature in database
GPG Key ID: D2E4F00F29D02A4B
34 changed files with 1396 additions and 362 deletions

View File

@ -785,6 +785,22 @@ export function setupRefreshTransferPub(
}; };
} }
/**
*
* @param paytoUri
* @param salt 16-byte salt
* @returns
*/
export function hashWire(paytoUri: string, salt: string): string {
const r = kdf(
64,
stringToBytes(paytoUri + "\0"),
decodeCrock(salt),
stringToBytes("merchant-wire-signature"),
);
return encodeCrock(r);
}
export enum TalerSignaturePurpose { export enum TalerSignaturePurpose {
MERCHANT_TRACK_TRANSACTION = 1103, MERCHANT_TRACK_TRANSACTION = 1103,
WALLET_RESERVE_WITHDRAW = 1200, WALLET_RESERVE_WITHDRAW = 1200,

View File

@ -951,6 +951,15 @@ export interface MerchantPayResponse {
sig: string; sig: string;
} }
export interface ExchangeMeltRequest {
coin_pub: CoinPublicKeyString;
confirm_sig: EddsaSignatureString;
denom_pub_hash: HashCodeString;
denom_sig: UnblindedSignature;
rc: string;
value_with_fee: AmountString;
}
export interface ExchangeMeltResponse { export interface ExchangeMeltResponse {
/** /**
* Which of the kappa indices does the client not have to reveal. * Which of the kappa indices does the client not have to reveal.
@ -1710,3 +1719,40 @@ export interface ExchangeRefreshRevealRequest {
link_sigs: EddsaSignatureString[]; link_sigs: EddsaSignatureString[];
} }
export interface DepositSuccess {
// Optional base URL of the exchange for looking up wire transfers
// associated with this transaction. If not given,
// the base URL is the same as the one used for this request.
// Can be used if the base URL for /transactions/ differs from that
// for /coins/, i.e. for load balancing. Clients SHOULD
// respect the transaction_base_url if provided. Any HTTP server
// belonging to an exchange MUST generate a 307 or 308 redirection
// to the correct base URL should a client uses the wrong base
// URL, or if the base URL has changed since the deposit.
transaction_base_url?: string;
// timestamp when the deposit was received by the exchange.
exchange_timestamp: Timestamp;
// the EdDSA signature of TALER_DepositConfirmationPS using a current
// signing key of the exchange affirming the successful
// deposit and that the exchange will transfer the funds after the refund
// deadline, or as soon as possible if the refund deadline is zero.
exchange_sig: string;
// public EdDSA key of the exchange that was used to
// generate the signature.
// Should match one of the exchange's signing keys from /keys. It is given
// explicitly as the client might otherwise be confused by clock skew as to
// which signing key was used.
exchange_pub: string;
}
export const codecForDepositSuccess = (): Codec<DepositSuccess> =>
buildCodecForObject<DepositSuccess>()
.property("exchange_pub", codecForString())
.property("exchange_sig", codecForString())
.property("exchange_timestamp", codecForTimestamp)
.property("transaction_base_url", codecOptional(codecForString()))
.build("DepositSuccess");

View File

@ -78,6 +78,9 @@ export namespace Duration {
return Math.ceil(d.d_ms / 1000 / 60 / 60 / 24 / 365); return Math.ceil(d.d_ms / 1000 / 60 / 60 / 24 / 365);
} }
export const fromSpec = durationFromSpec; export const fromSpec = durationFromSpec;
export function getForever(): Duration {
return { d_ms: "forever" };
}
} }
export namespace Timestamp { export namespace Timestamp {

View File

@ -458,7 +458,7 @@ export interface TalerErrorDetails {
details: unknown; details: unknown;
} }
export interface PlanchetCreationResult { export interface WithdrawalPlanchet {
coinPub: string; coinPub: string;
coinPriv: string; coinPriv: string;
reservePub: string; reservePub: string;

View File

@ -0,0 +1,106 @@
/*
This file is part of GNU Taler
(C) 2022 Taler Systems S.A.
GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
Foundation; either version 3, or (at your option) any later version.
GNU 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 General Public License for more details.
You should have received a copy of the GNU General Public License along with
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
/**
* Imports.
*/
import {
buildCodecForObject,
codecForNumber,
codecForString,
codecOptional,
j2s,
Logger,
} from "@gnu-taler/taler-util";
import {
getDefaultNodeWallet2,
NodeHttpLib,
WalletApiOperation,
Wallet,
AccessStats,
downloadExchangeInfo,
} from "@gnu-taler/taler-wallet-core";
/**
* Entry point for the benchmark.
*
* The benchmark runs against an existing Taler deployment and does not
* set up its own services.
*/
export async function runBench2(configJson: any): Promise<void> {
const logger = new Logger("Bench1");
// Validate the configuration file for this benchmark.
const benchConf = codecForBench1Config().decode(configJson);
const myHttpLib = new NodeHttpLib();
myHttpLib.setThrottling(false);
const exchangeInfo = await downloadExchangeInfo(
benchConf.exchange,
myHttpLib,
);
}
/**
* Format of the configuration file passed to the benchmark
*/
interface Bench2Config {
/**
* Base URL of the bank.
*/
bank: string;
/**
* Payto url for deposits.
*/
payto: string;
/**
* Base URL of the exchange.
*/
exchange: string;
/**
* How many withdraw/deposit iterations should be made?
* Defaults to 1.
*/
iterations?: number;
currency: string;
deposits?: number;
/**
* How any iterations run until the wallet db gets purged
* Defaults to 20.
*/
restartAfter?: number;
}
/**
* Schema validation codec for Bench1Config.
*/
const codecForBench1Config = () =>
buildCodecForObject<Bench2Config>()
.property("bank", codecForString())
.property("payto", codecForString())
.property("exchange", codecForString())
.property("iterations", codecOptional(codecForNumber()))
.property("deposits", codecOptional(codecForNumber()))
.property("currency", codecForString())
.property("restartAfter", codecOptional(codecForNumber()))
.build("Bench1Config");

View File

@ -45,6 +45,9 @@ import {
MerchantInstancesResponse, MerchantInstancesResponse,
} from "./merchantApiTypes"; } from "./merchantApiTypes";
import { import {
BankServiceHandle,
HarnessExchangeBankAccount,
NodeHttpLib,
openPromise, openPromise,
OperationFailedError, OperationFailedError,
WalletCoreApiClient, WalletCoreApiClient,
@ -468,164 +471,6 @@ export async function pingProc(
} }
} }
export interface HarnessExchangeBankAccount {
accountName: string;
accountPassword: string;
accountPaytoUri: string;
wireGatewayApiBaseUrl: string;
}
export interface BankServiceInterface {
readonly baseUrl: string;
readonly port: number;
}
export enum CreditDebitIndicator {
Credit = "credit",
Debit = "debit",
}
export interface BankAccountBalanceResponse {
balance: {
amount: AmountString;
credit_debit_indicator: CreditDebitIndicator;
};
}
export namespace BankAccessApi {
export async function getAccountBalance(
bank: BankServiceInterface,
bankUser: BankUser,
): Promise<BankAccountBalanceResponse> {
const url = new URL(`accounts/${bankUser.username}`, bank.baseUrl);
const resp = await axios.get(url.href, {
auth: bankUser,
});
return resp.data;
}
export async function createWithdrawalOperation(
bank: BankServiceInterface,
bankUser: BankUser,
amount: string,
): Promise<WithdrawalOperationInfo> {
const url = new URL(
`accounts/${bankUser.username}/withdrawals`,
bank.baseUrl,
);
const resp = await axios.post(
url.href,
{
amount,
},
{
auth: bankUser,
},
);
return codecForWithdrawalOperationInfo().decode(resp.data);
}
}
export namespace BankApi {
export async function registerAccount(
bank: BankServiceInterface,
username: string,
password: string,
): Promise<BankUser> {
const url = new URL("testing/register", bank.baseUrl);
let resp = await axios.post(url.href, {
username,
password,
});
let paytoUri = `payto://x-taler-bank/localhost/${username}`;
if (process.env.WALLET_HARNESS_WITH_EUFIN) {
paytoUri = resp.data.paytoUri;
}
return {
password,
username,
accountPaytoUri: paytoUri,
};
}
export async function createRandomBankUser(
bank: BankServiceInterface,
): Promise<BankUser> {
const username = "user-" + encodeCrock(getRandomBytes(10)).toLowerCase();
const password = "pw-" + encodeCrock(getRandomBytes(10)).toLowerCase();
return await registerAccount(bank, username, password);
}
export async function adminAddIncoming(
bank: BankServiceInterface,
params: {
exchangeBankAccount: HarnessExchangeBankAccount;
amount: string;
reservePub: string;
debitAccountPayto: string;
},
) {
let maybeBaseUrl = bank.baseUrl;
if (process.env.WALLET_HARNESS_WITH_EUFIN) {
maybeBaseUrl = (bank as EufinBankService).baseUrlDemobank;
}
let url = new URL(
`taler-wire-gateway/${params.exchangeBankAccount.accountName}/admin/add-incoming`,
maybeBaseUrl,
);
await axios.post(
url.href,
{
amount: params.amount,
reserve_pub: params.reservePub,
debit_account: params.debitAccountPayto,
},
{
auth: {
username: params.exchangeBankAccount.accountName,
password: params.exchangeBankAccount.accountPassword,
},
},
);
}
export async function confirmWithdrawalOperation(
bank: BankServiceInterface,
bankUser: BankUser,
wopi: WithdrawalOperationInfo,
): Promise<void> {
const url = new URL(
`accounts/${bankUser.username}/withdrawals/${wopi.withdrawal_id}/confirm`,
bank.baseUrl,
);
await axios.post(
url.href,
{},
{
auth: bankUser,
},
);
}
export async function abortWithdrawalOperation(
bank: BankServiceInterface,
bankUser: BankUser,
wopi: WithdrawalOperationInfo,
): Promise<void> {
const url = new URL(
`accounts/${bankUser.username}/withdrawals/${wopi.withdrawal_id}/abort`,
bank.baseUrl,
);
await axios.post(
url.href,
{},
{
auth: bankUser,
},
);
}
}
class BankServiceBase { class BankServiceBase {
proc: ProcessWrapper | undefined; proc: ProcessWrapper | undefined;
@ -640,10 +485,12 @@ class BankServiceBase {
* Work in progress. The key point is that both Sandbox and Nexus * Work in progress. The key point is that both Sandbox and Nexus
* will be configured and started by this class. * will be configured and started by this class.
*/ */
class EufinBankService extends BankServiceBase implements BankServiceInterface { class EufinBankService extends BankServiceBase implements BankServiceHandle {
sandboxProc: ProcessWrapper | undefined; sandboxProc: ProcessWrapper | undefined;
nexusProc: ProcessWrapper | undefined; nexusProc: ProcessWrapper | undefined;
http = new NodeHttpLib();
static async create( static async create(
gc: GlobalTestState, gc: GlobalTestState,
bc: BankConfig, bc: BankConfig,
@ -914,9 +761,11 @@ class EufinBankService extends BankServiceBase implements BankServiceInterface {
} }
} }
class PybankService extends BankServiceBase implements BankServiceInterface { class PybankService extends BankServiceBase implements BankServiceHandle {
proc: ProcessWrapper | undefined; proc: ProcessWrapper | undefined;
http = new NodeHttpLib();
static async create( static async create(
gc: GlobalTestState, gc: GlobalTestState,
bc: BankConfig, bc: BankConfig,
@ -955,6 +804,7 @@ class PybankService extends BankServiceBase implements BankServiceInterface {
const config = Configuration.load(this.configFile); const config = Configuration.load(this.configFile);
config.setString("bank", "suggested_exchange", e.baseUrl); config.setString("bank", "suggested_exchange", e.baseUrl);
config.setString("bank", "suggested_exchange_payto", exchangePayto); config.setString("bank", "suggested_exchange_payto", exchangePayto);
config.write(this.configFile);
} }
get baseUrl(): string { get baseUrl(): string {
@ -1087,23 +937,6 @@ export class FakeBankService {
} }
} }
export interface BankUser {
username: string;
password: string;
accountPaytoUri: string;
}
export interface WithdrawalOperationInfo {
withdrawal_id: string;
taler_withdraw_uri: string;
}
const codecForWithdrawalOperationInfo = (): Codec<WithdrawalOperationInfo> =>
buildCodecForObject<WithdrawalOperationInfo>()
.property("withdrawal_id", codecForString())
.property("taler_withdraw_uri", codecForString())
.build("WithdrawalOperationInfo");
export interface ExchangeConfig { export interface ExchangeConfig {
name: string; name: string;
currency: string; currency: string;

View File

@ -30,22 +30,19 @@ import {
Duration, Duration,
PreparePayResultType, PreparePayResultType,
} from "@gnu-taler/taler-util"; } from "@gnu-taler/taler-util";
import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; import { BankAccessApi, BankApi, HarnessExchangeBankAccount, WalletApiOperation } from "@gnu-taler/taler-wallet-core";
import { CoinConfig, defaultCoinConfig } from "./denomStructures.js"; import { CoinConfig, defaultCoinConfig } from "./denomStructures.js";
import { import {
FaultInjectedExchangeService, FaultInjectedExchangeService,
FaultInjectedMerchantService, FaultInjectedMerchantService,
} from "./faultInjection.js"; } from "./faultInjection.js";
import { import {
BankAccessApi,
BankApi,
BankService, BankService,
DbInfo, DbInfo,
ExchangeService, ExchangeService,
ExchangeServiceInterface, ExchangeServiceInterface,
getPayto, getPayto,
GlobalTestState, GlobalTestState,
HarnessExchangeBankAccount,
MerchantPrivateApi, MerchantPrivateApi,
MerchantService, MerchantService,
MerchantServiceInterface, MerchantServiceInterface,

View File

@ -24,13 +24,15 @@ import {
setupDb, setupDb,
BankService, BankService,
MerchantService, MerchantService,
BankApi, getPayto,
BankAccessApi,
CreditDebitIndicator,
getPayto
} from "../harness/harness.js"; } from "../harness/harness.js";
import { createEddsaKeyPair, encodeCrock } from "@gnu-taler/taler-util"; import { createEddsaKeyPair, encodeCrock } from "@gnu-taler/taler-util";
import { defaultCoinConfig } from "../harness/denomStructures"; import { defaultCoinConfig } from "../harness/denomStructures";
import {
BankApi,
BankAccessApi,
CreditDebitIndicator,
} from "@gnu-taler/taler-wallet-core";
/** /**
* Run test for basic, bank-integrated withdrawal. * Run test for basic, bank-integrated withdrawal.
@ -97,8 +99,6 @@ export async function runBankApiTest(t: GlobalTestState) {
console.log("setup done!"); console.log("setup done!");
const wallet = new WalletCli(t);
const bankUser = await BankApi.registerAccount(bank, "user1", "pw1"); const bankUser = await BankApi.registerAccount(bank, "user1", "pw1");
// Make sure that registering twice results in a 409 Conflict // Make sure that registering twice results in a 409 Conflict

View File

@ -24,11 +24,13 @@ import {
BankService, BankService,
ExchangeService, ExchangeService,
MerchantService, MerchantService,
getPayto,
} from "../harness/harness.js";
import {
WalletApiOperation,
BankApi, BankApi,
BankAccessApi, BankAccessApi,
getPayto } from "@gnu-taler/taler-wallet-core";
} from "../harness/harness.js";
import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
import { import {
ExchangesListRespose, ExchangesListRespose,
URL, URL,

View File

@ -19,15 +19,16 @@
*/ */
import { import {
ContractTerms, ContractTerms,
CoreApiResponse,
getTimestampNow, getTimestampNow,
timestampTruncateToSecond, timestampTruncateToSecond,
} from "@gnu-taler/taler-util"; } from "@gnu-taler/taler-util";
import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; import {
WalletApiOperation,
HarnessExchangeBankAccount,
} from "@gnu-taler/taler-wallet-core";
import { CoinConfig, defaultCoinConfig } from "../harness/denomStructures"; import { CoinConfig, defaultCoinConfig } from "../harness/denomStructures";
import { import {
DbInfo, DbInfo,
HarnessExchangeBankAccount,
ExchangeService, ExchangeService,
GlobalTestState, GlobalTestState,
MerchantService, MerchantService,
@ -233,13 +234,8 @@ export async function createLibeufinTestEnvironment(
export async function runLibeufinBasicTest(t: GlobalTestState) { export async function runLibeufinBasicTest(t: GlobalTestState) {
// Set up test environment // Set up test environment
const { const { wallet, exchange, merchant, libeufinSandbox, libeufinNexus } =
wallet, await createLibeufinTestEnvironment(t);
exchange,
merchant,
libeufinSandbox,
libeufinNexus,
} = await createLibeufinTestEnvironment(t);
await wallet.client.call(WalletApiOperation.AddExchange, { await wallet.client.call(WalletApiOperation.AddExchange, {
exchangeBaseUrl: exchange.baseUrl, exchangeBaseUrl: exchange.baseUrl,

View File

@ -20,25 +20,30 @@
import { import {
GlobalTestState, GlobalTestState,
MerchantPrivateApi, MerchantPrivateApi,
BankServiceInterface,
MerchantServiceInterface, MerchantServiceInterface,
WalletCli, WalletCli,
ExchangeServiceInterface, ExchangeServiceInterface,
} from "../harness/harness.js"; } from "../harness/harness.js";
import { createSimpleTestkudosEnvironment, withdrawViaBank } from "../harness/helpers.js"; import {
createSimpleTestkudosEnvironment,
withdrawViaBank,
} from "../harness/helpers.js";
import { import {
URL, URL,
durationFromSpec, durationFromSpec,
PreparePayResultType, PreparePayResultType,
} from "@gnu-taler/taler-util"; } from "@gnu-taler/taler-util";
import axios from "axios"; import axios from "axios";
import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; import {
WalletApiOperation,
BankServiceHandle,
} from "@gnu-taler/taler-wallet-core";
async function testRefundApiWithFulfillmentUrl( async function testRefundApiWithFulfillmentUrl(
t: GlobalTestState, t: GlobalTestState,
env: { env: {
merchant: MerchantServiceInterface; merchant: MerchantServiceInterface;
bank: BankServiceInterface; bank: BankServiceHandle;
wallet: WalletCli; wallet: WalletCli;
exchange: ExchangeServiceInterface; exchange: ExchangeServiceInterface;
}, },
@ -152,7 +157,7 @@ async function testRefundApiWithFulfillmentMessage(
t: GlobalTestState, t: GlobalTestState,
env: { env: {
merchant: MerchantServiceInterface; merchant: MerchantServiceInterface;
bank: BankServiceInterface; bank: BankServiceHandle;
wallet: WalletCli; wallet: WalletCli;
exchange: ExchangeServiceInterface; exchange: ExchangeServiceInterface;
}, },
@ -267,12 +272,8 @@ async function testRefundApiWithFulfillmentMessage(
export async function runMerchantRefundApiTest(t: GlobalTestState) { export async function runMerchantRefundApiTest(t: GlobalTestState) {
// Set up test environment // Set up test environment
const { const { wallet, bank, exchange, merchant } =
wallet, await createSimpleTestkudosEnvironment(t);
bank,
exchange,
merchant,
} = await createSimpleTestkudosEnvironment(t);
// Withdraw digital cash into the wallet. // Withdraw digital cash into the wallet.

View File

@ -29,9 +29,7 @@ import {
BankService, BankService,
WalletCli, WalletCli,
MerchantPrivateApi, MerchantPrivateApi,
BankApi, getPayto,
BankAccessApi,
getPayto
} from "../harness/harness.js"; } from "../harness/harness.js";
import { import {
FaultInjectedExchangeService, FaultInjectedExchangeService,
@ -40,7 +38,11 @@ import {
} from "../harness/faultInjection"; } from "../harness/faultInjection";
import { CoreApiResponse } from "@gnu-taler/taler-util"; import { CoreApiResponse } from "@gnu-taler/taler-util";
import { defaultCoinConfig } from "../harness/denomStructures"; import { defaultCoinConfig } from "../harness/denomStructures";
import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; import {
WalletApiOperation,
BankApi,
BankAccessApi,
} from "@gnu-taler/taler-wallet-core";
/** /**
* Run test for basic, bank-integrated withdrawal. * Run test for basic, bank-integrated withdrawal.
@ -146,7 +148,6 @@ export async function runPaymentFaultTest(t: GlobalTestState) {
await wallet.runUntilDone(); await wallet.runUntilDone();
// Check balance // Check balance
await wallet.client.call(WalletApiOperation.GetBalances, {}); await wallet.client.call(WalletApiOperation.GetBalances, {});

View File

@ -17,31 +17,33 @@
/** /**
* Imports. * Imports.
*/ */
import { GlobalTestState, WalletCli } from "../harness/harness.js";
import { makeTestPayment } from "../harness/helpers.js";
import { import {
GlobalTestState, WalletApiOperation,
BankApi, BankApi,
WalletCli, BankAccessApi,
BankAccessApi BankServiceHandle,
} from "../harness/harness.js"; NodeHttpLib,
import { } from "@gnu-taler/taler-wallet-core";
makeTestPayment,
} from "../harness/helpers.js";
import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
/** /**
* Run test for basic, bank-integrated withdrawal and payment. * Run test for basic, bank-integrated withdrawal and payment.
*/ */
export async function runPaymentDemoTest(t: GlobalTestState) { export async function runPaymentDemoTest(t: GlobalTestState) {
// Withdraw digital cash into the wallet. // Withdraw digital cash into the wallet.
let bankInterface = { let bankInterface: BankServiceHandle = {
baseUrl: "https://bank.demo.taler.net/", baseUrl: "https://bank.demo.taler.net/",
port: 0 // unused. http: new NodeHttpLib(),
}; };
let user = await BankApi.createRandomBankUser(bankInterface); let user = await BankApi.createRandomBankUser(bankInterface);
let wop = await BankAccessApi.createWithdrawalOperation(bankInterface, user, "KUDOS:20"); let wop = await BankAccessApi.createWithdrawalOperation(
bankInterface,
user,
"KUDOS:20",
);
let wallet = new WalletCli(t); let wallet = new WalletCli(t);
await wallet.client.call(WalletApiOperation.GetWithdrawalDetailsForUri, { await wallet.client.call(WalletApiOperation.GetWithdrawalDetailsForUri, {
talerWithdrawUri: wop.taler_withdraw_uri, talerWithdrawUri: wop.taler_withdraw_uri,
}); });
@ -60,7 +62,10 @@ export async function runPaymentDemoTest(t: GlobalTestState) {
}); });
await wallet.runUntilDone(); await wallet.runUntilDone();
let balanceBefore = await wallet.client.call(WalletApiOperation.GetBalances, {}); let balanceBefore = await wallet.client.call(
WalletApiOperation.GetBalances,
{},
);
t.assertTrue(balanceBefore["balances"].length == 1); t.assertTrue(balanceBefore["balances"].length == 1);
const order = { const order = {
@ -70,7 +75,7 @@ export async function runPaymentDemoTest(t: GlobalTestState) {
}; };
let merchant = { let merchant = {
makeInstanceBaseUrl: function(instanceName?: string) { makeInstanceBaseUrl: function (instanceName?: string) {
return "https://backend.demo.taler.net/instances/donations/"; return "https://backend.demo.taler.net/instances/donations/";
}, },
port: 0, port: 0,
@ -82,17 +87,26 @@ export async function runPaymentDemoTest(t: GlobalTestState) {
await makeTestPayment( await makeTestPayment(
t, t,
{ {
merchant, wallet, order merchant,
wallet,
order,
}, },
{ {
"Authorization": `Bearer ${process.env["TALER_ENV_FRONTENDS_APITOKEN"]}`, Authorization: `Bearer ${process.env["TALER_ENV_FRONTENDS_APITOKEN"]}`,
}); },
);
await wallet.runUntilDone(); await wallet.runUntilDone();
let balanceAfter = await wallet.client.call(WalletApiOperation.GetBalances, {}); let balanceAfter = await wallet.client.call(
WalletApiOperation.GetBalances,
{},
);
t.assertTrue(balanceAfter["balances"].length == 1); t.assertTrue(balanceAfter["balances"].length == 1);
t.assertTrue(balanceBefore["balances"][0]["available"] > balanceAfter["balances"][0]["available"]); t.assertTrue(
balanceBefore["balances"][0]["available"] >
balanceAfter["balances"][0]["available"],
);
} }
runPaymentDemoTest.excludeByDefault = true; runPaymentDemoTest.excludeByDefault = true;

View File

@ -17,8 +17,12 @@
/** /**
* Imports. * Imports.
*/ */
import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; import { WalletApiOperation, BankApi } from "@gnu-taler/taler-wallet-core";
import { GlobalTestState, MerchantPrivateApi, BankApi, getWireMethod } from "../harness/harness.js"; import {
GlobalTestState,
MerchantPrivateApi,
getWireMethod,
} from "../harness/harness.js";
import { createSimpleTestkudosEnvironment } from "../harness/helpers.js"; import { createSimpleTestkudosEnvironment } from "../harness/helpers.js";
/** /**
@ -27,13 +31,8 @@ import { createSimpleTestkudosEnvironment } from "../harness/helpers.js";
export async function runTippingTest(t: GlobalTestState) { export async function runTippingTest(t: GlobalTestState) {
// Set up test environment // Set up test environment
const { const { wallet, bank, exchange, merchant, exchangeBankAccount } =
wallet, await createSimpleTestkudosEnvironment(t);
bank,
exchange,
merchant,
exchangeBankAccount,
} = await createSimpleTestkudosEnvironment(t);
const mbu = await BankApi.createRandomBankUser(bank); const mbu = await BankApi.createRandomBankUser(bank);

View File

@ -0,0 +1,358 @@
/*
This file is part of GNU Taler
(C) 2020 Taler Systems S.A.
GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
Foundation; either version 3, or (at your option) any later version.
GNU 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 General Public License for more details.
You should have received a copy of the GNU General Public License along with
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
/**
* Imports.
*/
import {
AmountJson,
AmountLike,
Amounts,
AmountString,
codecForBankWithdrawalOperationPostResponse,
codecForDepositSuccess,
codecForExchangeMeltResponse,
codecForWithdrawResponse,
DenominationPubKey,
eddsaGetPublic,
encodeCrock,
ExchangeMeltRequest,
ExchangeProtocolVersion,
ExchangeWithdrawRequest,
getRandomBytes,
getTimestampNow,
hashWire,
j2s,
Timestamp,
UnblindedSignature,
} from "@gnu-taler/taler-util";
import {
BankAccessApi,
BankApi,
BankServiceHandle,
CryptoApi,
DenominationRecord,
downloadExchangeInfo,
ExchangeInfo,
getBankWithdrawalInfo,
HttpRequestLibrary,
isWithdrawableDenom,
NodeHttpLib,
OperationFailedError,
readSuccessResponseJsonOrThrow,
SynchronousCryptoWorkerFactory,
} from "@gnu-taler/taler-wallet-core";
import { GlobalTestState } from "../harness/harness.js";
import { createSimpleTestkudosEnvironment } from "../harness/helpers.js";
const httpLib = new NodeHttpLib();
export interface ReserveKeypair {
reservePub: string;
reservePriv: string;
}
/**
* Denormalized info about a coin.
*/
export interface CoinInfo {
coinPub: string;
coinPriv: string;
exchangeBaseUrl: string;
denomSig: UnblindedSignature;
denomPub: DenominationPubKey;
denomPubHash: string;
feeDeposit: string;
feeRefresh: string;
}
export function generateReserveKeypair(): ReserveKeypair {
const priv = getRandomBytes(32);
const pub = eddsaGetPublic(priv);
return {
reservePriv: encodeCrock(priv),
reservePub: encodeCrock(pub),
};
}
async function topupReserveWithDemobank(
reservePub: string,
bankBaseUrl: string,
exchangeInfo: ExchangeInfo,
amount: AmountString,
) {
const bankHandle: BankServiceHandle = {
baseUrl: bankBaseUrl,
http: httpLib,
};
const bankUser = await BankApi.createRandomBankUser(bankHandle);
const wopi = await BankAccessApi.createWithdrawalOperation(
bankHandle,
bankUser,
amount,
);
const bankInfo = await getBankWithdrawalInfo(
httpLib,
wopi.taler_withdraw_uri,
);
const bankStatusUrl = bankInfo.extractedStatusUrl;
if (!bankInfo.suggestedExchange) {
throw Error("no suggested exchange");
}
const plainPaytoUris =
exchangeInfo.wire.accounts.map((x) => x.payto_uri) ?? [];
if (plainPaytoUris.length <= 0) {
throw new Error();
}
const httpResp = await httpLib.postJson(bankStatusUrl, {
reserve_pub: reservePub,
selected_exchange: plainPaytoUris[0],
});
await readSuccessResponseJsonOrThrow(
httpResp,
codecForBankWithdrawalOperationPostResponse(),
);
await BankApi.confirmWithdrawalOperation(bankHandle, bankUser, wopi);
}
async function withdrawCoin(args: {
http: HttpRequestLibrary;
cryptoApi: CryptoApi;
reserveKeyPair: ReserveKeypair;
denom: DenominationRecord;
exchangeBaseUrl: string;
}): Promise<CoinInfo> {
const { http, cryptoApi, reserveKeyPair, denom, exchangeBaseUrl } = args;
const planchet = await cryptoApi.createPlanchet({
coinIndex: 0,
denomPub: denom.denomPub,
feeWithdraw: denom.feeWithdraw,
reservePriv: reserveKeyPair.reservePriv,
reservePub: reserveKeyPair.reservePub,
secretSeed: encodeCrock(getRandomBytes(32)),
value: denom.value,
});
const reqBody: ExchangeWithdrawRequest = {
denom_pub_hash: planchet.denomPubHash,
reserve_sig: planchet.withdrawSig,
coin_ev: planchet.coinEv,
};
const reqUrl = new URL(
`reserves/${planchet.reservePub}/withdraw`,
exchangeBaseUrl,
).href;
const resp = await http.postJson(reqUrl, reqBody);
const r = await readSuccessResponseJsonOrThrow(
resp,
codecForWithdrawResponse(),
);
const ubSig = await cryptoApi.unblindDenominationSignature({
planchet,
evSig: r.ev_sig,
});
return {
coinPriv: planchet.coinPriv,
coinPub: planchet.coinPub,
denomSig: ubSig,
denomPub: denom.denomPub,
denomPubHash: denom.denomPubHash,
feeDeposit: Amounts.stringify(denom.feeDeposit),
feeRefresh: Amounts.stringify(denom.feeRefresh),
exchangeBaseUrl: args.exchangeBaseUrl,
};
}
function findDenomOrThrow(
exchangeInfo: ExchangeInfo,
amount: AmountString,
): DenominationRecord {
for (const d of exchangeInfo.keys.currentDenominations) {
if (Amounts.cmp(d.value, amount) === 0 && isWithdrawableDenom(d)) {
return d;
}
}
throw new Error("no matching denomination found");
}
async function depositCoin(args: {
http: HttpRequestLibrary;
cryptoApi: CryptoApi;
exchangeBaseUrl: string;
coin: CoinInfo;
amount: AmountString;
}) {
const { coin, http, cryptoApi } = args;
const depositPayto = "payto://x-taler-bank/localhost/foo";
const wireSalt = encodeCrock(getRandomBytes(16));
const contractTermsHash = encodeCrock(getRandomBytes(64));
const depositTimestamp = getTimestampNow();
const refundDeadline = getTimestampNow();
const merchantPub = encodeCrock(getRandomBytes(32));
const dp = await cryptoApi.signDepositPermission({
coinPriv: coin.coinPriv,
coinPub: coin.coinPub,
contractTermsHash,
denomKeyType: coin.denomPub.cipher,
denomPubHash: coin.denomPubHash,
denomSig: coin.denomSig,
exchangeBaseUrl: args.exchangeBaseUrl,
feeDeposit: Amounts.parseOrThrow(coin.feeDeposit),
merchantPub,
spendAmount: Amounts.parseOrThrow(args.amount),
timestamp: depositTimestamp,
refundDeadline: refundDeadline,
wireInfoHash: hashWire(depositPayto, wireSalt),
});
const requestBody = {
contribution: Amounts.stringify(dp.contribution),
merchant_payto_uri: depositPayto,
wire_salt: wireSalt,
h_contract_terms: contractTermsHash,
ub_sig: coin.denomSig,
timestamp: depositTimestamp,
wire_transfer_deadline: getTimestampNow(),
refund_deadline: refundDeadline,
coin_sig: dp.coin_sig,
denom_pub_hash: dp.h_denom,
merchant_pub: merchantPub,
};
const url = new URL(`coins/${dp.coin_pub}/deposit`, dp.exchange_url);
const httpResp = await http.postJson(url.href, requestBody);
await readSuccessResponseJsonOrThrow(httpResp, codecForDepositSuccess());
}
async function refreshCoin(req: {
http: HttpRequestLibrary;
cryptoApi: CryptoApi;
oldCoin: CoinInfo;
newDenoms: DenominationRecord[];
}): Promise<void> {
const { cryptoApi, oldCoin, http } = req;
const refreshSessionSeed = encodeCrock(getRandomBytes(32));
const session = await cryptoApi.deriveRefreshSession({
exchangeProtocolVersion: ExchangeProtocolVersion.V12,
feeRefresh: Amounts.parseOrThrow(oldCoin.feeRefresh),
kappa: 3,
meltCoinDenomPubHash: oldCoin.denomPubHash,
meltCoinPriv: oldCoin.coinPriv,
meltCoinPub: oldCoin.coinPub,
sessionSecretSeed: refreshSessionSeed,
newCoinDenoms: req.newDenoms.map((x) => ({
count: 1,
denomPub: x.denomPub,
feeWithdraw: x.feeWithdraw,
value: x.value,
})),
});
const meltReqBody: ExchangeMeltRequest = {
coin_pub: oldCoin.coinPub,
confirm_sig: session.confirmSig,
denom_pub_hash: oldCoin.denomPubHash,
denom_sig: oldCoin.denomSig,
rc: session.hash,
value_with_fee: Amounts.stringify(session.meltValueWithFee),
};
const reqUrl = new URL(
`coins/${oldCoin.coinPub}/melt`,
oldCoin.exchangeBaseUrl,
);
const resp = await http.postJson(reqUrl.href, meltReqBody);
const meltResponse = await readSuccessResponseJsonOrThrow(
resp,
codecForExchangeMeltResponse(),
);
const norevealIndex = meltResponse.noreveal_index;
}
/**
* Run test for basic, bank-integrated withdrawal and payment.
*/
export async function runWalletDblessTest(t: GlobalTestState) {
// Set up test environment
const { bank, exchange } = await createSimpleTestkudosEnvironment(t);
const http = new NodeHttpLib();
const cryptoApi = new CryptoApi(new SynchronousCryptoWorkerFactory());
try {
// Withdraw digital cash into the wallet.
const exchangeInfo = await downloadExchangeInfo(exchange.baseUrl, http);
const reserveKeyPair = generateReserveKeypair();
await topupReserveWithDemobank(
reserveKeyPair.reservePub,
bank.baseUrl,
exchangeInfo,
"TESTKUDOS:10",
);
await exchange.runWirewatchOnce();
const d1 = findDenomOrThrow(exchangeInfo, "TESTKUDOS:8");
const coin = await withdrawCoin({
http,
cryptoApi,
reserveKeyPair,
denom: d1,
exchangeBaseUrl: exchange.baseUrl,
});
await depositCoin({
amount: "TESTKUDOS:4",
coin: coin,
cryptoApi,
exchangeBaseUrl: exchange.baseUrl,
http,
});
const refreshDenoms = [
findDenomOrThrow(exchangeInfo, "TESTKUDOS:1"),
findDenomOrThrow(exchangeInfo, "TESTKUDOS:1"),
];
const freshCoins = await refreshCoin({
oldCoin: coin,
cryptoApi,
http,
newDenoms: refreshDenoms,
});
} catch (e) {
if (e instanceof OperationFailedError) {
console.log(e);
console.log(j2s(e.operationError));
} else {
console.log(e);
}
throw e;
}
}
runWalletDblessTest.suites = ["wallet"];

View File

@ -18,8 +18,12 @@
* Imports. * Imports.
*/ */
import { TalerErrorCode } from "@gnu-taler/taler-util"; import { TalerErrorCode } from "@gnu-taler/taler-util";
import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; import {
import { GlobalTestState, BankApi, BankAccessApi } from "../harness/harness.js"; WalletApiOperation,
BankApi,
BankAccessApi,
} from "@gnu-taler/taler-wallet-core";
import { GlobalTestState } from "../harness/harness.js";
import { createSimpleTestkudosEnvironment } from "../harness/helpers.js"; import { createSimpleTestkudosEnvironment } from "../harness/helpers.js";
/** /**

View File

@ -17,10 +17,13 @@
/** /**
* Imports. * Imports.
*/ */
import { GlobalTestState, BankApi, BankAccessApi } from "../harness/harness.js"; import { GlobalTestState } from "../harness/harness.js";
import { createSimpleTestkudosEnvironment } from "../harness/helpers.js"; import { createSimpleTestkudosEnvironment } from "../harness/helpers.js";
import { codecForBalancesResponse } from "@gnu-taler/taler-util"; import {
import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; WalletApiOperation,
BankApi,
BankAccessApi,
} from "@gnu-taler/taler-wallet-core";
/** /**
* Run test for basic, bank-integrated withdrawal. * Run test for basic, bank-integrated withdrawal.
@ -41,18 +44,24 @@ export async function runWithdrawalBankIntegratedTest(t: GlobalTestState) {
// Hand it to the wallet // Hand it to the wallet
const r1 = await wallet.client.call(WalletApiOperation.GetWithdrawalDetailsForUri, { const r1 = await wallet.client.call(
talerWithdrawUri: wop.taler_withdraw_uri, WalletApiOperation.GetWithdrawalDetailsForUri,
}); {
talerWithdrawUri: wop.taler_withdraw_uri,
},
);
await wallet.runPending(); await wallet.runPending();
// Withdraw // Withdraw
const r2 = await wallet.client.call(WalletApiOperation.AcceptBankIntegratedWithdrawal, { const r2 = await wallet.client.call(
exchangeBaseUrl: exchange.baseUrl, WalletApiOperation.AcceptBankIntegratedWithdrawal,
talerWithdrawUri: wop.taler_withdraw_uri, {
}); exchangeBaseUrl: exchange.baseUrl,
talerWithdrawUri: wop.taler_withdraw_uri,
},
);
await wallet.runPending(); await wallet.runPending();
// Confirm it // Confirm it

View File

@ -19,13 +19,11 @@
*/ */
import { import {
GlobalTestState, GlobalTestState,
BankApi,
WalletCli, WalletCli,
setupDb, setupDb,
ExchangeService, ExchangeService,
FakeBankService, FakeBankService,
} from "../harness/harness.js"; } from "../harness/harness.js";
import { createSimpleTestkudosEnvironment } from "../harness/helpers.js";
import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
import { CoinConfig, defaultCoinConfig } from "../harness/denomStructures.js"; import { CoinConfig, defaultCoinConfig } from "../harness/denomStructures.js";
import { URL } from "@gnu-taler/taler-util"; import { URL } from "@gnu-taler/taler-util";

View File

@ -17,9 +17,9 @@
/** /**
* Imports. * Imports.
*/ */
import { GlobalTestState, BankApi } from "../harness/harness.js"; import { GlobalTestState } from "../harness/harness.js";
import { createSimpleTestkudosEnvironment } from "../harness/helpers.js"; import { createSimpleTestkudosEnvironment } from "../harness/helpers.js";
import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; import { WalletApiOperation, BankApi } from "@gnu-taler/taler-wallet-core";
/** /**
* Run test for basic, bank-integrated withdrawal. * Run test for basic, bank-integrated withdrawal.
@ -27,12 +27,8 @@ import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
export async function runTestWithdrawalManualTest(t: GlobalTestState) { export async function runTestWithdrawalManualTest(t: GlobalTestState) {
// Set up test environment // Set up test environment
const { const { wallet, bank, exchange, exchangeBankAccount } =
wallet, await createSimpleTestkudosEnvironment(t);
bank,
exchange,
exchangeBankAccount,
} = await createSimpleTestkudosEnvironment(t);
// Create a withdrawal operation // Create a withdrawal operation
@ -42,11 +38,13 @@ export async function runTestWithdrawalManualTest(t: GlobalTestState) {
exchangeBaseUrl: exchange.baseUrl, exchangeBaseUrl: exchange.baseUrl,
}); });
const wres = await wallet.client.call(
const wres = await wallet.client.call(WalletApiOperation.AcceptManualWithdrawal, { WalletApiOperation.AcceptManualWithdrawal,
exchangeBaseUrl: exchange.baseUrl, {
amount: "TESTKUDOS:10", exchangeBaseUrl: exchange.baseUrl,
}); amount: "TESTKUDOS:10",
},
);
const reservePub: string = wres.reservePub; const reservePub: string = wres.reservePub;

View File

@ -87,6 +87,7 @@ import { runExchangeTimetravelTest } from "./test-exchange-timetravel.js";
import { runDenomUnofferedTest } from "./test-denom-unoffered.js"; import { runDenomUnofferedTest } from "./test-denom-unoffered.js";
import { runWithdrawalFakebankTest } from "./test-withdrawal-fakebank.js"; import { runWithdrawalFakebankTest } from "./test-withdrawal-fakebank.js";
import { runClauseSchnorrTest } from "./test-clause-schnorr.js"; import { runClauseSchnorrTest } from "./test-clause-schnorr.js";
import { runWalletDblessTest } from "./test-wallet-dbless.js";
/** /**
* Test runner. * Test runner.
@ -162,6 +163,7 @@ const allTests: TestMainFunction[] = [
runWalletBackupBasicTest, runWalletBackupBasicTest,
runWalletBackupDoublespendTest, runWalletBackupDoublespendTest,
runWallettestingTest, runWallettestingTest,
runWalletDblessTest,
runWithdrawalAbortBankTest, runWithdrawalAbortBankTest,
runWithdrawalBankIntegratedTest, runWithdrawalBankIntegratedTest,
]; ];

View File

@ -0,0 +1,249 @@
/*
This file is part of GNU Taler
(C) 2022 Taler Systems S.A.
GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
Foundation; either version 3, or (at your option) any later version.
GNU 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 General Public License for more details.
You should have received a copy of the GNU General Public License along with
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
/**
* Client for the Taler (demo-)bank.
*/
/**
* Imports.
*/
import {
AmountString,
buildCodecForObject,
Codec,
codecForString,
encodeCrock,
getRandomBytes,
} from "@gnu-taler/taler-util";
import {
HttpRequestLibrary,
readSuccessResponseJsonOrErrorCode,
readSuccessResponseJsonOrThrow,
} from "./index.browser.js";
export enum CreditDebitIndicator {
Credit = "credit",
Debit = "debit",
}
export interface BankAccountBalanceResponse {
balance: {
amount: AmountString;
credit_debit_indicator: CreditDebitIndicator;
};
}
export interface BankServiceHandle {
readonly baseUrl: string;
readonly http: HttpRequestLibrary;
}
export interface BankUser {
username: string;
password: string;
accountPaytoUri: string;
}
export interface WithdrawalOperationInfo {
withdrawal_id: string;
taler_withdraw_uri: string;
}
/**
* FIXME: Rename, this is not part of the integration test harness anymore.
*/
export interface HarnessExchangeBankAccount {
accountName: string;
accountPassword: string;
accountPaytoUri: string;
wireGatewayApiBaseUrl: string;
}
/**
* Helper function to generate the "Authorization" HTTP header.
*/
function makeBasicAuthHeader(username: string, password: string): string {
const auth = `${username}:${password}`;
const authEncoded: string = Buffer.from(auth).toString("base64");
return `Basic ${authEncoded}`;
}
const codecForWithdrawalOperationInfo = (): Codec<WithdrawalOperationInfo> =>
buildCodecForObject<WithdrawalOperationInfo>()
.property("withdrawal_id", codecForString())
.property("taler_withdraw_uri", codecForString())
.build("WithdrawalOperationInfo");
export namespace BankApi {
export async function registerAccount(
bank: BankServiceHandle,
username: string,
password: string,
): Promise<BankUser> {
const url = new URL("testing/register", bank.baseUrl);
const resp = await bank.http.postJson(url.href, { username, password });
let paytoUri = `payto://x-taler-bank/localhost/${username}`;
if (resp.status !== 200 && resp.status !== 202) {
throw new Error();
}
try {
const respJson = await resp.json();
// LibEuFin demobank returns payto URI in response
if (respJson.paytoUri) {
paytoUri = respJson.paytoUri;
}
} catch (e) {}
return {
password,
username,
accountPaytoUri: paytoUri,
};
}
export async function createRandomBankUser(
bank: BankServiceHandle,
): Promise<BankUser> {
const username = "user-" + encodeCrock(getRandomBytes(10)).toLowerCase();
const password = "pw-" + encodeCrock(getRandomBytes(10)).toLowerCase();
return await registerAccount(bank, username, password);
}
export async function adminAddIncoming(
bank: BankServiceHandle,
params: {
exchangeBankAccount: HarnessExchangeBankAccount;
amount: string;
reservePub: string;
debitAccountPayto: string;
},
) {
let maybeBaseUrl = bank.baseUrl;
let url = new URL(
`taler-wire-gateway/${params.exchangeBankAccount.accountName}/admin/add-incoming`,
maybeBaseUrl,
);
await bank.http.postJson(
url.href,
{
amount: params.amount,
reserve_pub: params.reservePub,
debit_account: params.debitAccountPayto,
},
{
headers: {
Authorization: makeBasicAuthHeader(
params.exchangeBankAccount.accountName,
params.exchangeBankAccount.accountPassword,
),
},
},
);
}
export async function confirmWithdrawalOperation(
bank: BankServiceHandle,
bankUser: BankUser,
wopi: WithdrawalOperationInfo,
): Promise<void> {
const url = new URL(
`accounts/${bankUser.username}/withdrawals/${wopi.withdrawal_id}/confirm`,
bank.baseUrl,
);
await bank.http.postJson(
url.href,
{},
{
headers: {
Authorization: makeBasicAuthHeader(
bankUser.username,
bankUser.password,
),
},
},
);
}
export async function abortWithdrawalOperation(
bank: BankServiceHandle,
bankUser: BankUser,
wopi: WithdrawalOperationInfo,
): Promise<void> {
const url = new URL(
`accounts/${bankUser.username}/withdrawals/${wopi.withdrawal_id}/abort`,
bank.baseUrl,
);
await bank.http.postJson(
url.href,
{},
{
headers: {
Authorization: makeBasicAuthHeader(
bankUser.username,
bankUser.password,
),
},
},
);
}
}
export namespace BankAccessApi {
export async function getAccountBalance(
bank: BankServiceHandle,
bankUser: BankUser,
): Promise<BankAccountBalanceResponse> {
const url = new URL(`accounts/${bankUser.username}`, bank.baseUrl);
const resp = await bank.http.get(url.href, {
headers: {
Authorization: makeBasicAuthHeader(
bankUser.username,
bankUser.password,
),
},
});
return await resp.json();
}
export async function createWithdrawalOperation(
bank: BankServiceHandle,
bankUser: BankUser,
amount: string,
): Promise<WithdrawalOperationInfo> {
const url = new URL(
`accounts/${bankUser.username}/withdrawals`,
bank.baseUrl,
);
const resp = await bank.http.postJson(
url.href,
{
amount,
},
{
headers: {
Authorization: makeBasicAuthHeader(
bankUser.username,
bankUser.password,
),
},
},
);
return readSuccessResponseJsonOrThrow(
resp,
codecForWithdrawalOperationInfo(),
);
}
}

View File

@ -22,20 +22,22 @@
/** /**
* Imports. * Imports.
*/ */
import { CoinRecord, DenominationRecord, WireFee } from "../../db.js"; import { DenominationRecord, WireFee } from "../../db.js";
import { CryptoWorker } from "./cryptoWorkerInterface.js"; import { CryptoWorker } from "./cryptoWorkerInterface.js";
import { import {
BlindedDenominationSignature,
CoinDepositPermission, CoinDepositPermission,
CoinEnvelope, CoinEnvelope,
RecoupRefreshRequest, RecoupRefreshRequest,
RecoupRequest, RecoupRequest,
UnblindedSignature,
} from "@gnu-taler/taler-util"; } from "@gnu-taler/taler-util";
import { import {
BenchmarkResult, BenchmarkResult,
PlanchetCreationResult, WithdrawalPlanchet,
PlanchetCreationRequest, PlanchetCreationRequest,
DepositInfo, DepositInfo,
MakeSyncSignatureRequest, MakeSyncSignatureRequest,
@ -324,10 +326,19 @@ export class CryptoApi {
return p; return p;
} }
createPlanchet( createPlanchet(req: PlanchetCreationRequest): Promise<WithdrawalPlanchet> {
req: PlanchetCreationRequest, return this.doRpc<WithdrawalPlanchet>("createPlanchet", 1, req);
): Promise<PlanchetCreationResult> { }
return this.doRpc<PlanchetCreationResult>("createPlanchet", 1, req);
unblindDenominationSignature(req: {
planchet: WithdrawalPlanchet;
evSig: BlindedDenominationSignature;
}): Promise<UnblindedSignature> {
return this.doRpc<UnblindedSignature>(
"unblindDenominationSignature",
1,
req,
);
} }
createTipPlanchet(req: DeriveTipRequest): Promise<DerivedTipPlanchet> { createTipPlanchet(req: DeriveTipRequest): Promise<DerivedTipPlanchet> {

View File

@ -53,7 +53,7 @@ import {
Logger, Logger,
MakeSyncSignatureRequest, MakeSyncSignatureRequest,
PlanchetCreationRequest, PlanchetCreationRequest,
PlanchetCreationResult, WithdrawalPlanchet,
randomBytes, randomBytes,
RecoupRefreshRequest, RecoupRefreshRequest,
RecoupRequest, RecoupRequest,
@ -70,6 +70,9 @@ import {
Timestamp, Timestamp,
timestampTruncateToSecond, timestampTruncateToSecond,
typedArrayConcat, typedArrayConcat,
BlindedDenominationSignature,
RsaUnblindedSignature,
UnblindedSignature,
} from "@gnu-taler/taler-util"; } from "@gnu-taler/taler-util";
import bigint from "big-integer"; import bigint from "big-integer";
import { DenominationRecord, WireFee } from "../../db.js"; import { DenominationRecord, WireFee } from "../../db.js";
@ -169,7 +172,7 @@ export class CryptoImplementation {
*/ */
async createPlanchet( async createPlanchet(
req: PlanchetCreationRequest, req: PlanchetCreationRequest,
): Promise<PlanchetCreationResult> { ): Promise<WithdrawalPlanchet> {
const denomPub = req.denomPub; const denomPub = req.denomPub;
if (denomPub.cipher === DenomKeyType.Rsa) { if (denomPub.cipher === DenomKeyType.Rsa) {
const reservePub = decodeCrock(req.reservePub); const reservePub = decodeCrock(req.reservePub);
@ -200,7 +203,7 @@ export class CryptoImplementation {
priv: req.reservePriv, priv: req.reservePriv,
}); });
const planchet: PlanchetCreationResult = { const planchet: WithdrawalPlanchet = {
blindingKey: encodeCrock(derivedPlanchet.bks), blindingKey: encodeCrock(derivedPlanchet.bks),
coinEv, coinEv,
coinPriv: encodeCrock(derivedPlanchet.coinPriv), coinPriv: encodeCrock(derivedPlanchet.coinPriv),
@ -428,6 +431,30 @@ export class CryptoImplementation {
}; };
} }
unblindDenominationSignature(req: {
planchet: WithdrawalPlanchet;
evSig: BlindedDenominationSignature;
}): UnblindedSignature {
if (req.evSig.cipher === DenomKeyType.Rsa) {
if (req.planchet.denomPub.cipher !== DenomKeyType.Rsa) {
throw new Error(
"planchet cipher does not match blind signature cipher",
);
}
const denomSig = rsaUnblind(
decodeCrock(req.evSig.blinded_rsa_signature),
decodeCrock(req.planchet.denomPub.rsa_public_key),
decodeCrock(req.planchet.blindingKey),
);
return {
cipher: DenomKeyType.Rsa,
rsa_signature: encodeCrock(denomSig),
};
} else {
throw Error(`unblinding for cipher ${req.evSig.cipher} not implemented`);
}
}
/** /**
* Unblind a blindly signed value. * Unblind a blindly signed value.
*/ */

View File

@ -36,7 +36,7 @@ export * from "./db-utils.js";
export { CryptoImplementation } from "./crypto/workers/cryptoImplementation.js"; export { CryptoImplementation } from "./crypto/workers/cryptoImplementation.js";
export type { CryptoWorker } from "./crypto/workers/cryptoWorkerInterface.js"; export type { CryptoWorker } from "./crypto/workers/cryptoWorkerInterface.js";
export { CryptoWorkerFactory, CryptoApi } from "./crypto/workers/cryptoApi.js"; export { CryptoWorkerFactory, CryptoApi } from "./crypto/workers/cryptoApi.js";
export { SynchronousCryptoWorker } from "./crypto/workers/synchronousWorker.js" export { SynchronousCryptoWorker } from "./crypto/workers/synchronousWorker.js";
export * from "./pending-types.js"; export * from "./pending-types.js";
@ -47,3 +47,12 @@ export * from "./wallet.js";
export * from "./operations/backup/index.js"; export * from "./operations/backup/index.js";
export { makeEventId } from "./operations/transactions.js"; export { makeEventId } from "./operations/transactions.js";
export * from "./operations/exchanges.js";
export * from "./bank-api-client.js";
export * from "./operations/reserves.js";
export * from "./operations/withdraw.js";
export * from "./crypto/workers/synchronousWorkerFactory.js";

View File

@ -20,6 +20,7 @@ import {
buildCodecForObject, buildCodecForObject,
canonicalJson, canonicalJson,
Codec, Codec,
codecForDepositSuccess,
codecForString, codecForString,
codecForTimestamp, codecForTimestamp,
codecOptional, codecOptional,
@ -32,6 +33,7 @@ import {
GetFeeForDepositRequest, GetFeeForDepositRequest,
getRandomBytes, getRandomBytes,
getTimestampNow, getTimestampNow,
hashWire,
Logger, Logger,
NotificationType, NotificationType,
parsePaytoUri, parsePaytoUri,
@ -57,7 +59,6 @@ import {
generateDepositPermissions, generateDepositPermissions,
getCandidatePayCoins, getCandidatePayCoins,
getTotalPaymentCost, getTotalPaymentCost,
hashWire,
} from "./pay.js"; } from "./pay.js";
import { getTotalRefreshCost } from "./refresh.js"; import { getTotalRefreshCost } from "./refresh.js";
@ -66,43 +67,6 @@ import { getTotalRefreshCost } from "./refresh.js";
*/ */
const logger = new Logger("deposits.ts"); const logger = new Logger("deposits.ts");
interface DepositSuccess {
// Optional base URL of the exchange for looking up wire transfers
// associated with this transaction. If not given,
// the base URL is the same as the one used for this request.
// Can be used if the base URL for /transactions/ differs from that
// for /coins/, i.e. for load balancing. Clients SHOULD
// respect the transaction_base_url if provided. Any HTTP server
// belonging to an exchange MUST generate a 307 or 308 redirection
// to the correct base URL should a client uses the wrong base
// URL, or if the base URL has changed since the deposit.
transaction_base_url?: string;
// timestamp when the deposit was received by the exchange.
exchange_timestamp: Timestamp;
// the EdDSA signature of TALER_DepositConfirmationPS using a current
// signing key of the exchange affirming the successful
// deposit and that the exchange will transfer the funds after the refund
// deadline, or as soon as possible if the refund deadline is zero.
exchange_sig: string;
// public EdDSA key of the exchange that was used to
// generate the signature.
// Should match one of the exchange's signing keys from /keys. It is given
// explicitly as the client might otherwise be confused by clock skew as to
// which signing key was used.
exchange_pub: string;
}
const codecForDepositSuccess = (): Codec<DepositSuccess> =>
buildCodecForObject<DepositSuccess>()
.property("exchange_pub", codecForString())
.property("exchange_sig", codecForString())
.property("exchange_timestamp", codecForTimestamp)
.property("transaction_base_url", codecOptional(codecForString()))
.build("DepositSuccess");
async function resetDepositGroupRetry( async function resetDepositGroupRetry(
ws: InternalWalletState, ws: InternalWalletState,
depositGroupId: string, depositGroupId: string,
@ -202,7 +166,6 @@ async function processDepositGroupImpl(
} }
const perm = depositPermissions[i]; const perm = depositPermissions[i];
let requestBody: any; let requestBody: any;
logger.info("creating v10 deposit request");
requestBody = { requestBody = {
contribution: Amounts.stringify(perm.contribution), contribution: Amounts.stringify(perm.contribution),
merchant_payto_uri: depositGroup.wire.payto_uri, merchant_payto_uri: depositGroup.wire.payto_uri,

View File

@ -43,6 +43,7 @@ import {
codecForAny, codecForAny,
DenominationPubKey, DenominationPubKey,
DenomKeyType, DenomKeyType,
ExchangeKeysJson,
} from "@gnu-taler/taler-util"; } from "@gnu-taler/taler-util";
import { decodeCrock, encodeCrock, hash } from "@gnu-taler/taler-util"; import { decodeCrock, encodeCrock, hash } from "@gnu-taler/taler-util";
import { CryptoApi } from "../crypto/workers/cryptoApi.js"; import { CryptoApi } from "../crypto/workers/cryptoApi.js";
@ -292,12 +293,37 @@ async function validateWireInfo(
}; };
} }
export interface ExchangeInfo {
wire: ExchangeWireJson;
keys: ExchangeKeysDownloadResult;
}
export async function downloadExchangeInfo(
exchangeBaseUrl: string,
http: HttpRequestLibrary,
): Promise<ExchangeInfo> {
const wireInfo = await downloadExchangeWireInfo(
exchangeBaseUrl,
http,
Duration.getForever(),
);
const keysInfo = await downloadExchangeKeysInfo(
exchangeBaseUrl,
http,
Duration.getForever(),
);
return {
keys: keysInfo,
wire: wireInfo,
};
}
/** /**
* Fetch wire information for an exchange. * Fetch wire information for an exchange.
* *
* @param exchangeBaseUrl Exchange base URL, assumed to be already normalized. * @param exchangeBaseUrl Exchange base URL, assumed to be already normalized.
*/ */
async function downloadExchangeWithWireInfo( async function downloadExchangeWireInfo(
exchangeBaseUrl: string, exchangeBaseUrl: string,
http: HttpRequestLibrary, http: HttpRequestLibrary,
timeout: Duration, timeout: Duration,
@ -374,7 +400,7 @@ interface ExchangeKeysDownloadResult {
/** /**
* Download and validate an exchange's /keys data. * Download and validate an exchange's /keys data.
*/ */
async function downloadKeysInfo( async function downloadExchangeKeysInfo(
baseUrl: string, baseUrl: string,
http: HttpRequestLibrary, http: HttpRequestLibrary,
timeout: Duration, timeout: Duration,
@ -526,10 +552,10 @@ async function updateExchangeFromUrlImpl(
const timeout = getExchangeRequestTimeout(); const timeout = getExchangeRequestTimeout();
const keysInfo = await downloadKeysInfo(baseUrl, ws.http, timeout); const keysInfo = await downloadExchangeKeysInfo(baseUrl, ws.http, timeout);
logger.info("updating exchange /wire info"); logger.info("updating exchange /wire info");
const wireInfoDownload = await downloadExchangeWithWireInfo( const wireInfoDownload = await downloadExchangeWireInfo(
baseUrl, baseUrl,
ws.http, ws.http,
timeout, timeout,

View File

@ -112,19 +112,6 @@ import { createRefreshGroup, getTotalRefreshCost } from "./refresh.js";
*/ */
const logger = new Logger("pay.ts"); const logger = new Logger("pay.ts");
/**
* FIXME: Move this to crypto worker or at least talerCrypto.ts
*/
export function hashWire(paytoUri: string, salt: string): string {
const r = kdf(
64,
stringToBytes(paytoUri + "\0"),
decodeCrock(salt),
stringToBytes("merchant-wire-signature"),
);
return encodeCrock(r);
}
/** /**
* Compute the total cost of a payment to the customer. * Compute the total cost of a payment to the customer.
* *

View File

@ -17,6 +17,7 @@
import { import {
DenomKeyType, DenomKeyType,
encodeCrock, encodeCrock,
ExchangeMeltRequest,
ExchangeProtocolVersion, ExchangeProtocolVersion,
ExchangeRefreshRevealRequest, ExchangeRefreshRevealRequest,
getRandomBytes, getRandomBytes,
@ -394,17 +395,14 @@ async function refreshMelt(
`coins/${oldCoin.coinPub}/melt`, `coins/${oldCoin.coinPub}/melt`,
oldCoin.exchangeBaseUrl, oldCoin.exchangeBaseUrl,
); );
let meltReqBody: any; const meltReqBody: ExchangeMeltRequest = {
if (oldDenom.denomPub.cipher === DenomKeyType.Rsa) { coin_pub: oldCoin.coinPub,
meltReqBody = { confirm_sig: derived.confirmSig,
coin_pub: oldCoin.coinPub, denom_pub_hash: oldCoin.denomPubHash,
confirm_sig: derived.confirmSig, denom_sig: oldCoin.denomSig,
denom_pub_hash: oldCoin.denomPubHash, rc: derived.hash,
denom_sig: oldCoin.denomSig, value_with_fee: Amounts.stringify(derived.meltValueWithFee),
rc: derived.hash, };
value_with_fee: Amounts.stringify(derived.meltValueWithFee),
};
}
const resp = await ws.runSequentialized([EXCHANGE_COINS_LOCK], async () => { const resp = await ws.runSequentialized([EXCHANGE_COINS_LOCK], async () => {
return await ws.http.postJson(reqUrl.href, meltReqBody, { return await ws.http.postJson(reqUrl.href, meltReqBody, {

View File

@ -780,7 +780,7 @@ export async function createTalerWithdrawReserve(
selectedExchange: string, selectedExchange: string,
): Promise<AcceptWithdrawalResponse> { ): Promise<AcceptWithdrawalResponse> {
await updateExchangeFromUrl(ws, selectedExchange); await updateExchangeFromUrl(ws, selectedExchange);
const withdrawInfo = await getBankWithdrawalInfo(ws, talerWithdrawUri); const withdrawInfo = await getBankWithdrawalInfo(ws.http, talerWithdrawUri);
const exchangePaytoUri = await getExchangePaytoUri( const exchangePaytoUri = await getExchangePaytoUri(
ws, ws,
selectedExchange, selectedExchange,

View File

@ -74,7 +74,7 @@ function makeId(length: number): string {
/** /**
* Helper function to generate the "Authorization" HTTP header. * Helper function to generate the "Authorization" HTTP header.
*/ */
function makeAuth(username: string, password: string): string { function makeBasicAuthHeader(username: string, password: string): string {
const auth = `${username}:${password}`; const auth = `${username}:${password}`;
const authEncoded: string = Buffer.from(auth).toString("base64"); const authEncoded: string = Buffer.from(auth).toString("base64");
return `Basic ${authEncoded}`; return `Basic ${authEncoded}`;
@ -89,7 +89,7 @@ export async function withdrawTestBalance(
const bankUser = await registerRandomBankUser(ws.http, bankBaseUrl); const bankUser = await registerRandomBankUser(ws.http, bankBaseUrl);
logger.trace(`Registered bank user ${JSON.stringify(bankUser)}`); logger.trace(`Registered bank user ${JSON.stringify(bankUser)}`);
const wresp = await createBankWithdrawalUri( const wresp = await createDemoBankWithdrawalUri(
ws.http, ws.http,
bankBaseUrl, bankBaseUrl,
bankUser, bankUser,
@ -119,7 +119,11 @@ function getMerchantAuthHeader(m: MerchantBackendInfo): Record<string, string> {
return {}; return {};
} }
async function createBankWithdrawalUri( /**
* Use the testing API of a demobank to create a taler://withdraw URI
* that the wallet can then use to make a withdrawal.
*/
export async function createDemoBankWithdrawalUri(
http: HttpRequestLibrary, http: HttpRequestLibrary,
bankBaseUrl: string, bankBaseUrl: string,
bankUser: BankUser, bankUser: BankUser,
@ -136,7 +140,7 @@ async function createBankWithdrawalUri(
}, },
{ {
headers: { headers: {
Authorization: makeAuth(bankUser.username, bankUser.password), Authorization: makeBasicAuthHeader(bankUser.username, bankUser.password),
}, },
}, },
); );
@ -159,7 +163,7 @@ async function confirmBankWithdrawalUri(
{}, {},
{ {
headers: { headers: {
Authorization: makeAuth(bankUser.username, bankUser.password), Authorization: makeBasicAuthHeader(bankUser.username, bankUser.password),
}, },
}, },
); );

View File

@ -59,7 +59,10 @@ import {
WithdrawalGroupRecord, WithdrawalGroupRecord,
} from "../db.js"; } from "../db.js";
import { walletCoreDebugFlags } from "../util/debugFlags.js"; import { walletCoreDebugFlags } from "../util/debugFlags.js";
import { readSuccessResponseJsonOrThrow } from "../util/http.js"; import {
HttpRequestLibrary,
readSuccessResponseJsonOrThrow,
} from "../util/http.js";
import { initRetryInfo, updateRetryInfoTimeout } from "../util/retries.js"; import { initRetryInfo, updateRetryInfoTimeout } from "../util/retries.js";
import { import {
guardOperationException, guardOperationException,
@ -271,9 +274,11 @@ export function selectWithdrawalDenominations(
/** /**
* Get information about a withdrawal from * Get information about a withdrawal from
* a taler://withdraw URI by asking the bank. * a taler://withdraw URI by asking the bank.
*
* FIXME: Move into bank client.
*/ */
export async function getBankWithdrawalInfo( export async function getBankWithdrawalInfo(
ws: InternalWalletState, http: HttpRequestLibrary,
talerWithdrawUri: string, talerWithdrawUri: string,
): Promise<BankWithdrawDetails> { ): Promise<BankWithdrawDetails> {
const uriResult = parseWithdrawUri(talerWithdrawUri); const uriResult = parseWithdrawUri(talerWithdrawUri);
@ -283,7 +288,7 @@ export async function getBankWithdrawalInfo(
const configReqUrl = new URL("config", uriResult.bankIntegrationApiBaseUrl); const configReqUrl = new URL("config", uriResult.bankIntegrationApiBaseUrl);
const configResp = await ws.http.get(configReqUrl.href); const configResp = await http.get(configReqUrl.href);
const config = await readSuccessResponseJsonOrThrow( const config = await readSuccessResponseJsonOrThrow(
configResp, configResp,
codecForTalerConfigResponse(), codecForTalerConfigResponse(),
@ -309,7 +314,7 @@ export async function getBankWithdrawalInfo(
`withdrawal-operation/${uriResult.withdrawalOperationId}`, `withdrawal-operation/${uriResult.withdrawalOperationId}`,
uriResult.bankIntegrationApiBaseUrl, uriResult.bankIntegrationApiBaseUrl,
); );
const resp = await ws.http.get(reqUrl.href); const resp = await http.get(reqUrl.href);
const status = await readSuccessResponseJsonOrThrow( const status = await readSuccessResponseJsonOrThrow(
resp, resp,
codecForWithdrawOperationStatusResponse(), codecForWithdrawOperationStatusResponse(),
@ -1076,7 +1081,7 @@ export async function getWithdrawalDetailsForUri(
talerWithdrawUri: string, talerWithdrawUri: string,
): Promise<WithdrawUriInfoResponse> { ): Promise<WithdrawUriInfoResponse> {
logger.trace(`getting withdrawal details for URI ${talerWithdrawUri}`); logger.trace(`getting withdrawal details for URI ${talerWithdrawUri}`);
const info = await getBankWithdrawalInfo(ws, talerWithdrawUri); const info = await getBankWithdrawalInfo(ws.http, talerWithdrawUri);
logger.trace(`got bank info`); logger.trace(`got bank info`);
if (info.suggestedExchange) { if (info.suggestedExchange) {
// FIXME: right now the exchange gets permanently added, // FIXME: right now the exchange gets permanently added,

View File

@ -34,6 +34,7 @@ import {
timestampMax, timestampMax,
TalerErrorDetails, TalerErrorDetails,
Codec, Codec,
j2s,
} from "@gnu-taler/taler-util"; } from "@gnu-taler/taler-util";
import { TalerErrorCode } from "@gnu-taler/taler-util"; import { TalerErrorCode } from "@gnu-taler/taler-util";
@ -131,6 +132,11 @@ export async function readTalerErrorResponse(
const errJson = await httpResponse.json(); const errJson = await httpResponse.json();
const talerErrorCode = errJson.code; const talerErrorCode = errJson.code;
if (typeof talerErrorCode !== "number") { if (typeof talerErrorCode !== "number") {
logger.warn(
`malformed error response (status ${httpResponse.status}): ${j2s(
errJson,
)}`,
);
throw new OperationFailedError( throw new OperationFailedError(
makeErrorDetails( makeErrorDetails(
TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE, TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE,

View File

@ -0,0 +1,366 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2016-11-23 00:00+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
#: /home/dold/repos/taler/wallet-core/packages/taler-wallet-webextension/src/NavigationBar.tsx:86
#, c-format
msgid "Balance"
msgstr ""
#: /home/dold/repos/taler/wallet-core/packages/taler-wallet-webextension/src/NavigationBar.tsx:87
#, c-format
msgid "Pending"
msgstr ""
#: /home/dold/repos/taler/wallet-core/packages/taler-wallet-webextension/src/NavigationBar.tsx:88
#, c-format
msgid "Backup"
msgstr ""
#: /home/dold/repos/taler/wallet-core/packages/taler-wallet-webextension/src/NavigationBar.tsx:89
#, c-format
msgid "Settings"
msgstr ""
#: /home/dold/repos/taler/wallet-core/packages/taler-wallet-webextension/src/NavigationBar.tsx:90
#, c-format
msgid "Dev"
msgstr ""
#: /home/dold/repos/taler/wallet-core/packages/taler-wallet-webextension/src/wallet/BackupPage.tsx:127
#, c-format
msgid "Add provider"
msgstr ""
#: /home/dold/repos/taler/wallet-core/packages/taler-wallet-webextension/src/wallet/BackupPage.tsx:137
#, c-format
msgid "Sync all backups"
msgstr ""
#: /home/dold/repos/taler/wallet-core/packages/taler-wallet-webextension/src/wallet/BackupPage.tsx:139
#, c-format
msgid "Sync now"
msgstr ""
#: /home/dold/repos/taler/wallet-core/packages/taler-wallet-webextension/src/popup/BalancePage.tsx:79
#, c-format
msgid "You have no balance to show. Need some %1$s getting started?"
msgstr ""
#: /home/dold/repos/taler/wallet-core/packages/taler-wallet-webextension/src/wallet/ProviderAddPage.tsx:145
#, c-format
msgid "&lt; Back"
msgstr ""
#: /home/dold/repos/taler/wallet-core/packages/taler-wallet-webextension/src/wallet/ProviderAddPage.tsx:156
#, c-format
msgid "Next"
msgstr ""
#: /home/dold/repos/taler/wallet-core/packages/taler-wallet-webextension/src/wallet/ProviderAddPage.tsx:210
#, c-format
msgid "&lt; Back"
msgstr ""
#: /home/dold/repos/taler/wallet-core/packages/taler-wallet-webextension/src/wallet/ProviderAddPage.tsx:213
#, c-format
msgid "Add provider"
msgstr ""
#: /home/dold/repos/taler/wallet-core/packages/taler-wallet-webextension/src/wallet/ProviderDetailPage.tsx:57
#, c-format
msgid "Loading..."
msgstr ""
#: /home/dold/repos/taler/wallet-core/packages/taler-wallet-webextension/src/wallet/ProviderDetailPage.tsx:64
#, c-format
msgid "There was an error loading the provider detail for "%1$s""
msgstr ""
#: /home/dold/repos/taler/wallet-core/packages/taler-wallet-webextension/src/wallet/ProviderDetailPage.tsx:75
#, c-format
msgid "There is not known provider with url "%1$s". Redirecting back..."
msgstr ""
#: /home/dold/repos/taler/wallet-core/packages/taler-wallet-webextension/src/wallet/ProviderDetailPage.tsx:131
#, c-format
msgid "Back up"
msgstr ""
#: /home/dold/repos/taler/wallet-core/packages/taler-wallet-webextension/src/wallet/ProviderDetailPage.tsx:142
#, c-format
msgid "Extend"
msgstr ""
#: /home/dold/repos/taler/wallet-core/packages/taler-wallet-webextension/src/wallet/ProviderDetailPage.tsx:148
#, c-format
msgid ""
"terms has changed, extending the service will imply accepting the new terms of "
"service"
msgstr ""
#: /home/dold/repos/taler/wallet-core/packages/taler-wallet-webextension/src/wallet/ProviderDetailPage.tsx:158
#, c-format
msgid "old"
msgstr ""
#: /home/dold/repos/taler/wallet-core/packages/taler-wallet-webextension/src/wallet/ProviderDetailPage.tsx:162
#, c-format
msgid "new"
msgstr ""
#: /home/dold/repos/taler/wallet-core/packages/taler-wallet-webextension/src/wallet/ProviderDetailPage.tsx:169
#, c-format
msgid "fee"
msgstr ""
#: /home/dold/repos/taler/wallet-core/packages/taler-wallet-webextension/src/wallet/ProviderDetailPage.tsx:177
#, c-format
msgid "storage"
msgstr ""
#: /home/dold/repos/taler/wallet-core/packages/taler-wallet-webextension/src/wallet/ProviderDetailPage.tsx:190
#, c-format
msgid "&lt; back"
msgstr ""
#: /home/dold/repos/taler/wallet-core/packages/taler-wallet-webextension/src/wallet/ProviderDetailPage.tsx:194
#, c-format
msgid "remove provider"
msgstr ""
#: /home/dold/repos/taler/wallet-core/packages/taler-wallet-webextension/src/wallet/ProviderDetailPage.tsx:213
#, c-format
msgid "There is conflict with another backup from %1$s"
msgstr ""
#: /home/dold/repos/taler/wallet-core/packages/taler-wallet-webextension/src/wallet/ProviderDetailPage.tsx:228
#, c-format
msgid "Unknown backup problem: %1$s"
msgstr ""
#: /home/dold/repos/taler/wallet-core/packages/taler-wallet-webextension/src/wallet/ProviderDetailPage.tsx:247
#, c-format
msgid "service paid"
msgstr ""
#: /home/dold/repos/taler/wallet-core/packages/taler-wallet-webextension/src/popup/Settings.tsx:46
#, c-format
msgid "Permissions"
msgstr ""
#: /home/dold/repos/taler/wallet-core/packages/taler-wallet-webextension/src/cta/TermsOfServiceSection.tsx:37
#, c-format
msgid "Exchange doesn't have terms of service"
msgstr ""
#: /home/dold/repos/taler/wallet-core/packages/taler-wallet-webextension/src/cta/TermsOfServiceSection.tsx:49
#, c-format
msgid "Exchange doesn't have terms of service"
msgstr ""
#: /home/dold/repos/taler/wallet-core/packages/taler-wallet-webextension/src/cta/TermsOfServiceSection.tsx:56
#, c-format
msgid "Review exchange terms of service"
msgstr ""
#: /home/dold/repos/taler/wallet-core/packages/taler-wallet-webextension/src/cta/TermsOfServiceSection.tsx:63
#, c-format
msgid "Review new version of terms of service"
msgstr ""
#: /home/dold/repos/taler/wallet-core/packages/taler-wallet-webextension/src/cta/TermsOfServiceSection.tsx:75
#, c-format
msgid "Show terms of service"
msgstr ""
#: /home/dold/repos/taler/wallet-core/packages/taler-wallet-webextension/src/cta/TermsOfServiceSection.tsx:83
#, c-format
msgid "I accept the exchange terms of service"
msgstr ""
#: /home/dold/repos/taler/wallet-core/packages/taler-wallet-webextension/src/cta/TermsOfServiceSection.tsx:127
#, c-format
msgid "Hide terms of service"
msgstr ""
#: /home/dold/repos/taler/wallet-core/packages/taler-wallet-webextension/src/cta/TermsOfServiceSection.tsx:136
#, c-format
msgid "I accept the exchange terms of service"
msgstr ""
#: /home/dold/repos/taler/wallet-core/packages/taler-wallet-webextension/src/wallet/ExchangeAddConfirm.tsx:110
#, c-format
msgid "Cancel"
msgstr ""
#: /home/dold/repos/taler/wallet-core/packages/taler-wallet-webextension/src/wallet/ExchangeAddConfirm.tsx:114
#, c-format
msgid "Loading terms.."
msgstr ""
#: /home/dold/repos/taler/wallet-core/packages/taler-wallet-webextension/src/wallet/ExchangeAddConfirm.tsx:121
#, c-format
msgid "Add exchange"
msgstr ""
#: /home/dold/repos/taler/wallet-core/packages/taler-wallet-webextension/src/wallet/ExchangeAddConfirm.tsx:126
#, c-format
msgid "Add exchange"
msgstr ""
#: /home/dold/repos/taler/wallet-core/packages/taler-wallet-webextension/src/wallet/ExchangeAddConfirm.tsx:131
#, c-format
msgid "Add exchange anyway"
msgstr ""
#: /home/dold/repos/taler/wallet-core/packages/taler-wallet-webextension/src/wallet/ExchangeSetUrl.tsx:133
#, c-format
msgid "Cancel"
msgstr ""
#: /home/dold/repos/taler/wallet-core/packages/taler-wallet-webextension/src/wallet/ExchangeSetUrl.tsx:149
#, c-format
msgid "Next"
msgstr ""
#: /home/dold/repos/taler/wallet-core/packages/taler-wallet-webextension/src/wallet/BalancePage.tsx:83
#, c-format
msgid "You have no balance to show. Need some %1$s getting started?"
msgstr ""
#: /home/dold/repos/taler/wallet-core/packages/taler-wallet-webextension/src/wallet/CreateManualWithdraw.tsx:104
#, c-format
msgid "Add exchange"
msgstr ""
#: /home/dold/repos/taler/wallet-core/packages/taler-wallet-webextension/src/wallet/CreateManualWithdraw.tsx:144
#, c-format
msgid "Add exchange"
msgstr ""
#: /home/dold/repos/taler/wallet-core/packages/taler-wallet-webextension/src/wallet/Settings.tsx:84
#, c-format
msgid "Permissions"
msgstr ""
#: /home/dold/repos/taler/wallet-core/packages/taler-wallet-webextension/src/wallet/Settings.tsx:95
#, c-format
msgid "Known exchanges"
msgstr ""
#: /home/dold/repos/taler/wallet-core/packages/taler-wallet-webextension/src/wallet/Transaction.tsx:154
#, c-format
msgid "&lt; Back"
msgstr ""
#: /home/dold/repos/taler/wallet-core/packages/taler-wallet-webextension/src/wallet/Transaction.tsx:159
#, c-format
msgid "retry"
msgstr ""
#: /home/dold/repos/taler/wallet-core/packages/taler-wallet-webextension/src/wallet/Transaction.tsx:163
#, c-format
msgid "Forget"
msgstr ""
#: /home/dold/repos/taler/wallet-core/packages/taler-wallet-webextension/src/wallet/Transaction.tsx:194
#, c-format
msgid "Cancel"
msgstr ""
#: /home/dold/repos/taler/wallet-core/packages/taler-wallet-webextension/src/wallet/Transaction.tsx:198
#, c-format
msgid "Confirm"
msgstr ""
#: /home/dold/repos/taler/wallet-core/packages/taler-wallet-webextension/src/cta/Pay.tsx:211
#, c-format
msgid "Pay with a mobile phone"
msgstr ""
#: /home/dold/repos/taler/wallet-core/packages/taler-wallet-webextension/src/cta/Pay.tsx:211
#, c-format
msgid "Hide QR"
msgstr ""
#: /home/dold/repos/taler/wallet-core/packages/taler-wallet-webextension/src/cta/Pay.tsx:241
#, c-format
msgid "Pay"
msgstr ""
#: /home/dold/repos/taler/wallet-core/packages/taler-wallet-webextension/src/cta/Pay.tsx:265
#, c-format
msgid "Withdraw digital cash"
msgstr ""
#: /home/dold/repos/taler/wallet-core/packages/taler-wallet-webextension/src/cta/Pay.tsx:295
#, c-format
msgid "Digital cash payment"
msgstr ""
#: /home/dold/repos/taler/wallet-core/packages/taler-wallet-webextension/src/cta/Withdraw.tsx:101
#, c-format
msgid "Digital cash withdrawal"
msgstr ""
#: /home/dold/repos/taler/wallet-core/packages/taler-wallet-webextension/src/cta/Withdraw.tsx:149
#, c-format
msgid "Cancel exchange selection"
msgstr ""
#: /home/dold/repos/taler/wallet-core/packages/taler-wallet-webextension/src/cta/Withdraw.tsx:150
#, c-format
msgid "Confirm exchange selection"
msgstr ""
#: /home/dold/repos/taler/wallet-core/packages/taler-wallet-webextension/src/cta/Withdraw.tsx:155
#, c-format
msgid "Switch exchange"
msgstr ""
#: /home/dold/repos/taler/wallet-core/packages/taler-wallet-webextension/src/cta/Withdraw.tsx:174
#, c-format
msgid "Confirm withdrawal"
msgstr ""
#: /home/dold/repos/taler/wallet-core/packages/taler-wallet-webextension/src/cta/Withdraw.tsx:183
#, c-format
msgid "Withdraw anyway"
msgstr ""
#: /home/dold/repos/taler/wallet-core/packages/taler-wallet-webextension/src/cta/Withdraw.tsx:310
#, c-format
msgid "missing withdraw uri"
msgstr ""
#: /home/dold/repos/taler/wallet-core/packages/taler-wallet-webextension/src/cta/Deposit.tsx:119
#, c-format
msgid "Digital cash payment"
msgstr ""
#: /home/dold/repos/taler/wallet-core/packages/taler-wallet-webextension/src/cta/Deposit.tsx:133
#, c-format
msgid "Digital cash payment"
msgstr ""
#: /home/dold/repos/taler/wallet-core/packages/taler-wallet-webextension/src/cta/Deposit.tsx:186
#, c-format
msgid "Digital cash deposit"
msgstr ""

View File

@ -38,7 +38,7 @@ import {
RemoveBackupProviderRequest RemoveBackupProviderRequest
} from "@gnu-taler/taler-wallet-core"; } from "@gnu-taler/taler-wallet-core";
import { DepositFee } from "@gnu-taler/taler-wallet-core/src/operations/deposits"; import { DepositFee } from "@gnu-taler/taler-wallet-core/src/operations/deposits";
import { ExchangeWithdrawDetails } from "@gnu-taler/taler-wallet-core/src/operations/withdraw"; import type { ExchangeWithdrawDetails } from "@gnu-taler/taler-wallet-core/src/operations/withdraw";
import { MessageFromBackend } from "./wxBackend"; import { MessageFromBackend } from "./wxBackend";
/** /**