wallet: towards db-less benchmarking, some refactoring
This commit is contained in:
parent
9e7ee06ad1
commit
332745862e
@ -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 {
|
||||
MERCHANT_TRACK_TRANSACTION = 1103,
|
||||
WALLET_RESERVE_WITHDRAW = 1200,
|
||||
|
@ -951,6 +951,15 @@ export interface MerchantPayResponse {
|
||||
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 {
|
||||
/**
|
||||
* Which of the kappa indices does the client not have to reveal.
|
||||
@ -1710,3 +1719,40 @@ export interface ExchangeRefreshRevealRequest {
|
||||
|
||||
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");
|
||||
|
@ -78,6 +78,9 @@ export namespace Duration {
|
||||
return Math.ceil(d.d_ms / 1000 / 60 / 60 / 24 / 365);
|
||||
}
|
||||
export const fromSpec = durationFromSpec;
|
||||
export function getForever(): Duration {
|
||||
return { d_ms: "forever" };
|
||||
}
|
||||
}
|
||||
|
||||
export namespace Timestamp {
|
||||
|
@ -458,7 +458,7 @@ export interface TalerErrorDetails {
|
||||
details: unknown;
|
||||
}
|
||||
|
||||
export interface PlanchetCreationResult {
|
||||
export interface WithdrawalPlanchet {
|
||||
coinPub: string;
|
||||
coinPriv: string;
|
||||
reservePub: string;
|
||||
|
106
packages/taler-wallet-cli/src/bench2.ts
Normal file
106
packages/taler-wallet-cli/src/bench2.ts
Normal 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");
|
@ -45,6 +45,9 @@ import {
|
||||
MerchantInstancesResponse,
|
||||
} from "./merchantApiTypes";
|
||||
import {
|
||||
BankServiceHandle,
|
||||
HarnessExchangeBankAccount,
|
||||
NodeHttpLib,
|
||||
openPromise,
|
||||
OperationFailedError,
|
||||
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 {
|
||||
proc: ProcessWrapper | undefined;
|
||||
|
||||
@ -640,10 +485,12 @@ class BankServiceBase {
|
||||
* Work in progress. The key point is that both Sandbox and Nexus
|
||||
* will be configured and started by this class.
|
||||
*/
|
||||
class EufinBankService extends BankServiceBase implements BankServiceInterface {
|
||||
class EufinBankService extends BankServiceBase implements BankServiceHandle {
|
||||
sandboxProc: ProcessWrapper | undefined;
|
||||
nexusProc: ProcessWrapper | undefined;
|
||||
|
||||
http = new NodeHttpLib();
|
||||
|
||||
static async create(
|
||||
gc: GlobalTestState,
|
||||
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;
|
||||
|
||||
http = new NodeHttpLib();
|
||||
|
||||
static async create(
|
||||
gc: GlobalTestState,
|
||||
bc: BankConfig,
|
||||
@ -955,6 +804,7 @@ class PybankService extends BankServiceBase implements BankServiceInterface {
|
||||
const config = Configuration.load(this.configFile);
|
||||
config.setString("bank", "suggested_exchange", e.baseUrl);
|
||||
config.setString("bank", "suggested_exchange_payto", exchangePayto);
|
||||
config.write(this.configFile);
|
||||
}
|
||||
|
||||
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 {
|
||||
name: string;
|
||||
currency: string;
|
||||
|
@ -30,22 +30,19 @@ import {
|
||||
Duration,
|
||||
PreparePayResultType,
|
||||
} 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 {
|
||||
FaultInjectedExchangeService,
|
||||
FaultInjectedMerchantService,
|
||||
} from "./faultInjection.js";
|
||||
import {
|
||||
BankAccessApi,
|
||||
BankApi,
|
||||
BankService,
|
||||
DbInfo,
|
||||
ExchangeService,
|
||||
ExchangeServiceInterface,
|
||||
getPayto,
|
||||
GlobalTestState,
|
||||
HarnessExchangeBankAccount,
|
||||
MerchantPrivateApi,
|
||||
MerchantService,
|
||||
MerchantServiceInterface,
|
||||
|
@ -24,13 +24,15 @@ import {
|
||||
setupDb,
|
||||
BankService,
|
||||
MerchantService,
|
||||
BankApi,
|
||||
BankAccessApi,
|
||||
CreditDebitIndicator,
|
||||
getPayto
|
||||
getPayto,
|
||||
} from "../harness/harness.js";
|
||||
import { createEddsaKeyPair, encodeCrock } from "@gnu-taler/taler-util";
|
||||
import { defaultCoinConfig } from "../harness/denomStructures";
|
||||
import {
|
||||
BankApi,
|
||||
BankAccessApi,
|
||||
CreditDebitIndicator,
|
||||
} from "@gnu-taler/taler-wallet-core";
|
||||
|
||||
/**
|
||||
* Run test for basic, bank-integrated withdrawal.
|
||||
@ -97,8 +99,6 @@ export async function runBankApiTest(t: GlobalTestState) {
|
||||
|
||||
console.log("setup done!");
|
||||
|
||||
const wallet = new WalletCli(t);
|
||||
|
||||
const bankUser = await BankApi.registerAccount(bank, "user1", "pw1");
|
||||
|
||||
// Make sure that registering twice results in a 409 Conflict
|
||||
|
@ -24,11 +24,13 @@ import {
|
||||
BankService,
|
||||
ExchangeService,
|
||||
MerchantService,
|
||||
getPayto,
|
||||
} from "../harness/harness.js";
|
||||
import {
|
||||
WalletApiOperation,
|
||||
BankApi,
|
||||
BankAccessApi,
|
||||
getPayto
|
||||
} from "../harness/harness.js";
|
||||
import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
|
||||
} from "@gnu-taler/taler-wallet-core";
|
||||
import {
|
||||
ExchangesListRespose,
|
||||
URL,
|
||||
|
@ -19,15 +19,16 @@
|
||||
*/
|
||||
import {
|
||||
ContractTerms,
|
||||
CoreApiResponse,
|
||||
getTimestampNow,
|
||||
timestampTruncateToSecond,
|
||||
} 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 {
|
||||
DbInfo,
|
||||
HarnessExchangeBankAccount,
|
||||
ExchangeService,
|
||||
GlobalTestState,
|
||||
MerchantService,
|
||||
@ -233,13 +234,8 @@ export async function createLibeufinTestEnvironment(
|
||||
export async function runLibeufinBasicTest(t: GlobalTestState) {
|
||||
// Set up test environment
|
||||
|
||||
const {
|
||||
wallet,
|
||||
exchange,
|
||||
merchant,
|
||||
libeufinSandbox,
|
||||
libeufinNexus,
|
||||
} = await createLibeufinTestEnvironment(t);
|
||||
const { wallet, exchange, merchant, libeufinSandbox, libeufinNexus } =
|
||||
await createLibeufinTestEnvironment(t);
|
||||
|
||||
await wallet.client.call(WalletApiOperation.AddExchange, {
|
||||
exchangeBaseUrl: exchange.baseUrl,
|
||||
|
@ -20,25 +20,30 @@
|
||||
import {
|
||||
GlobalTestState,
|
||||
MerchantPrivateApi,
|
||||
BankServiceInterface,
|
||||
MerchantServiceInterface,
|
||||
WalletCli,
|
||||
ExchangeServiceInterface,
|
||||
} from "../harness/harness.js";
|
||||
import { createSimpleTestkudosEnvironment, withdrawViaBank } from "../harness/helpers.js";
|
||||
import {
|
||||
createSimpleTestkudosEnvironment,
|
||||
withdrawViaBank,
|
||||
} from "../harness/helpers.js";
|
||||
import {
|
||||
URL,
|
||||
durationFromSpec,
|
||||
PreparePayResultType,
|
||||
} from "@gnu-taler/taler-util";
|
||||
import axios from "axios";
|
||||
import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
|
||||
import {
|
||||
WalletApiOperation,
|
||||
BankServiceHandle,
|
||||
} from "@gnu-taler/taler-wallet-core";
|
||||
|
||||
async function testRefundApiWithFulfillmentUrl(
|
||||
t: GlobalTestState,
|
||||
env: {
|
||||
merchant: MerchantServiceInterface;
|
||||
bank: BankServiceInterface;
|
||||
bank: BankServiceHandle;
|
||||
wallet: WalletCli;
|
||||
exchange: ExchangeServiceInterface;
|
||||
},
|
||||
@ -152,7 +157,7 @@ async function testRefundApiWithFulfillmentMessage(
|
||||
t: GlobalTestState,
|
||||
env: {
|
||||
merchant: MerchantServiceInterface;
|
||||
bank: BankServiceInterface;
|
||||
bank: BankServiceHandle;
|
||||
wallet: WalletCli;
|
||||
exchange: ExchangeServiceInterface;
|
||||
},
|
||||
@ -267,12 +272,8 @@ async function testRefundApiWithFulfillmentMessage(
|
||||
export async function runMerchantRefundApiTest(t: GlobalTestState) {
|
||||
// Set up test environment
|
||||
|
||||
const {
|
||||
wallet,
|
||||
bank,
|
||||
exchange,
|
||||
merchant,
|
||||
} = await createSimpleTestkudosEnvironment(t);
|
||||
const { wallet, bank, exchange, merchant } =
|
||||
await createSimpleTestkudosEnvironment(t);
|
||||
|
||||
// Withdraw digital cash into the wallet.
|
||||
|
||||
|
@ -29,9 +29,7 @@ import {
|
||||
BankService,
|
||||
WalletCli,
|
||||
MerchantPrivateApi,
|
||||
BankApi,
|
||||
BankAccessApi,
|
||||
getPayto
|
||||
getPayto,
|
||||
} from "../harness/harness.js";
|
||||
import {
|
||||
FaultInjectedExchangeService,
|
||||
@ -40,7 +38,11 @@ import {
|
||||
} from "../harness/faultInjection";
|
||||
import { CoreApiResponse } from "@gnu-taler/taler-util";
|
||||
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.
|
||||
@ -146,7 +148,6 @@ export async function runPaymentFaultTest(t: GlobalTestState) {
|
||||
|
||||
await wallet.runUntilDone();
|
||||
|
||||
|
||||
// Check balance
|
||||
|
||||
await wallet.client.call(WalletApiOperation.GetBalances, {});
|
||||
|
@ -17,31 +17,33 @@
|
||||
/**
|
||||
* Imports.
|
||||
*/
|
||||
import { GlobalTestState, WalletCli } from "../harness/harness.js";
|
||||
import { makeTestPayment } from "../harness/helpers.js";
|
||||
import {
|
||||
GlobalTestState,
|
||||
WalletApiOperation,
|
||||
BankApi,
|
||||
WalletCli,
|
||||
BankAccessApi
|
||||
} from "../harness/harness.js";
|
||||
import {
|
||||
makeTestPayment,
|
||||
} from "../harness/helpers.js";
|
||||
import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
|
||||
BankAccessApi,
|
||||
BankServiceHandle,
|
||||
NodeHttpLib,
|
||||
} from "@gnu-taler/taler-wallet-core";
|
||||
|
||||
/**
|
||||
* Run test for basic, bank-integrated withdrawal and payment.
|
||||
*/
|
||||
export async function runPaymentDemoTest(t: GlobalTestState) {
|
||||
|
||||
// Withdraw digital cash into the wallet.
|
||||
let bankInterface = {
|
||||
let bankInterface: BankServiceHandle = {
|
||||
baseUrl: "https://bank.demo.taler.net/",
|
||||
port: 0 // unused.
|
||||
http: new NodeHttpLib(),
|
||||
};
|
||||
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, {
|
||||
talerWithdrawUri: wop.taler_withdraw_uri,
|
||||
});
|
||||
@ -60,7 +62,10 @@ export async function runPaymentDemoTest(t: GlobalTestState) {
|
||||
});
|
||||
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);
|
||||
|
||||
const order = {
|
||||
@ -70,7 +75,7 @@ export async function runPaymentDemoTest(t: GlobalTestState) {
|
||||
};
|
||||
|
||||
let merchant = {
|
||||
makeInstanceBaseUrl: function(instanceName?: string) {
|
||||
makeInstanceBaseUrl: function (instanceName?: string) {
|
||||
return "https://backend.demo.taler.net/instances/donations/";
|
||||
},
|
||||
port: 0,
|
||||
@ -82,17 +87,26 @@ export async function runPaymentDemoTest(t: GlobalTestState) {
|
||||
await makeTestPayment(
|
||||
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();
|
||||
|
||||
let balanceAfter = await wallet.client.call(WalletApiOperation.GetBalances, {});
|
||||
let balanceAfter = await wallet.client.call(
|
||||
WalletApiOperation.GetBalances,
|
||||
{},
|
||||
);
|
||||
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;
|
||||
|
@ -17,8 +17,12 @@
|
||||
/**
|
||||
* Imports.
|
||||
*/
|
||||
import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
|
||||
import { GlobalTestState, MerchantPrivateApi, BankApi, getWireMethod } from "../harness/harness.js";
|
||||
import { WalletApiOperation, BankApi } from "@gnu-taler/taler-wallet-core";
|
||||
import {
|
||||
GlobalTestState,
|
||||
MerchantPrivateApi,
|
||||
getWireMethod,
|
||||
} from "../harness/harness.js";
|
||||
import { createSimpleTestkudosEnvironment } from "../harness/helpers.js";
|
||||
|
||||
/**
|
||||
@ -27,13 +31,8 @@ import { createSimpleTestkudosEnvironment } from "../harness/helpers.js";
|
||||
export async function runTippingTest(t: GlobalTestState) {
|
||||
// Set up test environment
|
||||
|
||||
const {
|
||||
wallet,
|
||||
bank,
|
||||
exchange,
|
||||
merchant,
|
||||
exchangeBankAccount,
|
||||
} = await createSimpleTestkudosEnvironment(t);
|
||||
const { wallet, bank, exchange, merchant, exchangeBankAccount } =
|
||||
await createSimpleTestkudosEnvironment(t);
|
||||
|
||||
const mbu = await BankApi.createRandomBankUser(bank);
|
||||
|
||||
|
@ -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"];
|
@ -18,8 +18,12 @@
|
||||
* Imports.
|
||||
*/
|
||||
import { TalerErrorCode } from "@gnu-taler/taler-util";
|
||||
import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
|
||||
import { GlobalTestState, BankApi, BankAccessApi } from "../harness/harness.js";
|
||||
import {
|
||||
WalletApiOperation,
|
||||
BankApi,
|
||||
BankAccessApi,
|
||||
} from "@gnu-taler/taler-wallet-core";
|
||||
import { GlobalTestState } from "../harness/harness.js";
|
||||
import { createSimpleTestkudosEnvironment } from "../harness/helpers.js";
|
||||
|
||||
/**
|
||||
|
@ -17,10 +17,13 @@
|
||||
/**
|
||||
* Imports.
|
||||
*/
|
||||
import { GlobalTestState, BankApi, BankAccessApi } from "../harness/harness.js";
|
||||
import { GlobalTestState } from "../harness/harness.js";
|
||||
import { createSimpleTestkudosEnvironment } from "../harness/helpers.js";
|
||||
import { codecForBalancesResponse } from "@gnu-taler/taler-util";
|
||||
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.
|
||||
@ -41,18 +44,24 @@ export async function runWithdrawalBankIntegratedTest(t: GlobalTestState) {
|
||||
|
||||
// Hand it to the wallet
|
||||
|
||||
const r1 = await wallet.client.call(WalletApiOperation.GetWithdrawalDetailsForUri, {
|
||||
talerWithdrawUri: wop.taler_withdraw_uri,
|
||||
});
|
||||
const r1 = await wallet.client.call(
|
||||
WalletApiOperation.GetWithdrawalDetailsForUri,
|
||||
{
|
||||
talerWithdrawUri: wop.taler_withdraw_uri,
|
||||
},
|
||||
);
|
||||
|
||||
await wallet.runPending();
|
||||
|
||||
// Withdraw
|
||||
|
||||
const r2 = await wallet.client.call(WalletApiOperation.AcceptBankIntegratedWithdrawal, {
|
||||
exchangeBaseUrl: exchange.baseUrl,
|
||||
talerWithdrawUri: wop.taler_withdraw_uri,
|
||||
});
|
||||
const r2 = await wallet.client.call(
|
||||
WalletApiOperation.AcceptBankIntegratedWithdrawal,
|
||||
{
|
||||
exchangeBaseUrl: exchange.baseUrl,
|
||||
talerWithdrawUri: wop.taler_withdraw_uri,
|
||||
},
|
||||
);
|
||||
await wallet.runPending();
|
||||
|
||||
// Confirm it
|
||||
|
@ -19,13 +19,11 @@
|
||||
*/
|
||||
import {
|
||||
GlobalTestState,
|
||||
BankApi,
|
||||
WalletCli,
|
||||
setupDb,
|
||||
ExchangeService,
|
||||
FakeBankService,
|
||||
} from "../harness/harness.js";
|
||||
import { createSimpleTestkudosEnvironment } from "../harness/helpers.js";
|
||||
import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
|
||||
import { CoinConfig, defaultCoinConfig } from "../harness/denomStructures.js";
|
||||
import { URL } from "@gnu-taler/taler-util";
|
||||
|
@ -17,9 +17,9 @@
|
||||
/**
|
||||
* Imports.
|
||||
*/
|
||||
import { GlobalTestState, BankApi } from "../harness/harness.js";
|
||||
import { GlobalTestState } from "../harness/harness.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.
|
||||
@ -27,12 +27,8 @@ import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
|
||||
export async function runTestWithdrawalManualTest(t: GlobalTestState) {
|
||||
// Set up test environment
|
||||
|
||||
const {
|
||||
wallet,
|
||||
bank,
|
||||
exchange,
|
||||
exchangeBankAccount,
|
||||
} = await createSimpleTestkudosEnvironment(t);
|
||||
const { wallet, bank, exchange, exchangeBankAccount } =
|
||||
await createSimpleTestkudosEnvironment(t);
|
||||
|
||||
// Create a withdrawal operation
|
||||
|
||||
@ -42,11 +38,13 @@ export async function runTestWithdrawalManualTest(t: GlobalTestState) {
|
||||
exchangeBaseUrl: exchange.baseUrl,
|
||||
});
|
||||
|
||||
|
||||
const wres = await wallet.client.call(WalletApiOperation.AcceptManualWithdrawal, {
|
||||
exchangeBaseUrl: exchange.baseUrl,
|
||||
amount: "TESTKUDOS:10",
|
||||
});
|
||||
const wres = await wallet.client.call(
|
||||
WalletApiOperation.AcceptManualWithdrawal,
|
||||
{
|
||||
exchangeBaseUrl: exchange.baseUrl,
|
||||
amount: "TESTKUDOS:10",
|
||||
},
|
||||
);
|
||||
|
||||
const reservePub: string = wres.reservePub;
|
||||
|
||||
|
@ -87,6 +87,7 @@ import { runExchangeTimetravelTest } from "./test-exchange-timetravel.js";
|
||||
import { runDenomUnofferedTest } from "./test-denom-unoffered.js";
|
||||
import { runWithdrawalFakebankTest } from "./test-withdrawal-fakebank.js";
|
||||
import { runClauseSchnorrTest } from "./test-clause-schnorr.js";
|
||||
import { runWalletDblessTest } from "./test-wallet-dbless.js";
|
||||
|
||||
/**
|
||||
* Test runner.
|
||||
@ -162,6 +163,7 @@ const allTests: TestMainFunction[] = [
|
||||
runWalletBackupBasicTest,
|
||||
runWalletBackupDoublespendTest,
|
||||
runWallettestingTest,
|
||||
runWalletDblessTest,
|
||||
runWithdrawalAbortBankTest,
|
||||
runWithdrawalBankIntegratedTest,
|
||||
];
|
||||
|
249
packages/taler-wallet-core/src/bank-api-client.ts
Normal file
249
packages/taler-wallet-core/src/bank-api-client.ts
Normal 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(),
|
||||
);
|
||||
}
|
||||
}
|
@ -22,20 +22,22 @@
|
||||
/**
|
||||
* Imports.
|
||||
*/
|
||||
import { CoinRecord, DenominationRecord, WireFee } from "../../db.js";
|
||||
import { DenominationRecord, WireFee } from "../../db.js";
|
||||
|
||||
import { CryptoWorker } from "./cryptoWorkerInterface.js";
|
||||
|
||||
import {
|
||||
BlindedDenominationSignature,
|
||||
CoinDepositPermission,
|
||||
CoinEnvelope,
|
||||
RecoupRefreshRequest,
|
||||
RecoupRequest,
|
||||
UnblindedSignature,
|
||||
} from "@gnu-taler/taler-util";
|
||||
|
||||
import {
|
||||
BenchmarkResult,
|
||||
PlanchetCreationResult,
|
||||
WithdrawalPlanchet,
|
||||
PlanchetCreationRequest,
|
||||
DepositInfo,
|
||||
MakeSyncSignatureRequest,
|
||||
@ -324,10 +326,19 @@ export class CryptoApi {
|
||||
return p;
|
||||
}
|
||||
|
||||
createPlanchet(
|
||||
req: PlanchetCreationRequest,
|
||||
): Promise<PlanchetCreationResult> {
|
||||
return this.doRpc<PlanchetCreationResult>("createPlanchet", 1, req);
|
||||
createPlanchet(req: PlanchetCreationRequest): Promise<WithdrawalPlanchet> {
|
||||
return this.doRpc<WithdrawalPlanchet>("createPlanchet", 1, req);
|
||||
}
|
||||
|
||||
unblindDenominationSignature(req: {
|
||||
planchet: WithdrawalPlanchet;
|
||||
evSig: BlindedDenominationSignature;
|
||||
}): Promise<UnblindedSignature> {
|
||||
return this.doRpc<UnblindedSignature>(
|
||||
"unblindDenominationSignature",
|
||||
1,
|
||||
req,
|
||||
);
|
||||
}
|
||||
|
||||
createTipPlanchet(req: DeriveTipRequest): Promise<DerivedTipPlanchet> {
|
||||
|
@ -53,7 +53,7 @@ import {
|
||||
Logger,
|
||||
MakeSyncSignatureRequest,
|
||||
PlanchetCreationRequest,
|
||||
PlanchetCreationResult,
|
||||
WithdrawalPlanchet,
|
||||
randomBytes,
|
||||
RecoupRefreshRequest,
|
||||
RecoupRequest,
|
||||
@ -70,6 +70,9 @@ import {
|
||||
Timestamp,
|
||||
timestampTruncateToSecond,
|
||||
typedArrayConcat,
|
||||
BlindedDenominationSignature,
|
||||
RsaUnblindedSignature,
|
||||
UnblindedSignature,
|
||||
} from "@gnu-taler/taler-util";
|
||||
import bigint from "big-integer";
|
||||
import { DenominationRecord, WireFee } from "../../db.js";
|
||||
@ -169,7 +172,7 @@ export class CryptoImplementation {
|
||||
*/
|
||||
async createPlanchet(
|
||||
req: PlanchetCreationRequest,
|
||||
): Promise<PlanchetCreationResult> {
|
||||
): Promise<WithdrawalPlanchet> {
|
||||
const denomPub = req.denomPub;
|
||||
if (denomPub.cipher === DenomKeyType.Rsa) {
|
||||
const reservePub = decodeCrock(req.reservePub);
|
||||
@ -200,7 +203,7 @@ export class CryptoImplementation {
|
||||
priv: req.reservePriv,
|
||||
});
|
||||
|
||||
const planchet: PlanchetCreationResult = {
|
||||
const planchet: WithdrawalPlanchet = {
|
||||
blindingKey: encodeCrock(derivedPlanchet.bks),
|
||||
coinEv,
|
||||
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.
|
||||
*/
|
||||
|
@ -36,7 +36,7 @@ export * from "./db-utils.js";
|
||||
export { CryptoImplementation } from "./crypto/workers/cryptoImplementation.js";
|
||||
export type { CryptoWorker } from "./crypto/workers/cryptoWorkerInterface.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";
|
||||
|
||||
@ -47,3 +47,12 @@ export * from "./wallet.js";
|
||||
|
||||
export * from "./operations/backup/index.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";
|
||||
|
@ -20,6 +20,7 @@ import {
|
||||
buildCodecForObject,
|
||||
canonicalJson,
|
||||
Codec,
|
||||
codecForDepositSuccess,
|
||||
codecForString,
|
||||
codecForTimestamp,
|
||||
codecOptional,
|
||||
@ -32,6 +33,7 @@ import {
|
||||
GetFeeForDepositRequest,
|
||||
getRandomBytes,
|
||||
getTimestampNow,
|
||||
hashWire,
|
||||
Logger,
|
||||
NotificationType,
|
||||
parsePaytoUri,
|
||||
@ -57,7 +59,6 @@ import {
|
||||
generateDepositPermissions,
|
||||
getCandidatePayCoins,
|
||||
getTotalPaymentCost,
|
||||
hashWire,
|
||||
} from "./pay.js";
|
||||
import { getTotalRefreshCost } from "./refresh.js";
|
||||
|
||||
@ -66,43 +67,6 @@ import { getTotalRefreshCost } from "./refresh.js";
|
||||
*/
|
||||
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(
|
||||
ws: InternalWalletState,
|
||||
depositGroupId: string,
|
||||
@ -202,7 +166,6 @@ async function processDepositGroupImpl(
|
||||
}
|
||||
const perm = depositPermissions[i];
|
||||
let requestBody: any;
|
||||
logger.info("creating v10 deposit request");
|
||||
requestBody = {
|
||||
contribution: Amounts.stringify(perm.contribution),
|
||||
merchant_payto_uri: depositGroup.wire.payto_uri,
|
||||
|
@ -43,6 +43,7 @@ import {
|
||||
codecForAny,
|
||||
DenominationPubKey,
|
||||
DenomKeyType,
|
||||
ExchangeKeysJson,
|
||||
} from "@gnu-taler/taler-util";
|
||||
import { decodeCrock, encodeCrock, hash } from "@gnu-taler/taler-util";
|
||||
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.
|
||||
*
|
||||
* @param exchangeBaseUrl Exchange base URL, assumed to be already normalized.
|
||||
*/
|
||||
async function downloadExchangeWithWireInfo(
|
||||
async function downloadExchangeWireInfo(
|
||||
exchangeBaseUrl: string,
|
||||
http: HttpRequestLibrary,
|
||||
timeout: Duration,
|
||||
@ -374,7 +400,7 @@ interface ExchangeKeysDownloadResult {
|
||||
/**
|
||||
* Download and validate an exchange's /keys data.
|
||||
*/
|
||||
async function downloadKeysInfo(
|
||||
async function downloadExchangeKeysInfo(
|
||||
baseUrl: string,
|
||||
http: HttpRequestLibrary,
|
||||
timeout: Duration,
|
||||
@ -526,10 +552,10 @@ async function updateExchangeFromUrlImpl(
|
||||
|
||||
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");
|
||||
const wireInfoDownload = await downloadExchangeWithWireInfo(
|
||||
const wireInfoDownload = await downloadExchangeWireInfo(
|
||||
baseUrl,
|
||||
ws.http,
|
||||
timeout,
|
||||
|
@ -112,19 +112,6 @@ import { createRefreshGroup, getTotalRefreshCost } from "./refresh.js";
|
||||
*/
|
||||
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.
|
||||
*
|
||||
|
@ -17,6 +17,7 @@
|
||||
import {
|
||||
DenomKeyType,
|
||||
encodeCrock,
|
||||
ExchangeMeltRequest,
|
||||
ExchangeProtocolVersion,
|
||||
ExchangeRefreshRevealRequest,
|
||||
getRandomBytes,
|
||||
@ -394,17 +395,14 @@ async function refreshMelt(
|
||||
`coins/${oldCoin.coinPub}/melt`,
|
||||
oldCoin.exchangeBaseUrl,
|
||||
);
|
||||
let meltReqBody: any;
|
||||
if (oldDenom.denomPub.cipher === DenomKeyType.Rsa) {
|
||||
meltReqBody = {
|
||||
coin_pub: oldCoin.coinPub,
|
||||
confirm_sig: derived.confirmSig,
|
||||
denom_pub_hash: oldCoin.denomPubHash,
|
||||
denom_sig: oldCoin.denomSig,
|
||||
rc: derived.hash,
|
||||
value_with_fee: Amounts.stringify(derived.meltValueWithFee),
|
||||
};
|
||||
}
|
||||
const meltReqBody: ExchangeMeltRequest = {
|
||||
coin_pub: oldCoin.coinPub,
|
||||
confirm_sig: derived.confirmSig,
|
||||
denom_pub_hash: oldCoin.denomPubHash,
|
||||
denom_sig: oldCoin.denomSig,
|
||||
rc: derived.hash,
|
||||
value_with_fee: Amounts.stringify(derived.meltValueWithFee),
|
||||
};
|
||||
|
||||
const resp = await ws.runSequentialized([EXCHANGE_COINS_LOCK], async () => {
|
||||
return await ws.http.postJson(reqUrl.href, meltReqBody, {
|
||||
|
@ -780,7 +780,7 @@ export async function createTalerWithdrawReserve(
|
||||
selectedExchange: string,
|
||||
): Promise<AcceptWithdrawalResponse> {
|
||||
await updateExchangeFromUrl(ws, selectedExchange);
|
||||
const withdrawInfo = await getBankWithdrawalInfo(ws, talerWithdrawUri);
|
||||
const withdrawInfo = await getBankWithdrawalInfo(ws.http, talerWithdrawUri);
|
||||
const exchangePaytoUri = await getExchangePaytoUri(
|
||||
ws,
|
||||
selectedExchange,
|
||||
|
@ -74,7 +74,7 @@ function makeId(length: number): string {
|
||||
/**
|
||||
* 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 authEncoded: string = Buffer.from(auth).toString("base64");
|
||||
return `Basic ${authEncoded}`;
|
||||
@ -89,7 +89,7 @@ export async function withdrawTestBalance(
|
||||
const bankUser = await registerRandomBankUser(ws.http, bankBaseUrl);
|
||||
logger.trace(`Registered bank user ${JSON.stringify(bankUser)}`);
|
||||
|
||||
const wresp = await createBankWithdrawalUri(
|
||||
const wresp = await createDemoBankWithdrawalUri(
|
||||
ws.http,
|
||||
bankBaseUrl,
|
||||
bankUser,
|
||||
@ -119,7 +119,11 @@ function getMerchantAuthHeader(m: MerchantBackendInfo): Record<string, string> {
|
||||
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,
|
||||
bankBaseUrl: string,
|
||||
bankUser: BankUser,
|
||||
@ -136,7 +140,7 @@ async function createBankWithdrawalUri(
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
Authorization: makeAuth(bankUser.username, bankUser.password),
|
||||
Authorization: makeBasicAuthHeader(bankUser.username, bankUser.password),
|
||||
},
|
||||
},
|
||||
);
|
||||
@ -159,7 +163,7 @@ async function confirmBankWithdrawalUri(
|
||||
{},
|
||||
{
|
||||
headers: {
|
||||
Authorization: makeAuth(bankUser.username, bankUser.password),
|
||||
Authorization: makeBasicAuthHeader(bankUser.username, bankUser.password),
|
||||
},
|
||||
},
|
||||
);
|
||||
|
@ -59,7 +59,10 @@ import {
|
||||
WithdrawalGroupRecord,
|
||||
} from "../db.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 {
|
||||
guardOperationException,
|
||||
@ -271,9 +274,11 @@ export function selectWithdrawalDenominations(
|
||||
/**
|
||||
* Get information about a withdrawal from
|
||||
* a taler://withdraw URI by asking the bank.
|
||||
*
|
||||
* FIXME: Move into bank client.
|
||||
*/
|
||||
export async function getBankWithdrawalInfo(
|
||||
ws: InternalWalletState,
|
||||
http: HttpRequestLibrary,
|
||||
talerWithdrawUri: string,
|
||||
): Promise<BankWithdrawDetails> {
|
||||
const uriResult = parseWithdrawUri(talerWithdrawUri);
|
||||
@ -283,7 +288,7 @@ export async function getBankWithdrawalInfo(
|
||||
|
||||
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(
|
||||
configResp,
|
||||
codecForTalerConfigResponse(),
|
||||
@ -309,7 +314,7 @@ export async function getBankWithdrawalInfo(
|
||||
`withdrawal-operation/${uriResult.withdrawalOperationId}`,
|
||||
uriResult.bankIntegrationApiBaseUrl,
|
||||
);
|
||||
const resp = await ws.http.get(reqUrl.href);
|
||||
const resp = await http.get(reqUrl.href);
|
||||
const status = await readSuccessResponseJsonOrThrow(
|
||||
resp,
|
||||
codecForWithdrawOperationStatusResponse(),
|
||||
@ -1076,7 +1081,7 @@ export async function getWithdrawalDetailsForUri(
|
||||
talerWithdrawUri: string,
|
||||
): Promise<WithdrawUriInfoResponse> {
|
||||
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`);
|
||||
if (info.suggestedExchange) {
|
||||
// FIXME: right now the exchange gets permanently added,
|
||||
|
@ -34,6 +34,7 @@ import {
|
||||
timestampMax,
|
||||
TalerErrorDetails,
|
||||
Codec,
|
||||
j2s,
|
||||
} 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 talerErrorCode = errJson.code;
|
||||
if (typeof talerErrorCode !== "number") {
|
||||
logger.warn(
|
||||
`malformed error response (status ${httpResponse.status}): ${j2s(
|
||||
errJson,
|
||||
)}`,
|
||||
);
|
||||
throw new OperationFailedError(
|
||||
makeErrorDetails(
|
||||
TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE,
|
||||
|
@ -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 "< 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 "< 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 "< 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 "< 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 ""
|
||||
|
@ -38,7 +38,7 @@ import {
|
||||
RemoveBackupProviderRequest
|
||||
} from "@gnu-taler/taler-wallet-core";
|
||||
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";
|
||||
|
||||
/**
|
||||
|
Loading…
Reference in New Issue
Block a user