summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--packages/taler-util/src/backupTypes.ts2
-rw-r--r--packages/taler-util/src/libtool-version.test.ts2
-rw-r--r--packages/taler-util/src/libtool-version.ts74
-rw-r--r--packages/taler-util/src/talerCrypto.ts4
-rw-r--r--packages/taler-util/src/talerTypes.ts63
-rw-r--r--packages/taler-wallet-cli/src/harness/harness.ts122
-rw-r--r--packages/taler-wallet-core/src/common.ts18
-rw-r--r--packages/taler-wallet-core/src/db.ts6
-rw-r--r--packages/taler-wallet-core/src/operations/backup/export.ts1
-rw-r--r--packages/taler-wallet-core/src/operations/backup/import.ts1
-rw-r--r--packages/taler-wallet-core/src/operations/exchanges.ts8
-rw-r--r--packages/taler-wallet-core/src/operations/merchants.ts68
-rw-r--r--packages/taler-wallet-core/src/operations/tip.ts46
-rw-r--r--packages/taler-wallet-core/src/operations/withdraw.ts6
-rw-r--r--packages/taler-wallet-core/src/wallet.ts9
15 files changed, 303 insertions, 127 deletions
diff --git a/packages/taler-util/src/backupTypes.ts b/packages/taler-util/src/backupTypes.ts
index ecdd6fdf8..8663850c9 100644
--- a/packages/taler-util/src/backupTypes.ts
+++ b/packages/taler-util/src/backupTypes.ts
@@ -1102,6 +1102,8 @@ export interface BackupExchange {
currency: string;
+ protocol_version_range: string;
+
/**
* Time when the pointer to the exchange details
* was last updated.
diff --git a/packages/taler-util/src/libtool-version.test.ts b/packages/taler-util/src/libtool-version.test.ts
index d35642518..c1683f0df 100644
--- a/packages/taler-util/src/libtool-version.test.ts
+++ b/packages/taler-util/src/libtool-version.test.ts
@@ -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";
diff --git a/packages/taler-util/src/libtool-version.ts b/packages/taler-util/src/libtool-version.ts
index 5e9d0b74e..17d2bbbdc 100644
--- a/packages/taler-util/src/libtool-version.ts
+++ b/packages/taler-util/src/libtool-version.ts
@@ -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);
-
- if (!(meVer && otherVer)) {
- return undefined;
- }
+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);
- const compatible =
- meVer.current - meVer.age <= otherVer.current &&
- meVer.current >= otherVer.current - otherVer.age;
+ if (!(meVer && otherVer)) {
+ return undefined;
+ }
- const currentCmp = Math.sign(meVer.current - otherVer.current);
+ const compatible =
+ meVer.current - meVer.age <= otherVer.current &&
+ meVer.current >= otherVer.current - otherVer.age;
- return { compatible, currentCmp };
-}
+ const currentCmp = Math.sign(meVer.current - otherVer.current);
-function parseVersion(v: string): Version | undefined {
- const [currentStr, revisionStr, ageStr, ...rest] = v.split(":");
- if (rest.length !== 0) {
- return undefined;
+ return { compatible, currentCmp };
}
- const current = Number.parseInt(currentStr);
- const revision = Number.parseInt(revisionStr);
- const age = Number.parseInt(ageStr);
- if (Number.isNaN(current)) {
- return undefined;
- }
+ 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(revision)) {
- return undefined;
- }
+ if (Number.isNaN(current)) {
+ return undefined;
+ }
- if (Number.isNaN(age)) {
- return undefined;
- }
+ if (Number.isNaN(revision)) {
+ return undefined;
+ }
- return { current, revision, age };
+ if (Number.isNaN(age)) {
+ return undefined;
+ }
+
+ return { current, revision, age };
+ }
}
diff --git a/packages/taler-util/src/talerCrypto.ts b/packages/taler-util/src/talerCrypto.ts
index b107786cd..c20ce72a6 100644
--- a/packages/taler-util/src/talerCrypto.ts
+++ b/packages/taler-util/src/talerCrypto.ts
@@ -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);
diff --git a/packages/taler-util/src/talerTypes.ts b/packages/taler-util/src/talerTypes.ts
index 04d700483..bd9c67d7e 100644
--- a/packages/taler-util/src/talerTypes.ts
+++ b/packages/taler-util/src/talerTypes.ts
@@ -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");
diff --git a/packages/taler-wallet-cli/src/harness/harness.ts b/packages/taler-wallet-cli/src/harness/harness.ts
index 4944e3471..9a33d572a 100644
--- a/packages/taler-wallet-cli/src/harness/harness.ts
+++ b/packages/taler-wallet-cli/src/harness/harness.ts
@@ -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));
}
diff --git a/packages/taler-wallet-core/src/common.ts b/packages/taler-wallet-core/src/common.ts
index dd8542def..81c43cf14 100644
--- a/packages/taler-wallet-core/src/common.ts
+++ b/packages/taler-wallet-core/src/common.ts
@@ -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;
diff --git a/packages/taler-wallet-core/src/db.ts b/packages/taler-wallet-core/src/db.ts
index 483cb16c2..ff47cf30d 100644
--- a/packages/taler-wallet-core/src/db.ts
+++ b/packages/taler-wallet-core/src/db.ts
@@ -484,9 +484,15 @@ 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.
*/
diff --git a/packages/taler-wallet-core/src/operations/backup/export.ts b/packages/taler-wallet-core/src/operations/backup/export.ts
index a66bc2e84..75724dca7 100644
--- a/packages/taler-wallet-core/src/operations/backup/export.ts
+++ b/packages/taler-wallet-core/src/operations/backup/export.ts
@@ -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,
});
});
diff --git a/packages/taler-wallet-core/src/operations/backup/import.ts b/packages/taler-wallet-core/src/operations/backup/import.ts
index e8e1de0b9..40fa4cdec 100644
--- a/packages/taler-wallet-core/src/operations/backup/import.ts
+++ b/packages/taler-wallet-core/src/operations/backup/import.ts
@@ -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(),
diff --git a/packages/taler-wallet-core/src/operations/exchanges.ts b/packages/taler-wallet-core/src/operations/exchanges.ts
index c170c5469..638af813a 100644
--- a/packages/taler-wallet-core/src/operations/exchanges.ts
+++ b/packages/taler-wallet-core/src/operations/exchanges.ts
@@ -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);
diff --git a/packages/taler-wallet-core/src/operations/merchants.ts b/packages/taler-wallet-core/src/operations/merchants.ts
new file mode 100644
index 000000000..d12417c7c
--- /dev/null
+++ b/packages/taler-wallet-core/src/operations/merchants.ts
@@ -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;
+}
diff --git a/packages/taler-wallet-core/src/operations/tip.ts b/packages/taler-wallet-core/src/operations/tip.ts
index 07ce00d2e..0253930ea 100644
--- a/packages/taler-wallet-core/src/operations/tip.ts
+++ b/packages/taler-wallet-core/src/operations/tip.ts
@@ -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,
);
diff --git a/packages/taler-wallet-core/src/operations/withdraw.ts b/packages/taler-wallet-core/src/operations/withdraw.ts
index 57bd49d23..a5a8653c6 100644
--- a/packages/taler-wallet-core/src/operations/withdraw.ts
+++ b/packages/taler-wallet-core/src/operations/withdraw.ts
@@ -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,
);
diff --git a/packages/taler-wallet-core/src/wallet.ts b/packages/taler-wallet-core/src/wallet.ts
index cd2dd7f1e..44591a268 100644
--- a/packages/taler-wallet-core/src/wallet.ts
+++ b/packages/taler-wallet-core/src/wallet.ts
@@ -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.
*/