wallet: tipping protocol change / merchant version info

This commit is contained in:
Florian Dold 2021-11-23 23:51:12 +01:00
parent 829a59e1a2
commit ae8af3f27c
No known key found for this signature in database
GPG Key ID: D2E4F00F29D02A4B
15 changed files with 307 additions and 131 deletions

View File

@ -1102,6 +1102,8 @@ export interface BackupExchange {
currency: string;
protocol_version_range: string;
/**
* Time when the pointer to the exchange details
* was last updated.

View File

@ -14,7 +14,7 @@
TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
import * as LibtoolVersion from "./libtool-version.js";
import { LibtoolVersion } from "./libtool-version.js";
import test from "ava";

View File

@ -40,49 +40,51 @@ interface Version {
age: number;
}
/**
* Compare two libtool-style version strings.
*/
export function compare(
me: string,
other: string,
): VersionMatchResult | undefined {
const meVer = parseVersion(me);
const otherVer = parseVersion(other);
export namespace LibtoolVersion {
/**
* Compare two libtool-style version strings.
*/
export function compare(
me: string,
other: string,
): VersionMatchResult | undefined {
const meVer = parseVersion(me);
const otherVer = parseVersion(other);
if (!(meVer && otherVer)) {
return undefined;
if (!(meVer && otherVer)) {
return undefined;
}
const compatible =
meVer.current - meVer.age <= otherVer.current &&
meVer.current >= otherVer.current - otherVer.age;
const currentCmp = Math.sign(meVer.current - otherVer.current);
return { compatible, currentCmp };
}
const compatible =
meVer.current - meVer.age <= otherVer.current &&
meVer.current >= otherVer.current - otherVer.age;
function parseVersion(v: string): Version | undefined {
const [currentStr, revisionStr, ageStr, ...rest] = v.split(":");
if (rest.length !== 0) {
return undefined;
}
const current = Number.parseInt(currentStr);
const revision = Number.parseInt(revisionStr);
const age = Number.parseInt(ageStr);
const currentCmp = Math.sign(meVer.current - otherVer.current);
if (Number.isNaN(current)) {
return undefined;
}
return { compatible, currentCmp };
}
function parseVersion(v: string): Version | undefined {
const [currentStr, revisionStr, ageStr, ...rest] = v.split(":");
if (rest.length !== 0) {
return undefined;
}
const current = Number.parseInt(currentStr);
const revision = Number.parseInt(revisionStr);
const age = Number.parseInt(ageStr);
if (Number.isNaN(current)) {
return undefined;
}
if (Number.isNaN(revision)) {
return undefined;
}
if (Number.isNaN(age)) {
return undefined;
}
return { current, revision, age };
if (Number.isNaN(revision)) {
return undefined;
}
if (Number.isNaN(age)) {
return undefined;
}
return { current, revision, age };
}
}

View File

@ -24,7 +24,7 @@
import * as nacl from "./nacl-fast.js";
import { kdf } from "./kdf.js";
import bigint from "big-integer";
import { DenominationPubKey } from "./talerTypes.js";
import { DenominationPubKey, DenomKeyType } from "./talerTypes.js";
export function getRandomBytes(n: number): Uint8Array {
return nacl.randomBytes(n);
@ -350,7 +350,7 @@ export function hash(d: Uint8Array): Uint8Array {
}
export function hashDenomPub(pub: DenominationPubKey): Uint8Array {
if (pub.cipher !== 1) {
if (pub.cipher !== DenomKeyType.Rsa) {
throw Error("unsupported cipher");
}
const pubBuf = decodeCrock(pub.rsa_public_key);

View File

@ -598,9 +598,9 @@ export interface TipPickupRequest {
/**
* Reserve signature, defined as separate class to facilitate
* schema validation with "@Checkable".
* schema validation.
*/
export interface BlindSigWrapper {
export interface MerchantBlindSigWrapperV1 {
/**
* Reserve signature.
*/
@ -611,11 +611,26 @@ export interface BlindSigWrapper {
* Response of the merchant
* to the TipPickupRequest.
*/
export interface TipResponse {
export interface MerchantTipResponseV1 {
/**
* The order of the signatures matches the planchets list.
*/
blind_sigs: BlindSigWrapper[];
blind_sigs: MerchantBlindSigWrapperV1[];
}
export interface MerchantBlindSigWrapperV2 {
blind_sig: BlindedDenominationSignature;
}
/**
* Response of the merchant
* to the TipPickupRequest.
*/
export interface MerchantTipResponseV2 {
/**
* The order of the signatures matches the planchets list.
*/
blind_sigs: MerchantBlindSigWrapperV2[];
}
/**
@ -1032,13 +1047,14 @@ export interface BankWithdrawalOperationPostResponse {
export type DenominationPubKey = RsaDenominationPubKey | CsDenominationPubKey;
export interface RsaDenominationPubKey {
cipher: 1;
cipher: DenomKeyType.Rsa;
rsa_public_key: string;
age_mask?: number;
}
export interface CsDenominationPubKey {
cipher: 2;
cipher: DenomKeyType.ClauseSchnorr;
// FIXME: finish definition
}
export const codecForDenominationPubKey = () =>
@ -1201,15 +1217,25 @@ export const codecForMerchantRefundResponse = (): Codec<MerchantRefundResponse>
.property("refunds", codecForList(codecForMerchantRefundPermission()))
.build("MerchantRefundResponse");
export const codecForBlindSigWrapper = (): Codec<BlindSigWrapper> =>
buildCodecForObject<BlindSigWrapper>()
export const codecForMerchantBlindSigWrapperV1 = (): Codec<MerchantBlindSigWrapperV1> =>
buildCodecForObject<MerchantBlindSigWrapperV1>()
.property("blind_sig", codecForString())
.build("BlindSigWrapper");
export const codecForTipResponse = (): Codec<TipResponse> =>
buildCodecForObject<TipResponse>()
.property("blind_sigs", codecForList(codecForBlindSigWrapper()))
.build("TipResponse");
export const codecForMerchantTipResponseV1 = (): Codec<MerchantTipResponseV1> =>
buildCodecForObject<MerchantTipResponseV1>()
.property("blind_sigs", codecForList(codecForMerchantBlindSigWrapperV1()))
.build("MerchantTipResponseV1");
export const codecForBlindSigWrapperV2 = (): Codec<MerchantBlindSigWrapperV2> =>
buildCodecForObject<MerchantBlindSigWrapperV2>()
.property("blind_sig", codecForBlindedDenominationSignature())
.build("MerchantBlindSigWrapperV2");
export const codecForMerchantTipResponseV2 = (): Codec<MerchantTipResponseV2> =>
buildCodecForObject<MerchantTipResponseV2>()
.property("blind_sigs", codecForList(codecForBlindSigWrapperV2()))
.build("MerchantTipResponseV2");
export const codecForRecoup = (): Codec<Recoup> =>
buildCodecForObject<Recoup>()
@ -1510,3 +1536,16 @@ export const codecForKeysManagementResponse = (): Codec<FutureKeysResponse> =>
.property("denom_secmod_public_key", codecForAny())
.property("signkey_secmod_public_key", codecForAny())
.build("FutureKeysResponse");
export interface MerchantConfigResponse {
currency: string;
name: string;
version: string;
}
export const codecForMerchantConfigResponse = (): Codec<MerchantConfigResponse> =>
buildCodecForObject<MerchantConfigResponse>()
.property("currency", codecForString())
.property("name", codecForString())
.property("version", codecForString())
.build("MerchantConfigResponse");

View File

@ -66,7 +66,7 @@ import {
encodeCrock,
getRandomBytes,
hash,
stringToBytes
stringToBytes,
} from "@gnu-taler/taler-util";
import { CoinConfig } from "./denomStructures.js";
import { LibeufinNexusApi, LibeufinSandboxApi } from "./libeufin-apis.js";
@ -445,7 +445,7 @@ export async function pingProc(
}
while (true) {
try {
console.log(`pinging ${serviceName}`);
console.log(`pinging ${serviceName} at ${url}`);
const resp = await axios.get(url);
console.log(`service ${serviceName} available`);
return;
@ -556,7 +556,6 @@ export namespace BankApi {
debitAccountPayto: string;
},
) {
let maybeBaseUrl = bank.baseUrl;
if (process.env.WALLET_HARNESS_WITH_EUFIN) {
maybeBaseUrl = (bank as EufinBankService).baseUrlDemobank;
@ -618,7 +617,6 @@ export namespace BankApi {
}
}
class BankServiceBase {
proc: ProcessWrapper | undefined;
@ -641,7 +639,6 @@ class EufinBankService extends BankServiceBase implements BankServiceInterface {
gc: GlobalTestState,
bc: BankConfig,
): Promise<EufinBankService> {
return new EufinBankService(gc, bc, "foo");
}
@ -650,16 +647,14 @@ class EufinBankService extends BankServiceBase implements BankServiceInterface {
}
get nexusPort() {
return this.bankConfig.httpPort + 1000;
}
get nexusDbConn(): string {
return `jdbc:sqlite:${this.globalTestState.testDir}/libeufin-nexus.sqlite3`;
return `jdbc:sqlite:${this.globalTestState.testDir}/libeufin-nexus.sqlite3`;
}
get sandboxDbConn(): string {
return `jdbc:sqlite:${this.globalTestState.testDir}/libeufin-sandbox.sqlite3`;
return `jdbc:sqlite:${this.globalTestState.testDir}/libeufin-sandbox.sqlite3`;
}
get nexusBaseUrl(): string {
@ -673,9 +668,9 @@ class EufinBankService extends BankServiceBase implements BankServiceInterface {
get baseUrlAccessApi(): string {
let url = new URL("access-api/", this.baseUrlDemobank);
return url.href;
return url.href;
}
get baseUrlNetloc(): string {
return `http://localhost:${this.bankConfig.httpPort}/`;
}
@ -686,7 +681,7 @@ class EufinBankService extends BankServiceBase implements BankServiceInterface {
async setSuggestedExchange(
e: ExchangeServiceInterface,
exchangePayto: string
exchangePayto: string,
) {
await sh(
this.globalTestState,
@ -712,11 +707,9 @@ class EufinBankService extends BankServiceBase implements BankServiceInterface {
*/
await this.start();
await this.pingUntilAvailable();
await LibeufinSandboxApi.createDemobankAccount(
accountName,
password,
{ baseUrl: this.baseUrlAccessApi }
);
await LibeufinSandboxApi.createDemobankAccount(accountName, password, {
baseUrl: this.baseUrlAccessApi,
});
let bankAccountLabel = accountName;
await LibeufinSandboxApi.createDemobankEbicsSubscriber(
{
@ -725,47 +718,49 @@ class EufinBankService extends BankServiceBase implements BankServiceInterface {
partnerID: "exchangeEbicsPartner",
},
bankAccountLabel,
{ baseUrl: this.baseUrlDemobank }
{ baseUrl: this.baseUrlDemobank },
);
await LibeufinNexusApi.createUser(
{ baseUrl: this.nexusBaseUrl },
{
username: accountName,
password: password
}
password: password,
},
);
await LibeufinNexusApi.createEbicsBankConnection(
{ baseUrl: this.nexusBaseUrl },
{
name: "ebics-connection", // connection name.
ebicsURL: (new URL("ebicsweb", this.baseUrlNetloc)).href,
ebicsURL: new URL("ebicsweb", this.baseUrlNetloc).href,
hostID: "talertestEbicsHost",
userID: "exchangeEbicsUser",
partnerID: "exchangeEbicsPartner",
}
},
);
await LibeufinNexusApi.connectBankConnection(
{ baseUrl: this.nexusBaseUrl }, "ebics-connection"
{ baseUrl: this.nexusBaseUrl },
"ebics-connection",
);
await LibeufinNexusApi.fetchAccounts(
{ baseUrl: this.nexusBaseUrl }, "ebics-connection"
{ baseUrl: this.nexusBaseUrl },
"ebics-connection",
);
await LibeufinNexusApi.importConnectionAccount(
{ baseUrl: this.nexusBaseUrl },
"ebics-connection", // connection name
accountName, // offered account label
`${accountName}-nexus-label` // bank account label at Nexus
`${accountName}-nexus-label`, // bank account label at Nexus
);
await LibeufinNexusApi.createTwgFacade(
{ baseUrl: this.nexusBaseUrl },
{
name: "exchange-facade",
connectionName: "ebics-connection",
connectionName: "ebics-connection",
accountName: `${accountName}-nexus-label`,
currency: "EUR",
reserveTransferLevel: "report"
}
currency: "EUR",
reserveTransferLevel: "report",
},
);
await LibeufinNexusApi.postPermission(
{ baseUrl: this.nexusBaseUrl },
@ -778,7 +773,7 @@ class EufinBankService extends BankServiceBase implements BankServiceInterface {
resourceId: "exchange-facade", // facade name
permissionName: "facade.talerWireGateway.transfer",
},
}
},
);
await LibeufinNexusApi.postPermission(
{ baseUrl: this.nexusBaseUrl },
@ -791,7 +786,7 @@ class EufinBankService extends BankServiceBase implements BankServiceInterface {
resourceId: "exchange-facade", // facade name
permissionName: "facade.talerWireGateway.history",
},
}
},
);
// Set fetch task.
await LibeufinNexusApi.postTask(
@ -804,8 +799,9 @@ class EufinBankService extends BankServiceBase implements BankServiceInterface {
params: {
level: "all",
rangeType: "all",
},
},
});
);
await LibeufinNexusApi.postTask(
{ baseUrl: this.nexusBaseUrl },
`${accountName}-nexus-label`,
@ -814,14 +810,16 @@ class EufinBankService extends BankServiceBase implements BankServiceInterface {
cronspec: "* * *",
type: "submit",
params: {},
}
},
);
let facadesResp = await LibeufinNexusApi.getAllFacades({ baseUrl: this.nexusBaseUrl });
let facadesResp = await LibeufinNexusApi.getAllFacades({
baseUrl: this.nexusBaseUrl,
});
let accountInfoResp = await LibeufinSandboxApi.demobankAccountInfo(
"admin",
"secret",
{ baseUrl: this.baseUrlAccessApi },
accountName // bank account label.
accountName, // bank account label.
);
return {
accountName: accountName,
@ -840,7 +838,7 @@ class EufinBankService extends BankServiceBase implements BankServiceInterface {
* them if they weren't launched earlier.
*/
// Only go ahead if BOTH aren't running.
// Only go ahead if BOTH aren't running.
if (this.sandboxProc || this.nexusProc) {
console.log("Nexus or Sandbox already running, not taking any action.");
return;
@ -864,7 +862,7 @@ class EufinBankService extends BankServiceBase implements BankServiceInterface {
LIBEUFIN_SANDBOX_DB_CONNECTION: this.sandboxDbConn,
LIBEUFIN_SANDBOX_ADMIN_PASSWORD: "secret",
},
);
);
await runCommand(
this.globalTestState,
"libeufin-nexus-superuser",
@ -889,7 +887,7 @@ class EufinBankService extends BankServiceBase implements BankServiceInterface {
await this.pingUntilAvailable();
LibeufinSandboxApi.createEbicsHost(
{ baseUrl: this.baseUrlNetloc },
"talertestEbicsHost"
"talertestEbicsHost",
);
}
@ -897,12 +895,12 @@ class EufinBankService extends BankServiceBase implements BankServiceInterface {
await pingProc(
this.sandboxProc,
`http://localhost:${this.bankConfig.httpPort}`,
"libeufin-sandbox"
"libeufin-sandbox",
);
await pingProc(
this.nexusProc,
`${this.nexusBaseUrl}/config`,
"libeufin-nexus"
"libeufin-nexus",
);
}
}
@ -999,7 +997,6 @@ class PybankService extends BankServiceBase implements BankServiceInterface {
}
}
/**
* Return a euFin or a pyBank implementation of
* the exported BankService class. This allows
@ -1007,19 +1004,18 @@ class PybankService extends BankServiceBase implements BankServiceInterface {
* on a particular env variable.
*/
function getBankServiceImpl(): {
prototype: typeof PybankService.prototype,
create: typeof PybankService.create
prototype: typeof PybankService.prototype;
create: typeof PybankService.create;
} {
if (process.env.WALLET_HARNESS_WITH_EUFIN)
if (process.env.WALLET_HARNESS_WITH_EUFIN)
return {
prototype: EufinBankService.prototype,
create: EufinBankService.create
}
create: EufinBankService.create,
};
return {
prototype: PybankService.prototype,
create: PybankService.create
}
create: PybankService.create,
};
}
export type BankService = PybankService;
@ -2088,10 +2084,8 @@ export class WalletCli {
}
export function getRandomIban(salt: string | null = null): string {
function getBban(salt: string | null): string {
if (!salt)
return Math.random().toString().substring(2, 6);
if (!salt) return Math.random().toString().substring(2, 6);
let hashed = hash(stringToBytes(salt));
let ret = "";
for (let i = 0; i < hashed.length; i++) {
@ -2101,19 +2095,21 @@ export function getRandomIban(salt: string | null = null): string {
}
let cc_no_check = "131400"; // == DE00
let bban = getBban(salt)
let check_digits = (98 - (Number.parseInt(`${bban}${cc_no_check}`) % 97)).toString();
let bban = getBban(salt);
let check_digits = (
98 -
(Number.parseInt(`${bban}${cc_no_check}`) % 97)
).toString();
if (check_digits.length == 1) {
check_digits = `0${check_digits}`;
}
return `DE${check_digits}${bban}`;
return `DE${check_digits}${bban}`;
}
// Only used in one tipping test.
export function getWireMethod(): string {
if (process.env.WALLET_HARNESS_WITH_EUFIN)
return "iban"
return "x-taler-bank"
if (process.env.WALLET_HARNESS_WITH_EUFIN) return "iban";
return "x-taler-bank";
}
/**
@ -2122,10 +2118,12 @@ export function getWireMethod(): string {
*/
export function getPayto(label: string): string {
if (process.env.WALLET_HARNESS_WITH_EUFIN)
return `payto://iban/SANDBOXX/${getRandomIban(label)}?receiver-name=${label}`
return `payto://x-taler-bank/${label}`
return `payto://iban/SANDBOXX/${getRandomIban(
label,
)}?receiver-name=${label}`;
return `payto://x-taler-bank/${label}`;
}
function waitMs(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
return new Promise((resolve) => setTimeout(resolve, ms));
}

View File

@ -51,6 +51,21 @@ export interface TrustInfo {
isAudited: boolean;
}
export interface MerchantInfo {
supportsMerchantProtocolV1: boolean;
supportsMerchantProtocolV2: boolean;
}
/**
* Interface for merchant-related operations.
*/
export interface MerchantOperations {
getMerchantInfo(
ws: InternalWalletState,
merchantBaseUrl: string,
): Promise<MerchantInfo>;
}
/**
* Interface for exchange-related operations.
*/
@ -131,8 +146,11 @@ export interface InternalWalletState {
initCalled: boolean;
merchantInfoCache: Record<string, MerchantInfo>;
exchangeOps: ExchangeOperations;
recoupOps: RecoupOperations;
merchantOps: MerchantOperations;
db: DbAccess<typeof WalletStoresV1>;
http: HttpRequestLibrary;

View File

@ -484,8 +484,14 @@ export interface WireInfo {
export interface ExchangeDetailsPointer {
masterPublicKey: string;
currency: string;
/**
* Last observed protocol version range offered by the exchange.
*/
protocolVersionRange: string;
/**
* Timestamp when the (masterPublicKey, currency) pointer
* has been updated.

View File

@ -273,6 +273,7 @@ export async function exportBackup(
currency: dp.currency,
master_public_key: dp.masterPublicKey,
update_clock: dp.updateClock,
protocol_version_range: dp.protocolVersionRange,
});
});

View File

@ -267,6 +267,7 @@ export async function importBackup(
currency: backupExchange.currency,
masterPublicKey: backupExchange.master_public_key,
updateClock: backupExchange.update_clock,
protocolVersionRange: backupExchange.protocol_version_range,
},
permanent: true,
retryInfo: initRetryInfo(),

View File

@ -23,7 +23,6 @@ import {
canonicalizeBaseUrl,
codecForExchangeKeysJson,
codecForExchangeWireJson,
compare,
Denomination,
Duration,
durationFromSpec,
@ -40,6 +39,7 @@ import {
TalerErrorDetails,
Timestamp,
hashDenomPub,
LibtoolVersion,
} from "@gnu-taler/taler-util";
import { decodeCrock, encodeCrock, hash } from "@gnu-taler/taler-util";
import { CryptoApi } from "../crypto/workers/cryptoApi.js";
@ -365,7 +365,10 @@ async function downloadKeysInfo(
const protocolVersion = exchangeKeysJson.version;
const versionRes = compare(WALLET_EXCHANGE_PROTOCOL_VERSION, protocolVersion);
const versionRes = LibtoolVersion.compare(
WALLET_EXCHANGE_PROTOCOL_VERSION,
protocolVersion,
);
if (versionRes?.compatible != true) {
const opErr = makeErrorDetails(
TalerErrorCode.WALLET_EXCHANGE_PROTOCOL_VERSION_INCOMPATIBLE,
@ -548,6 +551,7 @@ async function updateExchangeFromUrlImpl(
masterPublicKey: details.masterPublicKey,
// FIXME: only change if pointer really changed
updateClock: getTimestampNow(),
protocolVersionRange: keysInfo.protocolVersion,
};
await tx.exchanges.put(r);
await tx.exchangeDetails.put(details);

View File

@ -0,0 +1,68 @@
/*
This file is part of GNU Taler
(C) 2021 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 {
canonicalizeBaseUrl,
Logger,
URL,
codecForMerchantConfigResponse,
LibtoolVersion,
} from "@gnu-taler/taler-util";
import { InternalWalletState, MerchantInfo } from "../common.js";
import { readSuccessResponseJsonOrThrow } from "../index.js";
const logger = new Logger("taler-wallet-core:merchants.ts");
export async function getMerchantInfo(
ws: InternalWalletState,
merchantBaseUrl: string,
): Promise<MerchantInfo> {
const canonBaseUrl = canonicalizeBaseUrl(merchantBaseUrl);
const existingInfo = ws.merchantInfoCache[canonBaseUrl];
if (existingInfo) {
return existingInfo;
}
const configUrl = new URL("config", canonBaseUrl);
const resp = await ws.http.get(configUrl.href);
const configResp = await readSuccessResponseJsonOrThrow(
resp,
codecForMerchantConfigResponse(),
);
logger.info(
`merchant "${canonBaseUrl}" reports protocol ${configResp.version}"`,
);
const merchantInfo: MerchantInfo = {
supportsMerchantProtocolV1: !!LibtoolVersion.compare(
"1:0:0",
configResp.version,
)?.compatible,
supportsMerchantProtocolV2: !!LibtoolVersion.compare(
"2:0:0",
configResp.version,
)?.compatible,
};
ws.merchantInfoCache[canonBaseUrl] = merchantInfo;
return merchantInfo;
}

View File

@ -27,10 +27,12 @@ import {
NotificationType,
TipPlanchetDetail,
TalerErrorCode,
codecForTipResponse,
codecForMerchantTipResponseV1,
Logger,
URL,
DenomKeyType,
BlindedDenominationSignature,
codecForMerchantTipResponseV2,
} from "@gnu-taler/taler-util";
import { DerivedTipPlanchet } from "../crypto/cryptoTypes.js";
import {
@ -304,31 +306,57 @@ async function processTipImpl(
return;
}
const response = await readSuccessResponseJsonOrThrow(
merchantResp,
codecForTipResponse(),
// FIXME: Do this earlier?
const merchantInfo = await ws.merchantOps.getMerchantInfo(
ws,
tipRecord.merchantBaseUrl,
);
if (response.blind_sigs.length !== planchets.length) {
let blindedSigs: BlindedDenominationSignature[] = [];
if (merchantInfo.supportsMerchantProtocolV2) {
const response = await readSuccessResponseJsonOrThrow(
merchantResp,
codecForMerchantTipResponseV2(),
);
blindedSigs = response.blind_sigs.map((x) => x.blind_sig);
} else if (merchantInfo.supportsMerchantProtocolV1) {
const response = await readSuccessResponseJsonOrThrow(
merchantResp,
codecForMerchantTipResponseV1(),
);
blindedSigs = response.blind_sigs.map((x) => ({
cipher: DenomKeyType.Rsa,
blinded_rsa_signature: x.blind_sig,
}));
} else {
throw Error("unsupported merchant protocol version");
}
if (blindedSigs.length !== planchets.length) {
throw Error("number of tip responses does not match requested planchets");
}
const newCoinRecords: CoinRecord[] = [];
for (let i = 0; i < response.blind_sigs.length; i++) {
const blindedSig = response.blind_sigs[i].blind_sig;
for (let i = 0; i < blindedSigs.length; i++) {
const blindedSig = blindedSigs[i];
const denom = denomForPlanchet[i];
checkLogicInvariant(!!denom);
const planchet = planchets[i];
checkLogicInvariant(!!planchet);
if (denom.denomPub.cipher !== 1) {
if (denom.denomPub.cipher !== DenomKeyType.Rsa) {
throw Error("unsupported cipher");
}
if (blindedSig.cipher !== DenomKeyType.Rsa) {
throw Error("unsupported cipher");
}
const denomSigRsa = await ws.cryptoApi.rsaUnblind(
blindedSig,
blindedSig.blinded_rsa_signature,
planchet.blindingKey,
denom.denomPub.rsa_public_key,
);

View File

@ -24,7 +24,6 @@ import {
codecForTalerConfigResponse,
codecForWithdrawOperationStatusResponse,
codecForWithdrawResponse,
compare,
durationFromSpec,
ExchangeListItem,
getDurationRemaining,
@ -42,6 +41,7 @@ import {
WithdrawUriInfoResponse,
VersionMatchResult,
DenomKeyType,
LibtoolVersion,
} from "@gnu-taler/taler-util";
import {
CoinRecord,
@ -285,7 +285,7 @@ export async function getBankWithdrawalInfo(
codecForTalerConfigResponse(),
);
const versionRes = compare(
const versionRes = LibtoolVersion.compare(
WALLET_BANK_INTEGRATION_PROTOCOL_VERSION,
config.version,
);
@ -985,7 +985,7 @@ export async function getExchangeWithdrawalInfo(
let versionMatch;
if (exchangeDetails.protocolVersion) {
versionMatch = compare(
versionMatch = LibtoolVersion.compare(
WALLET_EXCHANGE_PROTOCOL_VERSION,
exchangeDetails.protocolVersion,
);

View File

@ -99,6 +99,8 @@ import {
import {
ExchangeOperations,
InternalWalletState,
MerchantInfo,
MerchantOperations,
NotificationListener,
RecoupOperations,
} from "./common.js";
@ -180,6 +182,7 @@ import {
HttpRequestLibrary,
readSuccessResponseJsonOrThrow,
} from "./util/http.js";
import { getMerchantInfo } from "./operations/merchants.js";
const builtinAuditors: AuditorTrustRecord[] = [
{
@ -1069,6 +1072,8 @@ class InternalWalletStateImpl implements InternalWalletState {
memoProcessDeposit: AsyncOpMemoMap<void> = new AsyncOpMemoMap();
cryptoApi: CryptoApi;
merchantInfoCache: Record<string, MerchantInfo> = {};
timerGroup: TimerGroup = new TimerGroup();
latch = new AsyncCondition();
stopped = false;
@ -1088,6 +1093,10 @@ class InternalWalletStateImpl implements InternalWalletState {
processRecoupGroup: processRecoupGroup,
};
merchantOps: MerchantOperations = {
getMerchantInfo: getMerchantInfo,
};
/**
* Promises that are waiting for a particular resource.
*/