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; currency: string;
protocol_version_range: string;
/** /**
* Time when the pointer to the exchange details * Time when the pointer to the exchange details
* was last updated. * was last updated.

View File

@ -14,7 +14,7 @@
TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> 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"; import test from "ava";

View File

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

View File

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

View File

@ -598,9 +598,9 @@ export interface TipPickupRequest {
/** /**
* Reserve signature, defined as separate class to facilitate * Reserve signature, defined as separate class to facilitate
* schema validation with "@Checkable". * schema validation.
*/ */
export interface BlindSigWrapper { export interface MerchantBlindSigWrapperV1 {
/** /**
* Reserve signature. * Reserve signature.
*/ */
@ -611,11 +611,26 @@ export interface BlindSigWrapper {
* Response of the merchant * Response of the merchant
* to the TipPickupRequest. * to the TipPickupRequest.
*/ */
export interface TipResponse { export interface MerchantTipResponseV1 {
/** /**
* The order of the signatures matches the planchets list. * 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 type DenominationPubKey = RsaDenominationPubKey | CsDenominationPubKey;
export interface RsaDenominationPubKey { export interface RsaDenominationPubKey {
cipher: 1; cipher: DenomKeyType.Rsa;
rsa_public_key: string; rsa_public_key: string;
age_mask?: number; age_mask?: number;
} }
export interface CsDenominationPubKey { export interface CsDenominationPubKey {
cipher: 2; cipher: DenomKeyType.ClauseSchnorr;
// FIXME: finish definition
} }
export const codecForDenominationPubKey = () => export const codecForDenominationPubKey = () =>
@ -1201,15 +1217,25 @@ export const codecForMerchantRefundResponse = (): Codec<MerchantRefundResponse>
.property("refunds", codecForList(codecForMerchantRefundPermission())) .property("refunds", codecForList(codecForMerchantRefundPermission()))
.build("MerchantRefundResponse"); .build("MerchantRefundResponse");
export const codecForBlindSigWrapper = (): Codec<BlindSigWrapper> => export const codecForMerchantBlindSigWrapperV1 = (): Codec<MerchantBlindSigWrapperV1> =>
buildCodecForObject<BlindSigWrapper>() buildCodecForObject<MerchantBlindSigWrapperV1>()
.property("blind_sig", codecForString()) .property("blind_sig", codecForString())
.build("BlindSigWrapper"); .build("BlindSigWrapper");
export const codecForTipResponse = (): Codec<TipResponse> => export const codecForMerchantTipResponseV1 = (): Codec<MerchantTipResponseV1> =>
buildCodecForObject<TipResponse>() buildCodecForObject<MerchantTipResponseV1>()
.property("blind_sigs", codecForList(codecForBlindSigWrapper())) .property("blind_sigs", codecForList(codecForMerchantBlindSigWrapperV1()))
.build("TipResponse"); .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> => export const codecForRecoup = (): Codec<Recoup> =>
buildCodecForObject<Recoup>() buildCodecForObject<Recoup>()
@ -1510,3 +1536,16 @@ export const codecForKeysManagementResponse = (): Codec<FutureKeysResponse> =>
.property("denom_secmod_public_key", codecForAny()) .property("denom_secmod_public_key", codecForAny())
.property("signkey_secmod_public_key", codecForAny()) .property("signkey_secmod_public_key", codecForAny())
.build("FutureKeysResponse"); .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, encodeCrock,
getRandomBytes, getRandomBytes,
hash, hash,
stringToBytes stringToBytes,
} from "@gnu-taler/taler-util"; } from "@gnu-taler/taler-util";
import { CoinConfig } from "./denomStructures.js"; import { CoinConfig } from "./denomStructures.js";
import { LibeufinNexusApi, LibeufinSandboxApi } from "./libeufin-apis.js"; import { LibeufinNexusApi, LibeufinSandboxApi } from "./libeufin-apis.js";
@ -445,7 +445,7 @@ export async function pingProc(
} }
while (true) { while (true) {
try { try {
console.log(`pinging ${serviceName}`); console.log(`pinging ${serviceName} at ${url}`);
const resp = await axios.get(url); const resp = await axios.get(url);
console.log(`service ${serviceName} available`); console.log(`service ${serviceName} available`);
return; return;
@ -556,7 +556,6 @@ export namespace BankApi {
debitAccountPayto: string; debitAccountPayto: string;
}, },
) { ) {
let maybeBaseUrl = bank.baseUrl; let maybeBaseUrl = bank.baseUrl;
if (process.env.WALLET_HARNESS_WITH_EUFIN) { if (process.env.WALLET_HARNESS_WITH_EUFIN) {
maybeBaseUrl = (bank as EufinBankService).baseUrlDemobank; maybeBaseUrl = (bank as EufinBankService).baseUrlDemobank;
@ -618,7 +617,6 @@ export namespace BankApi {
} }
} }
class BankServiceBase { class BankServiceBase {
proc: ProcessWrapper | undefined; proc: ProcessWrapper | undefined;
@ -641,7 +639,6 @@ class EufinBankService extends BankServiceBase implements BankServiceInterface {
gc: GlobalTestState, gc: GlobalTestState,
bc: BankConfig, bc: BankConfig,
): Promise<EufinBankService> { ): Promise<EufinBankService> {
return new EufinBankService(gc, bc, "foo"); return new EufinBankService(gc, bc, "foo");
} }
@ -650,16 +647,14 @@ class EufinBankService extends BankServiceBase implements BankServiceInterface {
} }
get nexusPort() { get nexusPort() {
return this.bankConfig.httpPort + 1000; return this.bankConfig.httpPort + 1000;
} }
get nexusDbConn(): string { get nexusDbConn(): string {
return `jdbc:sqlite:${this.globalTestState.testDir}/libeufin-nexus.sqlite3`; return `jdbc:sqlite:${this.globalTestState.testDir}/libeufin-nexus.sqlite3`;
} }
get sandboxDbConn(): string { get sandboxDbConn(): string {
return `jdbc:sqlite:${this.globalTestState.testDir}/libeufin-sandbox.sqlite3`; return `jdbc:sqlite:${this.globalTestState.testDir}/libeufin-sandbox.sqlite3`;
} }
get nexusBaseUrl(): string { get nexusBaseUrl(): string {
@ -673,9 +668,9 @@ class EufinBankService extends BankServiceBase implements BankServiceInterface {
get baseUrlAccessApi(): string { get baseUrlAccessApi(): string {
let url = new URL("access-api/", this.baseUrlDemobank); let url = new URL("access-api/", this.baseUrlDemobank);
return url.href; return url.href;
} }
get baseUrlNetloc(): string { get baseUrlNetloc(): string {
return `http://localhost:${this.bankConfig.httpPort}/`; return `http://localhost:${this.bankConfig.httpPort}/`;
} }
@ -686,7 +681,7 @@ class EufinBankService extends BankServiceBase implements BankServiceInterface {
async setSuggestedExchange( async setSuggestedExchange(
e: ExchangeServiceInterface, e: ExchangeServiceInterface,
exchangePayto: string exchangePayto: string,
) { ) {
await sh( await sh(
this.globalTestState, this.globalTestState,
@ -712,11 +707,9 @@ class EufinBankService extends BankServiceBase implements BankServiceInterface {
*/ */
await this.start(); await this.start();
await this.pingUntilAvailable(); await this.pingUntilAvailable();
await LibeufinSandboxApi.createDemobankAccount( await LibeufinSandboxApi.createDemobankAccount(accountName, password, {
accountName, baseUrl: this.baseUrlAccessApi,
password, });
{ baseUrl: this.baseUrlAccessApi }
);
let bankAccountLabel = accountName; let bankAccountLabel = accountName;
await LibeufinSandboxApi.createDemobankEbicsSubscriber( await LibeufinSandboxApi.createDemobankEbicsSubscriber(
{ {
@ -725,47 +718,49 @@ class EufinBankService extends BankServiceBase implements BankServiceInterface {
partnerID: "exchangeEbicsPartner", partnerID: "exchangeEbicsPartner",
}, },
bankAccountLabel, bankAccountLabel,
{ baseUrl: this.baseUrlDemobank } { baseUrl: this.baseUrlDemobank },
); );
await LibeufinNexusApi.createUser( await LibeufinNexusApi.createUser(
{ baseUrl: this.nexusBaseUrl }, { baseUrl: this.nexusBaseUrl },
{ {
username: accountName, username: accountName,
password: password password: password,
} },
); );
await LibeufinNexusApi.createEbicsBankConnection( await LibeufinNexusApi.createEbicsBankConnection(
{ baseUrl: this.nexusBaseUrl }, { baseUrl: this.nexusBaseUrl },
{ {
name: "ebics-connection", // connection name. name: "ebics-connection", // connection name.
ebicsURL: (new URL("ebicsweb", this.baseUrlNetloc)).href, ebicsURL: new URL("ebicsweb", this.baseUrlNetloc).href,
hostID: "talertestEbicsHost", hostID: "talertestEbicsHost",
userID: "exchangeEbicsUser", userID: "exchangeEbicsUser",
partnerID: "exchangeEbicsPartner", partnerID: "exchangeEbicsPartner",
} },
); );
await LibeufinNexusApi.connectBankConnection( await LibeufinNexusApi.connectBankConnection(
{ baseUrl: this.nexusBaseUrl }, "ebics-connection" { baseUrl: this.nexusBaseUrl },
"ebics-connection",
); );
await LibeufinNexusApi.fetchAccounts( await LibeufinNexusApi.fetchAccounts(
{ baseUrl: this.nexusBaseUrl }, "ebics-connection" { baseUrl: this.nexusBaseUrl },
"ebics-connection",
); );
await LibeufinNexusApi.importConnectionAccount( await LibeufinNexusApi.importConnectionAccount(
{ baseUrl: this.nexusBaseUrl }, { baseUrl: this.nexusBaseUrl },
"ebics-connection", // connection name "ebics-connection", // connection name
accountName, // offered account label accountName, // offered account label
`${accountName}-nexus-label` // bank account label at Nexus `${accountName}-nexus-label`, // bank account label at Nexus
); );
await LibeufinNexusApi.createTwgFacade( await LibeufinNexusApi.createTwgFacade(
{ baseUrl: this.nexusBaseUrl }, { baseUrl: this.nexusBaseUrl },
{ {
name: "exchange-facade", name: "exchange-facade",
connectionName: "ebics-connection", connectionName: "ebics-connection",
accountName: `${accountName}-nexus-label`, accountName: `${accountName}-nexus-label`,
currency: "EUR", currency: "EUR",
reserveTransferLevel: "report" reserveTransferLevel: "report",
} },
); );
await LibeufinNexusApi.postPermission( await LibeufinNexusApi.postPermission(
{ baseUrl: this.nexusBaseUrl }, { baseUrl: this.nexusBaseUrl },
@ -778,7 +773,7 @@ class EufinBankService extends BankServiceBase implements BankServiceInterface {
resourceId: "exchange-facade", // facade name resourceId: "exchange-facade", // facade name
permissionName: "facade.talerWireGateway.transfer", permissionName: "facade.talerWireGateway.transfer",
}, },
} },
); );
await LibeufinNexusApi.postPermission( await LibeufinNexusApi.postPermission(
{ baseUrl: this.nexusBaseUrl }, { baseUrl: this.nexusBaseUrl },
@ -791,7 +786,7 @@ class EufinBankService extends BankServiceBase implements BankServiceInterface {
resourceId: "exchange-facade", // facade name resourceId: "exchange-facade", // facade name
permissionName: "facade.talerWireGateway.history", permissionName: "facade.talerWireGateway.history",
}, },
} },
); );
// Set fetch task. // Set fetch task.
await LibeufinNexusApi.postTask( await LibeufinNexusApi.postTask(
@ -804,8 +799,9 @@ class EufinBankService extends BankServiceBase implements BankServiceInterface {
params: { params: {
level: "all", level: "all",
rangeType: "all", rangeType: "all",
},
}, },
}); );
await LibeufinNexusApi.postTask( await LibeufinNexusApi.postTask(
{ baseUrl: this.nexusBaseUrl }, { baseUrl: this.nexusBaseUrl },
`${accountName}-nexus-label`, `${accountName}-nexus-label`,
@ -814,14 +810,16 @@ class EufinBankService extends BankServiceBase implements BankServiceInterface {
cronspec: "* * *", cronspec: "* * *",
type: "submit", type: "submit",
params: {}, params: {},
} },
); );
let facadesResp = await LibeufinNexusApi.getAllFacades({ baseUrl: this.nexusBaseUrl }); let facadesResp = await LibeufinNexusApi.getAllFacades({
baseUrl: this.nexusBaseUrl,
});
let accountInfoResp = await LibeufinSandboxApi.demobankAccountInfo( let accountInfoResp = await LibeufinSandboxApi.demobankAccountInfo(
"admin", "admin",
"secret", "secret",
{ baseUrl: this.baseUrlAccessApi }, { baseUrl: this.baseUrlAccessApi },
accountName // bank account label. accountName, // bank account label.
); );
return { return {
accountName: accountName, accountName: accountName,
@ -840,7 +838,7 @@ class EufinBankService extends BankServiceBase implements BankServiceInterface {
* them if they weren't launched earlier. * 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) { if (this.sandboxProc || this.nexusProc) {
console.log("Nexus or Sandbox already running, not taking any action."); console.log("Nexus or Sandbox already running, not taking any action.");
return; return;
@ -864,7 +862,7 @@ class EufinBankService extends BankServiceBase implements BankServiceInterface {
LIBEUFIN_SANDBOX_DB_CONNECTION: this.sandboxDbConn, LIBEUFIN_SANDBOX_DB_CONNECTION: this.sandboxDbConn,
LIBEUFIN_SANDBOX_ADMIN_PASSWORD: "secret", LIBEUFIN_SANDBOX_ADMIN_PASSWORD: "secret",
}, },
); );
await runCommand( await runCommand(
this.globalTestState, this.globalTestState,
"libeufin-nexus-superuser", "libeufin-nexus-superuser",
@ -889,7 +887,7 @@ class EufinBankService extends BankServiceBase implements BankServiceInterface {
await this.pingUntilAvailable(); await this.pingUntilAvailable();
LibeufinSandboxApi.createEbicsHost( LibeufinSandboxApi.createEbicsHost(
{ baseUrl: this.baseUrlNetloc }, { baseUrl: this.baseUrlNetloc },
"talertestEbicsHost" "talertestEbicsHost",
); );
} }
@ -897,12 +895,12 @@ class EufinBankService extends BankServiceBase implements BankServiceInterface {
await pingProc( await pingProc(
this.sandboxProc, this.sandboxProc,
`http://localhost:${this.bankConfig.httpPort}`, `http://localhost:${this.bankConfig.httpPort}`,
"libeufin-sandbox" "libeufin-sandbox",
); );
await pingProc( await pingProc(
this.nexusProc, this.nexusProc,
`${this.nexusBaseUrl}/config`, `${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 * Return a euFin or a pyBank implementation of
* the exported BankService class. This allows * the exported BankService class. This allows
@ -1007,19 +1004,18 @@ class PybankService extends BankServiceBase implements BankServiceInterface {
* on a particular env variable. * on a particular env variable.
*/ */
function getBankServiceImpl(): { function getBankServiceImpl(): {
prototype: typeof PybankService.prototype, prototype: typeof PybankService.prototype;
create: typeof PybankService.create create: typeof PybankService.create;
} { } {
if (process.env.WALLET_HARNESS_WITH_EUFIN)
if (process.env.WALLET_HARNESS_WITH_EUFIN)
return { return {
prototype: EufinBankService.prototype, prototype: EufinBankService.prototype,
create: EufinBankService.create create: EufinBankService.create,
} };
return { return {
prototype: PybankService.prototype, prototype: PybankService.prototype,
create: PybankService.create create: PybankService.create,
} };
} }
export type BankService = PybankService; export type BankService = PybankService;
@ -2088,10 +2084,8 @@ export class WalletCli {
} }
export function getRandomIban(salt: string | null = null): string { export function getRandomIban(salt: string | null = null): string {
function getBban(salt: string | null): string { function getBban(salt: string | null): string {
if (!salt) if (!salt) return Math.random().toString().substring(2, 6);
return Math.random().toString().substring(2, 6);
let hashed = hash(stringToBytes(salt)); let hashed = hash(stringToBytes(salt));
let ret = ""; let ret = "";
for (let i = 0; i < hashed.length; i++) { 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 cc_no_check = "131400"; // == DE00
let bban = getBban(salt) let bban = getBban(salt);
let check_digits = (98 - (Number.parseInt(`${bban}${cc_no_check}`) % 97)).toString(); let check_digits = (
98 -
(Number.parseInt(`${bban}${cc_no_check}`) % 97)
).toString();
if (check_digits.length == 1) { if (check_digits.length == 1) {
check_digits = `0${check_digits}`; check_digits = `0${check_digits}`;
} }
return `DE${check_digits}${bban}`; return `DE${check_digits}${bban}`;
} }
// Only used in one tipping test. // Only used in one tipping test.
export function getWireMethod(): string { export function getWireMethod(): string {
if (process.env.WALLET_HARNESS_WITH_EUFIN) if (process.env.WALLET_HARNESS_WITH_EUFIN) return "iban";
return "iban" return "x-taler-bank";
return "x-taler-bank"
} }
/** /**
@ -2122,10 +2118,12 @@ export function getWireMethod(): string {
*/ */
export function getPayto(label: string): string { export function getPayto(label: string): string {
if (process.env.WALLET_HARNESS_WITH_EUFIN) if (process.env.WALLET_HARNESS_WITH_EUFIN)
return `payto://iban/SANDBOXX/${getRandomIban(label)}?receiver-name=${label}` return `payto://iban/SANDBOXX/${getRandomIban(
return `payto://x-taler-bank/${label}` label,
)}?receiver-name=${label}`;
return `payto://x-taler-bank/${label}`;
} }
function waitMs(ms: number): Promise<void> { 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; 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. * Interface for exchange-related operations.
*/ */
@ -131,8 +146,11 @@ export interface InternalWalletState {
initCalled: boolean; initCalled: boolean;
merchantInfoCache: Record<string, MerchantInfo>;
exchangeOps: ExchangeOperations; exchangeOps: ExchangeOperations;
recoupOps: RecoupOperations; recoupOps: RecoupOperations;
merchantOps: MerchantOperations;
db: DbAccess<typeof WalletStoresV1>; db: DbAccess<typeof WalletStoresV1>;
http: HttpRequestLibrary; http: HttpRequestLibrary;

View File

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

View File

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

View File

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

View File

@ -23,7 +23,6 @@ import {
canonicalizeBaseUrl, canonicalizeBaseUrl,
codecForExchangeKeysJson, codecForExchangeKeysJson,
codecForExchangeWireJson, codecForExchangeWireJson,
compare,
Denomination, Denomination,
Duration, Duration,
durationFromSpec, durationFromSpec,
@ -40,6 +39,7 @@ import {
TalerErrorDetails, TalerErrorDetails,
Timestamp, Timestamp,
hashDenomPub, hashDenomPub,
LibtoolVersion,
} from "@gnu-taler/taler-util"; } from "@gnu-taler/taler-util";
import { decodeCrock, encodeCrock, hash } from "@gnu-taler/taler-util"; import { decodeCrock, encodeCrock, hash } from "@gnu-taler/taler-util";
import { CryptoApi } from "../crypto/workers/cryptoApi.js"; import { CryptoApi } from "../crypto/workers/cryptoApi.js";
@ -365,7 +365,10 @@ async function downloadKeysInfo(
const protocolVersion = exchangeKeysJson.version; 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) { if (versionRes?.compatible != true) {
const opErr = makeErrorDetails( const opErr = makeErrorDetails(
TalerErrorCode.WALLET_EXCHANGE_PROTOCOL_VERSION_INCOMPATIBLE, TalerErrorCode.WALLET_EXCHANGE_PROTOCOL_VERSION_INCOMPATIBLE,
@ -548,6 +551,7 @@ async function updateExchangeFromUrlImpl(
masterPublicKey: details.masterPublicKey, masterPublicKey: details.masterPublicKey,
// FIXME: only change if pointer really changed // FIXME: only change if pointer really changed
updateClock: getTimestampNow(), updateClock: getTimestampNow(),
protocolVersionRange: keysInfo.protocolVersion,
}; };
await tx.exchanges.put(r); await tx.exchanges.put(r);
await tx.exchangeDetails.put(details); 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, NotificationType,
TipPlanchetDetail, TipPlanchetDetail,
TalerErrorCode, TalerErrorCode,
codecForTipResponse, codecForMerchantTipResponseV1,
Logger, Logger,
URL, URL,
DenomKeyType, DenomKeyType,
BlindedDenominationSignature,
codecForMerchantTipResponseV2,
} from "@gnu-taler/taler-util"; } from "@gnu-taler/taler-util";
import { DerivedTipPlanchet } from "../crypto/cryptoTypes.js"; import { DerivedTipPlanchet } from "../crypto/cryptoTypes.js";
import { import {
@ -304,31 +306,57 @@ async function processTipImpl(
return; return;
} }
const response = await readSuccessResponseJsonOrThrow( // FIXME: Do this earlier?
merchantResp, const merchantInfo = await ws.merchantOps.getMerchantInfo(
codecForTipResponse(), 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"); throw Error("number of tip responses does not match requested planchets");
} }
const newCoinRecords: CoinRecord[] = []; const newCoinRecords: CoinRecord[] = [];
for (let i = 0; i < response.blind_sigs.length; i++) { for (let i = 0; i < blindedSigs.length; i++) {
const blindedSig = response.blind_sigs[i].blind_sig; const blindedSig = blindedSigs[i];
const denom = denomForPlanchet[i]; const denom = denomForPlanchet[i];
checkLogicInvariant(!!denom); checkLogicInvariant(!!denom);
const planchet = planchets[i]; const planchet = planchets[i];
checkLogicInvariant(!!planchet); 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"); throw Error("unsupported cipher");
} }
const denomSigRsa = await ws.cryptoApi.rsaUnblind( const denomSigRsa = await ws.cryptoApi.rsaUnblind(
blindedSig, blindedSig.blinded_rsa_signature,
planchet.blindingKey, planchet.blindingKey,
denom.denomPub.rsa_public_key, denom.denomPub.rsa_public_key,
); );

View File

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

View File

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