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 {
MERCHANT_TRACK_TRANSACTION = 1103,
WALLET_RESERVE_WITHDRAW = 1200,

View File

@ -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");

View File

@ -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 {

View File

@ -458,7 +458,7 @@ export interface TalerErrorDetails {
details: unknown;
}
export interface PlanchetCreationResult {
export interface WithdrawalPlanchet {
coinPub: string;
coinPriv: 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,
} 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;

View File

@ -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,

View File

@ -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

View File

@ -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,

View File

@ -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,

View File

@ -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.

View File

@ -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, {});

View File

@ -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;

View File

@ -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);

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.
*/
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";
/**

View File

@ -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

View File

@ -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";

View File

@ -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;

View File

@ -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,
];

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.
*/
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> {

View File

@ -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.
*/

View File

@ -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";

View File

@ -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,

View File

@ -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,

View File

@ -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.
*

View File

@ -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, {

View File

@ -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,

View File

@ -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),
},
},
);

View File

@ -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,

View File

@ -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,

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
} 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";
/**