wallet: tipping protocol change / merchant version info
This commit is contained in:
parent
829a59e1a2
commit
ae8af3f27c
@ -1102,6 +1102,8 @@ export interface BackupExchange {
|
||||
|
||||
currency: string;
|
||||
|
||||
protocol_version_range: string;
|
||||
|
||||
/**
|
||||
* Time when the pointer to the exchange details
|
||||
* was last updated.
|
||||
|
@ -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";
|
||||
|
||||
|
@ -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 };
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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");
|
||||
|
@ -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,7 +647,6 @@ class EufinBankService extends BankServiceBase implements BankServiceInterface {
|
||||
}
|
||||
get nexusPort() {
|
||||
return this.bankConfig.httpPort + 1000;
|
||||
|
||||
}
|
||||
|
||||
get nexusDbConn(): string {
|
||||
@ -659,7 +655,6 @@ class EufinBankService extends BankServiceBase implements BankServiceInterface {
|
||||
|
||||
get sandboxDbConn(): string {
|
||||
return `jdbc:sqlite:${this.globalTestState.testDir}/libeufin-sandbox.sqlite3`;
|
||||
|
||||
}
|
||||
|
||||
get nexusBaseUrl(): string {
|
||||
@ -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,
|
||||
@ -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)
|
||||
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,8 +2095,11 @@ 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}`;
|
||||
}
|
||||
@ -2111,9 +2108,8 @@ export function getRandomIban(salt: string | null = null): string {
|
||||
|
||||
// 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));
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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.
|
||||
|
@ -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,
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -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(),
|
||||
|
@ -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);
|
||||
|
68
packages/taler-wallet-core/src/operations/merchants.ts
Normal file
68
packages/taler-wallet-core/src/operations/merchants.ts
Normal 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;
|
||||
}
|
@ -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,
|
||||
);
|
||||
|
@ -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,
|
||||
);
|
||||
|
@ -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.
|
||||
*/
|
||||
|
Loading…
Reference in New Issue
Block a user