fix lint issues and separate message types into multiple files

This commit is contained in:
Florian Dold 2018-01-03 14:42:06 +01:00
parent eb689d60ac
commit fd2cd9c383
No known key found for this signature in database
GPG Key ID: D2E4F00F29D02A4B
35 changed files with 2632 additions and 2256 deletions

View File

@ -1,7 +1,7 @@
// Place your settings in this file to overwrite default and user settings.
{
// Use latest language servicesu
"typescript.tsdk": "node_modules/typescript/lib",
"typescript.tsdk": "./node_modules/typescript/lib",
// Defines space handling after a comma delimiter
"typescript.format.insertSpaceAfterCommaDelimiter": true,
// Defines space handling after a semicolon in a for statement
@ -34,5 +34,6 @@
},
"**/*.js.map": true
},
"tslint.enable": true,
"editor.wrappingIndent": "same"
}

44
.vscode/tasks.json vendored Normal file
View File

@ -0,0 +1,44 @@
{
// See https://go.microsoft.com/fwlink/?LinkId=733558
// for the documentation about the tasks.json format
"version": "2.0.0",
"tasks": [
{
"type": "typescript",
"tsconfig": "tsconfig.json",
"option": "watch",
"problemMatcher": [
"$tsc-watch"
],
"group": "build",
"isBackground": true,
"promptOnClose": false
},
{
"type": "typescript",
"tsconfig": "tsconfig.json",
"problemMatcher": [
"$tsc"
],
"group": "build"
},
{
"label": "tslint",
"type": "shell",
"command": "make lint",
"problemMatcher": {
"owner": "tslint",
"applyTo": "allDocuments",
"fileLocation": "absolute",
"severity": "warning",
"pattern": "$tslint5"
},
"group": "build"
},
{
"label": "My Task",
"type": "shell",
"command": "echo Hello"
}
]
}

View File

@ -107,7 +107,7 @@ const tsBaseArgs = {
experimentalDecorators: true,
module: "commonjs",
sourceMap: true,
lib: ["ES6", "DOM"],
lib: ["es6", "dom"],
noImplicitReturns: true,
noFallthroughCasesInSwitch: true,
strict: true,
@ -266,6 +266,7 @@ gulp.task("pogen", function (cb) {
*/
function tsconfig(confBase) {
let conf = {
compileOnSave: true,
compilerOptions: {},
files: []
};

257
src/amounts.ts Normal file
View File

@ -0,0 +1,257 @@
/*
This file is part of TALER
(C) 2018 GNUnet e.V. and INRIA
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.
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
TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
/**
* Types and helper functions for dealing with Taler amounts.
*/
/**
* Imports.
*/
import { Checkable } from "./checkable";
/**
* Number of fractional units that one value unit represents.
*/
export const fractionalBase = 1e8;
/**
* Non-negative financial amount. Fractional values are expressed as multiples
* of 1e-8.
*/
@Checkable.Class()
export class AmountJson {
/**
* Value, must be an integer.
*/
@Checkable.Number
readonly value: number;
/**
* Fraction, must be an integer. Represent 1/1e8 of a unit.
*/
@Checkable.Number
readonly fraction: number;
/**
* Currency of the amount.
*/
@Checkable.String
readonly currency: string;
/**
* Verify that a value matches the schema of this class and convert it into a
* member.
*/
static checked: (obj: any) => AmountJson;
}
/**
* Result of a possibly overflowing operation.
*/
export interface Result {
/**
* Resulting, possibly saturated amount.
*/
amount: AmountJson;
/**
* Was there an over-/underflow?
*/
saturated: boolean;
}
/**
* Get the largest amount that is safely representable.
*/
export function getMaxAmount(currency: string): AmountJson {
return {
currency,
fraction: 2 ** 32,
value: Number.MAX_SAFE_INTEGER,
};
}
/**
* Get an amount that represents zero units of a currency.
*/
export function getZero(currency: string): AmountJson {
return {
currency,
fraction: 0,
value: 0,
};
}
/**
* Add two amounts. Return the result and whether
* the addition overflowed. The overflow is always handled
* by saturating and never by wrapping.
*
* Throws when currencies don't match.
*/
export function add(first: AmountJson, ...rest: AmountJson[]): Result {
const currency = first.currency;
let value = first.value + Math.floor(first.fraction / fractionalBase);
if (value > Number.MAX_SAFE_INTEGER) {
return { amount: getMaxAmount(currency), saturated: true };
}
let fraction = first.fraction % fractionalBase;
for (const x of rest) {
if (x.currency !== currency) {
throw Error(`Mismatched currency: ${x.currency} and ${currency}`);
}
value = value + x.value + Math.floor((fraction + x.fraction) / fractionalBase);
fraction = Math.floor((fraction + x.fraction) % fractionalBase);
if (value > Number.MAX_SAFE_INTEGER) {
return { amount: getMaxAmount(currency), saturated: true };
}
}
return { amount: { currency, value, fraction }, saturated: false };
}
/**
* Subtract two amounts. Return the result and whether
* the subtraction overflowed. The overflow is always handled
* by saturating and never by wrapping.
*
* Throws when currencies don't match.
*/
export function sub(a: AmountJson, ...rest: AmountJson[]): Result {
const currency = a.currency;
let value = a.value;
let fraction = a.fraction;
for (const b of rest) {
if (b.currency !== currency) {
throw Error(`Mismatched currency: ${b.currency} and ${currency}`);
}
if (fraction < b.fraction) {
if (value < 1) {
return { amount: { currency, value: 0, fraction: 0 }, saturated: true };
}
value--;
fraction += fractionalBase;
}
console.assert(fraction >= b.fraction);
fraction -= b.fraction;
if (value < b.value) {
return { amount: { currency, value: 0, fraction: 0 }, saturated: true };
}
value -= b.value;
}
return { amount: { currency, value, fraction }, saturated: false };
}
/**
* Compare two amounts. Returns 0 when equal, -1 when a < b
* and +1 when a > b. Throws when currencies don't match.
*/
export function cmp(a: AmountJson, b: AmountJson): number {
if (a.currency !== b.currency) {
throw Error(`Mismatched currency: ${a.currency} and ${b.currency}`);
}
const av = a.value + Math.floor(a.fraction / fractionalBase);
const af = a.fraction % fractionalBase;
const bv = b.value + Math.floor(b.fraction / fractionalBase);
const bf = b.fraction % fractionalBase;
switch (true) {
case av < bv:
return -1;
case av > bv:
return 1;
case af < bf:
return -1;
case af > bf:
return 1;
case af === bf:
return 0;
default:
throw Error("assertion failed");
}
}
/**
* Create a copy of an amount.
*/
export function copy(a: AmountJson): AmountJson {
return {
currency: a.currency,
fraction: a.fraction,
value: a.value,
};
}
/**
* Divide an amount. Throws on division by zero.
*/
export function divide(a: AmountJson, n: number): AmountJson {
if (n === 0) {
throw Error(`Division by 0`);
}
if (n === 1) {
return {value: a.value, fraction: a.fraction, currency: a.currency};
}
const r = a.value % n;
return {
currency: a.currency,
fraction: Math.floor(((r * fractionalBase) + a.fraction) / n),
value: Math.floor(a.value / n),
};
}
/**
* Check if an amount is non-zero.
*/
export function isNonZero(a: AmountJson): boolean {
return a.value > 0 || a.fraction > 0;
}
/**
* Parse an amount like 'EUR:20.5' for 20 Euros and 50 ct.
*/
export function parse(s: string): AmountJson|undefined {
const res = s.match(/([a-zA-Z0-9_*-]+):([0-9])+([.][0-9]+)?/);
if (!res) {
return undefined;
}
return {
currency: res[1],
fraction: Math.round(fractionalBase * Number.parseFloat(res[3] || "0")),
value: Number.parseInt(res[2]),
};
}
/**
* Convert the amount to a float.
*/
export function toFloat(a: AmountJson): number {
return a.value + (a.fraction / fractionalBase);
}
/**
* Convert a float to a Taler amount.
* Loss of precision possible.
*/
export function fromFloat(floatVal: number, currency: string) {
return {
currency,
fraction: Math.floor((floatVal - Math.floor(floatVal)) * fractionalBase),
value: Math.floor(floatVal),
};
}

View File

@ -16,15 +16,15 @@
// tslint:disable:max-line-length
import {test} from "ava";
import { test } from "ava";
import {
DenominationRecord,
DenominationStatus,
ReserveRecord,
} from "../types";
} from "../dbTypes";
import {CryptoApi} from "./cryptoApi";
import { CryptoApi } from "./cryptoApi";
const masterPub1: string = "CQQZ9DY3MZ1ARMN5K1VKDETS04Y2QCKMMCFHZSWJWWVN82BTTH00";

View File

@ -23,20 +23,27 @@
/**
* Imports.
*/
import { AmountJson } from "../amounts";
import {
AmountJson,
CoinRecord,
CoinWithDenom,
ContractTerms,
DenominationRecord,
PayCoinInfo,
PaybackRequest,
PreCoinRecord,
RefreshSessionRecord,
ReserveRecord,
TipPlanchet,
WireFee,
} from "../types";
} from "../dbTypes";
import {
ContractTerms,
PaybackRequest,
} from "../talerTypes";
import {
CoinWithDenom,
PayCoinInfo,
} from "../walletTypes";
import * as timer from "../timer";

View File

@ -22,27 +22,33 @@
/**
* Imports.
*/
import * as Amounts from "../amounts";
import { AmountJson } from "../amounts";
import {
canonicalJson,
} from "../helpers";
import {
AmountJson,
Amounts,
CoinPaySig,
CoinRecord,
CoinStatus,
CoinWithDenom,
ContractTerms,
DenominationRecord,
PayCoinInfo,
PaybackRequest,
PreCoinRecord,
RefreshPreCoinRecord,
RefreshSessionRecord,
ReserveRecord,
TipPlanchet,
WireFee,
} from "../types";
} from "../dbTypes";
import {
CoinPaySig,
ContractTerms,
PaybackRequest,
} from "../talerTypes";
import {
CoinWithDenom,
PayCoinInfo,
} from "../walletTypes";
import { canonicalJson } from "../helpers";
import {
Amount,
@ -112,6 +118,9 @@ namespace RpcFunctions {
}
/**
* Create a planchet used for tipping, including the private keys.
*/
export function createTipPlanchet(denom: DenominationRecord): TipPlanchet {
const denomPub = native.RsaPublicKey.fromCrock(denom.denomPub);
const coinPriv = native.EddsaPrivateKey.create();
@ -134,8 +143,8 @@ namespace RpcFunctions {
coinPriv: coinPriv.toCrock(),
coinPub: coinPub.toCrock(),
coinValue: denom.value,
denomPubHash: denomPub.encode().hash().toCrock(),
denomPub: denomPub.encode().toCrock(),
denomPubHash: denomPub.encode().hash().toCrock(),
};
return tipPlanchet;
}
@ -263,8 +272,8 @@ namespace RpcFunctions {
cds: CoinWithDenom[]): PayCoinInfo {
const ret: PayCoinInfo = {
originalCoins: [],
updatedCoins: [],
sigs: [],
updatedCoins: [],
};
const contractTermsHash = hashString(canonicalJson(contractTerms));
@ -325,8 +334,8 @@ namespace RpcFunctions {
const s: CoinPaySig = {
coin_pub: cd.coin.coinPub,
coin_sig: coinSig,
denom_pub: cd.coin.denomPub,
contribution: coinSpend.toJson(),
denom_pub: cd.coin.denomPub,
ub_sig: cd.coin.denomSig,
};
ret.sigs.push(s);

View File

@ -26,9 +26,9 @@
/**
* Imports.
*/
import {AmountJson} from "../types";
import { AmountJson } from "../amounts";
import {EmscFunGen, getLib} from "./emscLoader";
import { EmscFunGen, getLib } from "./emscLoader";
const emscLib = getLib();

811
src/dbTypes.ts Normal file
View File

@ -0,0 +1,811 @@
/*
This file is part of TALER
(C) 2018 GNUnet e.V. and INRIA
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.
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
TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
/**
* Types for records stored in the wallet's database.
*
* Types for the objects in the database should end in "-Record".
*/
/**
* Imports.
*/
import { AmountJson } from "./amounts";
import { Checkable } from "./checkable";
import {
Auditor,
CoinPaySig,
ContractTerms,
Denomination,
PayReq,
RefundPermission,
TipResponse,
} from "./talerTypes";
/**
* A reserve record as stored in the wallet's database.
*/
export interface ReserveRecord {
/**
* The reserve public key.
*/
reserve_pub: string;
/**
* The reserve private key.
*/
reserve_priv: string;
/**
* The exchange base URL.
*/
exchange_base_url: string;
/**
* Time when the reserve was created.
*/
created: number;
/**
* Time when the reserve was depleted.
* Set to 0 if not depleted yet.
*/
timestamp_depleted: number;
/**
* Time when the reserve was confirmed.
*
* Set to 0 if not confirmed yet.
*/
timestamp_confirmed: number;
/**
* Current amount left in the reserve
*/
current_amount: AmountJson | null;
/**
* Amount requested when the reserve was created.
* When a reserve is re-used (rare!) the current_amount can
* be higher than the requested_amount
*/
requested_amount: AmountJson;
/**
* What's the current amount that sits
* in precoins?
*/
precoin_amount: AmountJson;
/**
* We got some payback to this reserve. We'll cease to automatically
* withdraw money from it.
*/
hasPayback: boolean;
/**
* Wire information for the bank account that
* transfered funds for this reserve.
*/
senderWire?: object;
}
/**
* Auditor record as stored with currencies in the exchange database.
*/
export interface AuditorRecord {
/**
* Base url of the auditor.
*/
baseUrl: string;
/**
* Public signing key of the auditor.
*/
auditorPub: string;
/**
* Time when the auditing expires.
*/
expirationStamp: number;
}
/**
* Exchange for currencies as stored in the wallet's currency
* information database.
*/
export interface ExchangeForCurrencyRecord {
/**
* FIXME: unused?
*/
exchangePub: string;
/**
* Base URL of the exchange.
*/
baseUrl: string;
}
/**
* Information about a currency as displayed in the wallet's database.
*/
export interface CurrencyRecord {
/**
* Name of the currency.
*/
name: string;
/**
* Number of fractional digits to show when rendering the currency.
*/
fractionalDigits: number;
/**
* Auditors that the wallet trusts for this currency.
*/
auditors: AuditorRecord[];
/**
* Exchanges that the wallet trusts for this currency.
*/
exchanges: ExchangeForCurrencyRecord[];
}
/**
* Status of a denomination.
*/
export enum DenominationStatus {
/**
* Verification was delayed.
*/
Unverified,
/**
* Verified as valid.
*/
VerifiedGood,
/**
* Verified as invalid.
*/
VerifiedBad,
}
/**
* Denomination record as stored in the wallet's database.
*/
@Checkable.Class()
export class DenominationRecord {
/**
* Value of one coin of the denomination.
*/
@Checkable.Value(AmountJson)
value: AmountJson;
/**
* The denomination public key.
*/
@Checkable.String
denomPub: string;
/**
* Hash of the denomination public key.
* Stored in the database for faster lookups.
*/
@Checkable.String
denomPubHash: string;
/**
* Fee for withdrawing.
*/
@Checkable.Value(AmountJson)
feeWithdraw: AmountJson;
/**
* Fee for depositing.
*/
@Checkable.Value(AmountJson)
feeDeposit: AmountJson;
/**
* Fee for refreshing.
*/
@Checkable.Value(AmountJson)
feeRefresh: AmountJson;
/**
* Fee for refunding.
*/
@Checkable.Value(AmountJson)
feeRefund: AmountJson;
/**
* Validity start date of the denomination.
*/
@Checkable.String
stampStart: string;
/**
* Date after which the currency can't be withdrawn anymore.
*/
@Checkable.String
stampExpireWithdraw: string;
/**
* Date after the denomination officially doesn't exist anymore.
*/
@Checkable.String
stampExpireLegal: string;
/**
* Data after which coins of this denomination can't be deposited anymore.
*/
@Checkable.String
stampExpireDeposit: string;
/**
* Signature by the exchange's master key over the denomination
* information.
*/
@Checkable.String
masterSig: string;
/**
* Did we verify the signature on the denomination?
*/
@Checkable.Number
status: DenominationStatus;
/**
* Was this denomination still offered by the exchange the last time
* we checked?
* Only false when the exchange redacts a previously published denomination.
*/
@Checkable.Boolean
isOffered: boolean;
/**
* Base URL of the exchange.
*/
@Checkable.String
exchangeBaseUrl: string;
/**
* Verify that a value matches the schema of this class and convert it into a
* member.
*/
static checked: (obj: any) => Denomination;
}
/**
* Exchange record as stored in the wallet's database.
*/
export interface ExchangeRecord {
/**
* Base url of the exchange.
*/
baseUrl: string;
/**
* Master public key of the exchange.
*/
masterPublicKey: string;
/**
* Auditors (partially) auditing the exchange.
*/
auditors: Auditor[];
/**
* Currency that the exchange offers.
*/
currency: string;
/**
* Timestamp for last update.
*/
lastUpdateTime: number;
/**
* When did we actually use this exchange last (in milliseconds). If we
* never used the exchange for anything but just updated its info, this is
* set to 0. (Currently only updated when reserves are created.)
*/
lastUsedTime: number;
/**
* Last observed protocol version.
*/
protocolVersion?: string;
}
/**
* A coin that isn't yet signed by an exchange.
*/
export interface PreCoinRecord {
coinPub: string;
coinPriv: string;
reservePub: string;
denomPub: string;
blindingKey: string;
withdrawSig: string;
coinEv: string;
exchangeBaseUrl: string;
coinValue: AmountJson;
/**
* Set to true if this pre-coin came from a tip.
* Until the tip is marked as "accepted", the resulting
* coin will not be used for payments.
*/
isFromTip: boolean;
}
/**
* Planchet for a coin during refrehs.
*/
export interface RefreshPreCoinRecord {
/**
* Public key for the coin.
*/
publicKey: string;
/**
* Private key for the coin.
*/
privateKey: string;
/**
* Blinded public key.
*/
coinEv: string;
/**
* Blinding key used.
*/
blindingKey: string;
}
/**
* State of returning a list of coins
* to the customer's bank account.
*/
export interface CoinsReturnRecord {
/**
* Coins that we're returning.
*/
coins: CoinPaySig[];
/**
* Responses to the deposit requests.
*/
responses: any;
/**
* Ephemeral dummy merchant key for
* the coins returns operation.
*/
dummyMerchantPub: string;
/**
* Ephemeral dummy merchant key for
* the coins returns operation.
*/
dummyMerchantPriv: string;
/**
* Contract terms.
*/
contractTerms: string;
/**
* Hash of contract terms.
*/
contractTermsHash: string;
/**
* Wire info to send the money for the coins to.
*/
wire: object;
/**
* Hash of the wire object.
*/
wireHash: string;
/**
* All coins were deposited.
*/
finished: boolean;
}
/**
* Status of a coin.
*/
export enum CoinStatus {
/**
* Withdrawn and never shown to anybody.
*/
Fresh,
/**
* Currently planned to be sent to a merchant for a purchase.
*/
PurchasePending,
/**
* Used for a completed transaction and now dirty.
*/
Dirty,
/**
* A coin that was refreshed.
*/
Refreshed,
/**
* Coin marked to be paid back, but payback not finished.
*/
PaybackPending,
/**
* Coin fully paid back.
*/
PaybackDone,
/**
* Coin was dirty but can't be refreshed.
*/
Useless,
/**
* The coin was withdrawn for a tip that the user hasn't accepted yet.
*/
TainedByTip,
}
/**
* CoinRecord as stored in the "coins" data store
* of the wallet database.
*/
export interface CoinRecord {
/**
* Public key of the coin.
*/
coinPub: string;
/**
* Private key to authorize operations on the coin.
*/
coinPriv: string;
/**
* Key used by the exchange used to sign the coin.
*/
denomPub: string;
/**
* Unblinded signature by the exchange.
*/
denomSig: string;
/**
* Amount that's left on the coin.
*/
currentAmount: AmountJson;
/**
* Base URL that identifies the exchange from which we got the
* coin.
*/
exchangeBaseUrl: string;
/**
* We have withdrawn the coin, but it's not accepted by the exchange anymore.
* We have to tell an auditor and wait for compensation or for the exchange
* to fix it.
*/
suspended?: boolean;
/**
* Blinding key used when withdrawing the coin.
* Potentionally sed again during payback.
*/
blindingKey: string;
/**
* Reserve public key for the reserve we got this coin from,
* or zero when we got the coin from refresh.
*/
reservePub: string|undefined;
/**
* Status of the coin.
*/
status: CoinStatus;
}
/**
* Proposal record, stored in the wallet's database.
*/
@Checkable.Class()
export class ProposalRecord {
/**
* The contract that was offered by the merchant.
*/
@Checkable.Value(ContractTerms)
contractTerms: ContractTerms;
/**
* Signature by the merchant over the contract details.
*/
@Checkable.String
merchantSig: string;
/**
* Hash of the contract terms.
*/
@Checkable.String
contractTermsHash: string;
/**
* Serial ID when the offer is stored in the wallet DB.
*/
@Checkable.Optional(Checkable.Number)
id?: number;
/**
* Timestamp (in ms) of when the record
* was created.
*/
@Checkable.Number
timestamp: number;
/**
* Verify that a value matches the schema of this class and convert it into a
* member.
*/
static checked: (obj: any) => ProposalRecord;
}
/**
* Wire fees for an exchange.
*/
export interface ExchangeWireFeesRecord {
/**
* Base URL of the exchange.
*/
exchangeBaseUrl: string;
/**
* Mapping from wire method type to the wire fee.
*/
feesForType: { [wireMethod: string]: WireFee[] };
}
/**
* Status of a tip we got from a merchant.
*/
export interface TipRecord {
/**
* Has the user accepted the tip? Only after the tip has been accepted coins
* withdrawn from the tip may be used.
*/
accepted: boolean;
/**
* The tipped amount.
*/
amount: AmountJson;
/**
* Coin public keys from the planchets.
* This field is redundant and used for indexing the record via
* a multi-entry index to look up tip records by coin public key.
*/
coinPubs: string[];
/**
* Timestamp, the tip can't be picked up anymore after this deadline.
*/
deadline: number;
/**
* The exchange that will sign our coins, chosen by the merchant.
*/
exchangeUrl: string;
/**
* Domain of the merchant, necessary to uniquely identify the tip since
* merchants can freely choose the ID and a malicious merchant might cause a
* collision.
*/
merchantDomain: string;
/**
* Planchets, the members included in TipPlanchetDetail will be sent to the
* merchant.
*/
planchets: TipPlanchet[];
/**
* Response if the merchant responded,
* undefined otherwise.
*/
response?: TipResponse[];
/**
* Identifier for the tip, chosen by the merchant.
*/
tipId: string;
/**
* URL to go to once the tip has been accepted.
*/
nextUrl: string;
timestamp: number;
}
/**
* Ongoing refresh
*/
export interface RefreshSessionRecord {
/**
* Public key that's being melted in this session.
*/
meltCoinPub: string;
/**
* How much of the coin's value is melted away
* with this refresh session?
*/
valueWithFee: AmountJson;
/**
* Sum of the value of denominations we want
* to withdraw in this session, without fees.
*/
valueOutput: AmountJson;
/**
* Signature to confirm the melting.
*/
confirmSig: string;
/**
* Hased denominations of the newly requested coins.
*/
newDenomHashes: string[];
/**
* Denominations of the newly requested coins.
*/
newDenoms: string[];
/**
* Precoins for each cut-and-choose instance.
*/
preCoinsForGammas: RefreshPreCoinRecord[][];
/**
* The transfer keys, kappa of them.
*/
transferPubs: string[];
/**
* Private keys for the transfer public keys.
*/
transferPrivs: string[];
/**
* The no-reveal-index after we've done the melting.
*/
norevealIndex?: number;
/**
* Hash of the session.
*/
hash: string;
/**
* Base URL for the exchange we're doing the refresh with.
*/
exchangeBaseUrl: string;
/**
* Is this session finished?
*/
finished: boolean;
/**
* Record ID when retrieved from the DB.
*/
id?: number;
}
/**
* Tipping planchet stored in the database.
*/
export interface TipPlanchet {
blindingKey: string;
coinEv: string;
coinPriv: string;
coinPub: string;
coinValue: AmountJson;
denomPubHash: string;
denomPub: string;
}
/**
* Wire fee for one wire method as stored in the
* wallet's database.
*/
export interface WireFee {
/**
* Fee for wire transfers.
*/
wireFee: AmountJson;
/**
* Fees to close and refund a reserve.
*/
closingFee: AmountJson;
/**
* Start date of the fee.
*/
startStamp: number;
/**
* End date of the fee.
*/
endStamp: number;
/**
* Signature made by the exchange master key.
*/
sig: string;
}
/**
* Record that stores status information about one purchase, starting from when
* the customer accepts a proposal. Includes refund status if applicable.
*/
export interface PurchaseRecord {
contractTermsHash: string;
contractTerms: ContractTerms;
payReq: PayReq;
merchantSig: string;
/**
* The purchase isn't active anymore, it's either successfully paid or
* refunded/aborted.
*/
finished: boolean;
refundsPending: { [refundSig: string]: RefundPermission };
refundsDone: { [refundSig: string]: RefundPermission };
/**
* When was the purchase made?
* Refers to the time that the user accepted.
*/
timestamp: number;
/**
* When was the last refund made?
* Set to 0 if no refund was made on the purchase.
*/
timestamp_refund: number;
}

View File

@ -21,7 +21,8 @@
/**
* Imports.
*/
import {AmountJson, Amounts} from "./types";
import { AmountJson } from "./amounts";
import * as Amounts from "./amounts";
import URI = require("urijs");

View File

@ -825,7 +825,7 @@ export class QueryRoot {
const req = tx.objectStore(store.name).get(key);
req.onsuccess = () => {
results.push(req.result);
if (results.length == keys.length) {
if (results.length === keys.length) {
resolve(results);
}
};

632
src/talerTypes.ts Normal file
View File

@ -0,0 +1,632 @@
/*
This file is part of TALER
(C) 2018 GNUnet e.V. and INRIA
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.
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
TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
/**
* Type and schema definitions for the base taler protocol.
*
* All types here should be "@Checkable".
*
* Even though the rest of the wallet uses camelCase for fields, use snake_case
* here, since that's the convention for the Taler JSON+HTTP API.
*/
/**
* Imports.
*/
import { AmountJson } from "./amounts";
import { Checkable } from "./checkable";
/**
* Denomination as found in the /keys response from the exchange.
*/
@Checkable.Class()
export class Denomination {
/**
* Value of one coin of the denomination.
*/
@Checkable.Value(AmountJson)
value: AmountJson;
/**
* Public signing key of the denomination.
*/
@Checkable.String
denom_pub: string;
/**
* Fee for withdrawing.
*/
@Checkable.Value(AmountJson)
fee_withdraw: AmountJson;
/**
* Fee for depositing.
*/
@Checkable.Value(AmountJson)
fee_deposit: AmountJson;
/**
* Fee for refreshing.
*/
@Checkable.Value(AmountJson)
fee_refresh: AmountJson;
/**
* Fee for refunding.
*/
@Checkable.Value(AmountJson)
fee_refund: AmountJson;
/**
* Start date from which withdraw is allowed.
*/
@Checkable.String
stamp_start: string;
/**
* End date for withdrawing.
*/
@Checkable.String
stamp_expire_withdraw: string;
/**
* Expiration date after which the exchange can forget about
* the currency.
*/
@Checkable.String
stamp_expire_legal: string;
/**
* Date after which the coins of this denomination can't be
* deposited anymore.
*/
@Checkable.String
stamp_expire_deposit: string;
/**
* Signature over the denomination information by the exchange's master
* signing key.
*/
@Checkable.String
master_sig: string;
/**
* Verify that a value matches the schema of this class and convert it into a
* member.
*/
static checked: (obj: any) => Denomination;
}
/**
* Signature by the auditor that a particular denomination key is audited.
*/
@Checkable.Class()
export class AuditorDenomSig {
/**
* Denomination public key's hash.
*/
@Checkable.String
denom_pub_h: string;
/**
* The signature.
*/
@Checkable.String
auditor_sig: string;
}
/**
* Auditor information as given by the exchange in /keys.
*/
@Checkable.Class()
export class Auditor {
/**
* Auditor's public key.
*/
@Checkable.String
auditor_pub: string;
/**
* Base URL of the auditor.
*/
@Checkable.String
auditor_url: string;
/**
* List of signatures for denominations by the auditor.
*/
@Checkable.List(Checkable.Value(AuditorDenomSig))
denomination_keys: AuditorDenomSig[];
}
/**
* Request that we send to the exchange to get a payback.
*/
export interface PaybackRequest {
/**
* Denomination public key of the coin we want to get
* paid back.
*/
denom_pub: string;
/**
* Signature over the coin public key by the denomination.
*/
denom_sig: string;
/**
* Coin public key of the coin we want to refund.
*/
coin_pub: string;
/**
* Blinding key that was used during withdraw,
* used to prove that we were actually withdrawing the coin.
*/
coin_blind_key_secret: string;
/**
* Signature made by the coin, authorizing the payback.
*/
coin_sig: string;
}
/**
* Response that we get from the exchange for a payback request.
*/
@Checkable.Class()
export class PaybackConfirmation {
/**
* public key of the reserve that will receive the payback.
*/
@Checkable.String
reserve_pub: string;
/**
* How much will the exchange pay back (needed by wallet in
* case coin was partially spent and wallet got restored from backup)
*/
@Checkable.Value(AmountJson)
amount: AmountJson;
/**
* Time by which the exchange received the /payback request.
*/
@Checkable.String
timestamp: string;
/**
* the EdDSA signature of TALER_PaybackConfirmationPS using a current
* signing key of the exchange affirming the successful
* payback request, and that the exchange promises to transfer the funds
* by the date specified (this allows the exchange delaying the transfer
* a bit to aggregate additional payback requests into a larger one).
*/
@Checkable.String
exchange_sig: string;
/**
* Public EdDSA key of the exchange that was used to generate the signature.
* Should match one of the exchange's signing keys from /keys. It is given
* explicitly as the client might otherwise be confused by clock skew as to
* which signing key was used.
*/
@Checkable.String
exchange_pub: string;
/**
* Verify that a value matches the schema of this class and convert it into a
* member.
*/
static checked: (obj: any) => PaybackConfirmation;
}
/**
* Deposit permission for a single coin.
*/
export interface CoinPaySig {
/**
* Signature by the coin.
*/
coin_sig: string;
/**
* Public key of the coin being spend.
*/
coin_pub: string;
/**
* Signature made by the denomination public key.
*/
ub_sig: string;
/**
* The denomination public key associated with this coin.
*/
denom_pub: string;
/**
* The amount that is subtracted from this coin with this payment.
*/
contribution: AmountJson;
}
/**
* Information about an exchange as stored inside a
* merchant's contract terms.
*/
@Checkable.Class()
export class ExchangeHandle {
/**
* Master public signing key of the exchange.
*/
@Checkable.String
master_pub: string;
/**
* Base URL of the exchange.
*/
@Checkable.String
url: string;
/**
* Verify that a value matches the schema of this class and convert it into a
* member.
*/
static checked: (obj: any) => ExchangeHandle;
}
/**
* Contract terms from a merchant.
*/
@Checkable.Class({validate: true})
export class ContractTerms {
static validate(x: ContractTerms) {
if (x.exchanges.length === 0) {
throw Error("no exchanges in contract terms");
}
}
/**
* Hash of the merchant's wire details.
*/
@Checkable.String
H_wire: string;
/**
* Wire method the merchant wants to use.
*/
@Checkable.String
wire_method: string;
/**
* Human-readable short summary of the contract.
*/
@Checkable.Optional(Checkable.String)
summary?: string;
/**
* Nonce used to ensure freshness.
*/
@Checkable.Optional(Checkable.String)
nonce?: string;
/**
* Total amount payable.
*/
@Checkable.Value(AmountJson)
amount: AmountJson;
/**
* Auditors accepted by the merchant.
*/
@Checkable.List(Checkable.AnyObject)
auditors: any[];
/**
* Deadline to pay for the contract.
*/
@Checkable.Optional(Checkable.String)
pay_deadline: string;
/**
* Delivery locations.
*/
@Checkable.Any
locations: any;
/**
* Maximum deposit fee covered by the merchant.
*/
@Checkable.Value(AmountJson)
max_fee: AmountJson;
/**
* Information about the merchant.
*/
@Checkable.Any
merchant: any;
/**
* Public key of the merchant.
*/
@Checkable.String
merchant_pub: string;
/**
* List of accepted exchanges.
*/
@Checkable.List(Checkable.Value(ExchangeHandle))
exchanges: ExchangeHandle[];
/**
* Products that are sold in this contract.
*/
@Checkable.List(Checkable.AnyObject)
products: any[];
/**
* Deadline for refunds.
*/
@Checkable.String
refund_deadline: string;
/**
* Time when the contract was generated by the merchant.
*/
@Checkable.String
timestamp: string;
/**
* Order id to uniquely identify the purchase within
* one merchant instance.
*/
@Checkable.String
order_id: string;
/**
* URL to post the payment to.
*/
@Checkable.String
pay_url: string;
/**
* Fulfillment URL to view the product or
* delivery status.
*/
@Checkable.String
fulfillment_url: string;
/**
* Share of the wire fee that must be settled with one payment.
*/
@Checkable.Optional(Checkable.Number)
wire_fee_amortization?: number;
/**
* Maximum wire fee that the merchant agrees to pay for.
*/
@Checkable.Optional(Checkable.Value(AmountJson))
max_wire_fee?: AmountJson;
/**
* Extra data, interpreted by the mechant only.
*/
@Checkable.Any
extra: any;
/**
* Verify that a value matches the schema of this class and convert it into a
* member.
*/
static checked: (obj: any) => ContractTerms;
}
/**
* Payment body sent to the merchant's /pay.
*/
export interface PayReq {
/**
* Coins with signature.
*/
coins: CoinPaySig[];
/**
* The merchant public key, used to uniquely
* identify the merchant instance.
*/
merchant_pub: string;
/**
* Order ID that's being payed for.
*/
order_id: string;
/**
* Exchange that the coins are from (base URL).
*/
exchange: string;
}
/**
* Refund permission in the format that the merchant gives it to us.
*/
export interface RefundPermission {
/**
* Amount to be refunded.
*/
refund_amount: AmountJson;
/**
* Fee for the refund.
*/
refund_fee: AmountJson;
/**
* Contract terms hash to identify the contract that this
* refund is for.
*/
h_contract_terms: string;
/**
* Public key of the coin being refunded.
*/
coin_pub: string;
/**
* Refund transaction ID between merchant and exchange.
*/
rtransaction_id: number;
/**
* Public key of the merchant.
*/
merchant_pub: string;
/**
* Signature made by the merchant over the refund permission.
*/
merchant_sig: string;
}
/**
* Planchet detail sent to the merchant.
*/
export interface TipPlanchetDetail {
/**
* Hashed denomination public key.
*/
denom_pub_hash: string;
/**
* Coin's blinded public key.
*/
coin_ev: string;
}
/**
* Request sent to the merchant to pick up a tip.
*/
export interface TipPickupRequest {
/**
* Identifier of the tip.
*/
tip_id: string;
/**
* List of planchets the wallet wants to use for the tip.
*/
planchets: TipPlanchetDetail[];
}
/**
* Reserve signature, defined as separate class to facilitate
* schema validation with "@Checkable".
*/
@Checkable.Class()
export class ReserveSigSingleton {
/**
* Reserve signature.
*/
@Checkable.String
reserve_sig: string;
/**
* Create a ReserveSigSingleton from untyped JSON.
*/
static checked: (obj: any) => ReserveSigSingleton;
}
/**
* Response of the merchant
* to the TipPickupRequest.
*/
@Checkable.Class()
export class TipResponse {
/**
* Public key of the reserve
*/
@Checkable.String
reserve_pub: string;
/**
* The order of the signatures matches the planchets list.
*/
@Checkable.List(Checkable.Value(ReserveSigSingleton))
reserve_sigs: ReserveSigSingleton[];
/**
* Create a TipResponse from untyped JSON.
*/
static checked: (obj: any) => TipResponse;
}
/**
* Token containing all the information for the wallet
* to process a tip. Given by the merchant to the wallet.
*/
@Checkable.Class()
export class TipToken {
/**
* Expiration for the tip.
*/
@Checkable.String
expiration: string;
/**
* URL of the exchange that the tip can be withdrawn from.
*/
@Checkable.String
exchange_url: string;
/**
* Merchant's URL to pick up the tip.
*/
@Checkable.String
pickup_url: string;
/**
* Merchant-chosen tip identifier.
*/
@Checkable.String
tip_id: string;
/**
* Amount of tip.
*/
@Checkable.Value(AmountJson)
amount: AmountJson;
/**
* URL to navigate after finishing tip processing.
*/
@Checkable.String
next_url: string;
/**
* Create a TipToken from untyped JSON.
* Validates the schema and throws on error.
*/
static checked: (obj: any) => TipToken;
}

View File

@ -14,17 +14,17 @@
TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
import {test} from "ava";
import {Amounts} from "./types";
import * as types from "./types";
import { test } from "ava";
import * as Amounts from "./amounts";
import { ContractTerms } from "./talerTypes";
const amt = (value: number, fraction: number, currency: string): types.AmountJson => ({value, fraction, currency});
const amt = (value: number, fraction: number, currency: string): Amounts.AmountJson => ({value, fraction, currency});
test("amount addition (simple)", (t) => {
const a1 = amt(1, 0, "EUR");
const a2 = amt(1, 0, "EUR");
const a3 = amt(2, 0, "EUR");
t.true(0 === types.Amounts.cmp(Amounts.add(a1, a2).amount, a3));
t.true(0 === Amounts.cmp(Amounts.add(a1, a2).amount, a3));
t.pass();
});
@ -39,7 +39,7 @@ test("amount subtraction (simple)", (t) => {
const a1 = amt(2, 5, "EUR");
const a2 = amt(1, 0, "EUR");
const a3 = amt(1, 5, "EUR");
t.true(0 === types.Amounts.cmp(Amounts.sub(a1, a2).amount, a3));
t.true(0 === Amounts.cmp(Amounts.sub(a1, a2).amount, a3));
t.pass();
});
@ -73,13 +73,13 @@ test("contract terms validation", (t) => {
wire_method: "test",
};
types.ContractTerms.checked(c);
ContractTerms.checked(c);
const c1 = JSON.parse(JSON.stringify(c));
c1.exchanges = [];
try {
types.ContractTerms.checked(c1);
ContractTerms.checked(c1);
} catch (e) {
t.pass();
return;

File diff suppressed because it is too large Load Diff

View File

@ -15,13 +15,19 @@
*/
import {test} from "ava";
import * as types from "./types";
import { test } from "ava";
import * as dbTypes from "./dbTypes";
import * as types from "./walletTypes";
import * as wallet from "./wallet";
import { AmountJson} from "./amounts";
import * as Amounts from "./amounts";
function a(x: string): types.AmountJson {
const amt = types.Amounts.parse(x);
function a(x: string): AmountJson {
const amt = Amounts.parse(x);
if (!amt) {
throw Error("invalid amount");
}
@ -40,7 +46,7 @@ function fakeCwd(current: string, value: string, feeDeposit: string): types.Coin
denomSig: "(mock)",
exchangeBaseUrl: "(mock)",
reservePub: "(mock)",
status: types.CoinStatus.Fresh,
status: dbTypes.CoinStatus.Fresh,
},
denom: {
denomPub: "(mock)",
@ -56,7 +62,7 @@ function fakeCwd(current: string, value: string, feeDeposit: string): types.Coin
stampExpireLegal: "(mock)",
stampExpireWithdraw: "(mock)",
stampStart: "(mock)",
status: types.DenominationStatus.VerifiedGood,
status: dbTypes.DenominationStatus.VerifiedGood,
value: a(value),
},
};

View File

@ -43,56 +43,64 @@ import {
QueryRoot,
Store,
} from "./query";
import {TimerGroup} from "./timer";
import { TimerGroup } from "./timer";
import { AmountJson } from "./amounts";
import * as Amounts from "./amounts";
import {
AmountJson,
Amounts,
Auditor,
CheckPayResult,
CoinPaySig,
CoinRecord,
CoinSelectionResult,
CoinStatus,
CoinWithDenom,
ConfirmPayResult,
ConfirmReserveRequest,
ContractTerms,
CreateReserveRequest,
CreateReserveResponse,
CurrencyRecord,
Denomination,
DenominationRecord,
DenominationStatus,
ExchangeHandle,
ExchangeRecord,
ExchangeWireFeesRecord,
HistoryRecord,
Notifier,
PayCoinInfo,
PayReq,
PaybackConfirmation,
PreCoinRecord,
ProposalRecord,
PurchaseRecord,
QueryPaymentResult,
RefreshPreCoinRecord,
RefreshSessionRecord,
RefundPermission,
ReserveCreationInfo,
ReserveRecord,
TipRecord,
WireFee,
} from "./dbTypes";
import URI = require("urijs");
import {
Auditor,
CoinPaySig,
ContractTerms,
Denomination,
ExchangeHandle,
PayReq,
PaybackConfirmation,
RefundPermission,
TipPlanchetDetail,
TipResponse,
} from "./talerTypes";
import {
CheckPayResult,
CoinSelectionResult,
CoinWithDenom,
ConfirmPayResult,
ConfirmReserveRequest,
CreateReserveRequest,
CreateReserveResponse,
HistoryRecord,
Notifier,
PayCoinInfo,
QueryPaymentResult,
ReserveCreationInfo,
ReturnCoinsRequest,
SenderWireInfos,
TipPlanchetDetail,
TipRecord,
TipResponse,
TipStatus,
WalletBalance,
WalletBalanceEntry,
WireFee,
WireInfo,
} from "./types";
import URI = require("urijs");
} from "./walletTypes";
/**
@ -561,7 +569,9 @@ export namespace Stores {
super("purchases", {keyPath: "contractTermsHash"});
}
fulfillmentUrlIndex = new Index<string, PurchaseRecord>(this, "fulfillmentUrlIndex", "contractTerms.fulfillment_url");
fulfillmentUrlIndex = new Index<string, PurchaseRecord>(this,
"fulfillmentUrlIndex",
"contractTerms.fulfillment_url");
orderIdIndex = new Index<string, PurchaseRecord>(this, "orderIdIndex", "contractTerms.order_id");
timestampIndex = new Index<string, PurchaseRecord>(this, "timestampIndex", "timestamp");
}
@ -1077,7 +1087,7 @@ export class Wallet {
if (!sp) {
return;
}
if (sp.proposalId != proposalId) {
if (sp.proposalId !== proposalId) {
return;
}
const coinKeys = sp.payCoinInfo.updatedCoins.map(x => x.coinPub);
@ -1090,8 +1100,8 @@ export class Wallet {
if (!currentCoin) {
return;
}
if (Amounts.cmp(specCoin.currentAmount, currentCoin.currentAmount) != 0) {
return
if (Amounts.cmp(specCoin.currentAmount, currentCoin.currentAmount) !== 0) {
return;
}
}
return sp;
@ -1135,7 +1145,7 @@ export class Wallet {
}
// Only create speculative signature if we don't already have one for this proposal
if ((!this.speculativePayData) || (this.speculativePayData && this.speculativePayData.proposalId != proposalId)) {
if ((!this.speculativePayData) || (this.speculativePayData && this.speculativePayData.proposalId !== proposalId)) {
const { exchangeUrl, cds } = res;
const payCoinInfo = await this.cryptoApi.signDeposit(proposal.contractTerms, cds);
this.speculativePayData = {
@ -1250,7 +1260,7 @@ export class Wallet {
.finish();
if (coin.status === CoinStatus.TainedByTip) {
let tip = await this.q().getIndexed(Stores.tips.coinPubIndex, coin.coinPub);
const tip = await this.q().getIndexed(Stores.tips.coinPubIndex, coin.coinPub);
if (!tip) {
throw Error(`inconsistent DB: tip for coin pub ${coin.coinPub} not found.`);
}
@ -1263,8 +1273,8 @@ export class Wallet {
c.status = CoinStatus.Fresh;
}
return c;
}
await this.q().mutate(Stores.coins, coin.coinPub, mutateCoin)
};
await this.q().mutate(Stores.coins, coin.coinPub, mutateCoin);
// Show notifications only for accepted tips
this.badge.showNotification();
}
@ -1724,6 +1734,7 @@ export class Wallet {
const ret: ReserveCreationInfo = {
earliestDepositExpiration,
exchangeInfo,
exchangeVersion: exchangeInfo.protocolVersion || "unknown",
isAudited,
isTrusted,
numOfferedDenoms: possibleDenoms.length,
@ -1731,11 +1742,10 @@ export class Wallet {
selectedDenoms,
trustedAuditorPubs,
versionMatch,
walletVersion: WALLET_PROTOCOL_VERSION,
wireFees,
wireInfo,
withdrawFee: acc,
exchangeVersion: exchangeInfo.protocolVersion || "unknown",
walletVersion: WALLET_PROTOCOL_VERSION,
};
return ret;
}
@ -1779,7 +1789,7 @@ export class Wallet {
.indexJoinLeft(Stores.denominations.exchangeBaseUrlIndex,
(e) => e.exchangeBaseUrl)
.fold((cd: JoinLeftResult<CoinRecord, DenominationRecord>,
suspendedCoins: CoinRecord[]) => {
suspendedCoins: CoinRecord[]) => {
if ((!cd.right) || (!cd.right.isOffered)) {
return Array.prototype.concat(suspendedCoins, [cd.left]);
}
@ -1922,8 +1932,7 @@ export class Wallet {
this.q().iterIndex(Stores.denominations.exchangeBaseUrlIndex,
exchangeInfo.baseUrl)
.fold((x: DenominationRecord,
acc: typeof existingDenoms) => (acc[x.denomPub] = x, acc),
{})
acc: typeof existingDenoms) => (acc[x.denomPub] = x, acc), {})
);
const newDenoms: typeof existingDenoms = {};
@ -2432,9 +2441,9 @@ export class Wallet {
for (const tip of tips) {
history.push({
detail: {
merchantDomain: tip.merchantDomain,
amount: tip.amount,
accepted: tip.accepted,
amount: tip.amount,
merchantDomain: tip.merchantDomain,
tipId: tip.tipId,
},
timestamp: tip.timestamp,
@ -2760,8 +2769,8 @@ export class Wallet {
H_wire: coinsReturnRecord.contractTerms.H_wire,
coin_pub: c.coinPaySig.coin_pub,
coin_sig: c.coinPaySig.coin_sig,
denom_pub: c.coinPaySig.denom_pub,
contribution: c.coinPaySig.contribution,
denom_pub: c.coinPaySig.denom_pub,
h_contract_terms: coinsReturnRecord.contractTermsHash,
merchant_pub: coinsReturnRecord.contractTerms.merchant_pub,
pay_deadline: coinsReturnRecord.contractTerms.pay_deadline,
@ -2950,7 +2959,12 @@ export class Wallet {
* Get planchets for a tip. Creates new planchets if they don't exist already
* for this tip. The tip is uniquely identified by the merchant's domain and the tip id.
*/
async getTipPlanchets(merchantDomain: string, tipId: string, amount: AmountJson, deadline: number, exchangeUrl: string, nextUrl: string): Promise<TipPlanchetDetail[]> {
async getTipPlanchets(merchantDomain: string,
tipId: string,
amount: AmountJson,
deadline: number,
exchangeUrl: string,
nextUrl: string): Promise<TipPlanchetDetail[]> {
let tipRecord = await this.q().get(Stores.tips, [tipId, merchantDomain]);
if (!tipRecord) {
await this.updateExchangeFromUrl(exchangeUrl);
@ -2973,9 +2987,9 @@ export class Wallet {
await this.q().put(Stores.tips, tipRecord).finish();
}
// Planchets in the form that the merchant expects
const planchetDetail: TipPlanchetDetail[]= tipRecord.planchets.map((p) => ({
denom_pub_hash: p.denomPubHash,
const planchetDetail: TipPlanchetDetail[] = tipRecord.planchets.map((p) => ({
coin_ev: p.coinEv,
denom_pub_hash: p.denomPubHash,
}));
return planchetDetail;
}
@ -2985,7 +2999,7 @@ export class Wallet {
* These coins will not appear in the wallet yet.
*/
async processTipResponse(merchantDomain: string, tipId: string, response: TipResponse): Promise<void> {
let tipRecord = await this.q().get(Stores.tips, [tipId, merchantDomain]);
const tipRecord = await this.q().get(Stores.tips, [tipId, merchantDomain]);
if (!tipRecord) {
throw Error("tip not found");
}
@ -2995,18 +3009,18 @@ export class Wallet {
}
for (let i = 0; i < tipRecord.planchets.length; i++) {
let planchet = tipRecord.planchets[i];
let preCoin = {
coinPub: planchet.coinPub,
coinPriv: planchet.coinPriv,
coinEv: planchet.coinEv,
coinValue: planchet.coinValue,
reservePub: response.reserve_pub,
denomPub: planchet.denomPub,
const planchet = tipRecord.planchets[i];
const preCoin = {
blindingKey: planchet.blindingKey,
withdrawSig: response.reserve_sigs[i].reserve_sig,
coinEv: planchet.coinEv,
coinPriv: planchet.coinPriv,
coinPub: planchet.coinPub,
coinValue: planchet.coinValue,
denomPub: planchet.denomPub,
exchangeBaseUrl: tipRecord.exchangeUrl,
isFromTip: true,
reservePub: response.reserve_pub,
withdrawSig: response.reserve_sigs[i].reserve_sig,
};
await this.q().put(Stores.precoins, preCoin);
this.processPreCoin(preCoin);
@ -3082,8 +3096,8 @@ export class Wallet {
const gcProposal = (d: ProposalRecord, n: number) => {
// Delete proposal after 60 minutes or 5 minutes before pay deadline,
// whatever comes first.
let deadlinePayMilli = getTalerStampSec(d.contractTerms.pay_deadline)! * 1000;
let deadlineExpireMilli = nowMilli + (1000 * 60 * 60);
const deadlinePayMilli = getTalerStampSec(d.contractTerms.pay_deadline)! * 1000;
const deadlineExpireMilli = nowMilli + (1000 * 60 * 60);
return d.timestamp < Math.min(deadlinePayMilli, deadlineExpireMilli);
};
await this.q().deleteIf(Stores.proposals, gcProposal).finish();
@ -3096,7 +3110,7 @@ export class Wallet {
}
activeExchanges.push(d.baseUrl);
return false;
}
};
await this.q().deleteIf(Stores.exchanges, gcExchange).finish();

572
src/walletTypes.ts Normal file
View File

@ -0,0 +1,572 @@
/*
This file is part of TALER
(C) 2015-2017 GNUnet e.V. and INRIA
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.
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
TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
/**
* Types used by clients of the wallet.
*
* These types are defined in a separate file make tree shaking easier, since
* some components use these types (via RPC) but do not depend on the wallet
* code directly.
*/
/**
* Imports.
*/
import { Checkable } from "./checkable";
import * as LibtoolVersion from "./libtoolVersion";
import { AmountJson } from "./amounts";
import {
CoinRecord,
DenominationRecord,
ExchangeRecord,
ExchangeWireFeesRecord,
TipRecord,
} from "./dbTypes";
import {
CoinPaySig,
ContractTerms,
PayReq,
TipResponse,
} from "./talerTypes";
/**
* Response for the create reserve request to the wallet.
*/
@Checkable.Class()
export class CreateReserveResponse {
/**
* Exchange URL where the bank should create the reserve.
* The URL is canonicalized in the response.
*/
@Checkable.String
exchange: string;
/**
* Reserve public key of the newly created reserve.
*/
@Checkable.String
reservePub: string;
/**
* Verify that a value matches the schema of this class and convert it into a
* member.
*/
static checked: (obj: any) => CreateReserveResponse;
}
/**
* Wire info, sent to the bank when creating a reserve. Fee information will
* be filtered out. Only methods that the bank also supports should be sent.
*/
export interface WireInfo {
/**
* Mapping from wire method type to the exchange's wire info,
* excluding fees.
*/
[type: string]: any;
}
/**
* Information about what will happen when creating a reserve.
*
* Sent to the wallet frontend to be rendered and shown to the user.
*/
export interface ReserveCreationInfo {
/**
* Exchange that the reserve will be created at.
*/
exchangeInfo: ExchangeRecord;
/**
* Filtered wire info to send to the bank.
*/
wireInfo: WireInfo;
/**
* Selected denominations for withdraw.
*/
selectedDenoms: DenominationRecord[];
/**
* Fees for withdraw.
*/
withdrawFee: AmountJson;
/**
* Remaining balance that is too small to be withdrawn.
*/
overhead: AmountJson;
/**
* Wire fees from the exchange.
*/
wireFees: ExchangeWireFeesRecord;
/**
* Does the wallet know about an auditor for
* the exchange that the reserve.
*/
isAudited: boolean;
/**
* The exchange is trusted directly.
*/
isTrusted: boolean;
/**
* The earliest deposit expiration of the selected coins.
*/
earliestDepositExpiration: number;
/**
* Number of currently offered denominations.
*/
numOfferedDenoms: number;
/**
* Public keys of trusted auditors for the currency we're withdrawing.
*/
trustedAuditorPubs: string[];
/**
* Result of checking the wallet's version
* against the exchange's version.
*
* Older exchanges don't return version information.
*/
versionMatch: LibtoolVersion.VersionMatchResult|undefined;
/**
* Libtool-style version string for the exchange or "unknown"
* for older exchanges.
*/
exchangeVersion: string;
/**
* Libtool-style version string for the wallet.
*/
walletVersion: string;
}
/**
* Mapping from currency/exchange to detailed balance
* information.
*/
export interface WalletBalance {
/**
* Mapping from currency name to detailed balance info.
*/
byExchange: { [exchangeBaseUrl: string]: WalletBalanceEntry };
/**
* Mapping from currency name to detailed balance info.
*/
byCurrency: { [currency: string]: WalletBalanceEntry };
}
/**
* Detailed wallet balance for a particular currency.
*/
export interface WalletBalanceEntry {
/**
* Directly available amount.
*/
available: AmountJson;
/**
* Amount that we're waiting for (refresh, withdrawal).
*/
pendingIncoming: AmountJson;
/**
* Amount that's marked for a pending payment.
*/
pendingPayment: AmountJson;
/**
* Amount that was paid back and we could withdraw again.
*/
paybackAmount: AmountJson;
}
/**
* Coins used for a payment, with signatures authorizing the payment and the
* coins with remaining value updated to accomodate for a payment.
*/
export interface PayCoinInfo {
originalCoins: CoinRecord[];
updatedCoins: CoinRecord[];
sigs: CoinPaySig[];
}
/**
* Listener for notifications from the wallet.
*/
export interface Notifier {
/**
* Called when a new notification arrives.
*/
notify(): void;
}
/**
* For terseness.
*/
export function mkAmount(value: number, fraction: number, currency: string): AmountJson {
return {value, fraction, currency};
}
/**
* Possible results for checkPay.
*/
export interface CheckPayResult {
status: "paid" | "payment-possible" | "insufficient-balance";
coinSelection?: CoinSelectionResult;
}
/**
* Possible results for confirmPay.
*/
export type ConfirmPayResult = "paid" | "insufficient-balance";
/**
* Activity history record.
*/
export interface HistoryRecord {
/**
* Type of the history event.
*/
type: string;
/**
* Time when the activity was recorded.
*/
timestamp: number;
/**
* Subject of the entry. Used to group multiple history records together.
* Only the latest history record with the same subjectId will be shown.
*/
subjectId?: string;
/**
* Details used when rendering the history record.
*/
detail: any;
}
/**
* Response to a query payment request. Tagged union over the 'found' field.
*/
export type QueryPaymentResult = QueryPaymentNotFound | QueryPaymentFound;
/**
* Query payment response when the payment was found.
*/
export interface QueryPaymentNotFound {
found: false;
}
/**
* Query payment response when the payment wasn't found.
*/
export interface QueryPaymentFound {
found: true;
contractTermsHash: string;
contractTerms: ContractTerms;
payReq: PayReq;
}
/**
* Information about all sender wire details known to the wallet,
* as well as exchanges that accept these wire types.
*/
export interface SenderWireInfos {
/**
* Mapping from exchange base url to list of accepted
* wire types.
*/
exchangeWireTypes: { [exchangeBaseUrl: string]: string[] };
/**
* Sender wire types stored in the wallet.
*/
senderWires: object[];
}
/**
* Request to mark a reserve as confirmed.
*/
@Checkable.Class()
export class CreateReserveRequest {
/**
* The initial amount for the reserve.
*/
@Checkable.Value(AmountJson)
amount: AmountJson;
/**
* Exchange URL where the bank should create the reserve.
*/
@Checkable.String
exchange: string;
/**
* Wire details for the bank account that sent the funds to the exchange.
*/
@Checkable.Optional(Checkable.Any)
senderWire?: object;
/**
* Verify that a value matches the schema of this class and convert it into a
* member.
*/
static checked: (obj: any) => CreateReserveRequest;
}
/**
* Request to mark a reserve as confirmed.
*/
@Checkable.Class()
export class ConfirmReserveRequest {
/**
* Public key of then reserve that should be marked
* as confirmed.
*/
@Checkable.String
reservePub: string;
/**
* Verify that a value matches the schema of this class and convert it into a
* member.
*/
static checked: (obj: any) => ConfirmReserveRequest;
}
/**
* Wire coins to the user's own bank account.
*/
@Checkable.Class()
export class ReturnCoinsRequest {
/**
* The amount to wire.
*/
@Checkable.Value(AmountJson)
amount: AmountJson;
/**
* The exchange to take the coins from.
*/
@Checkable.String
exchange: string;
/**
* Wire details for the bank account of the customer that will
* receive the funds.
*/
@Checkable.Any
senderWire?: object;
/**
* Verify that a value matches the schema of this class and convert it into a
* member.
*/
static checked: (obj: any) => ReturnCoinsRequest;
}
/**
* Result of selecting coins, contains the exchange, and selected
* coins with their denomination.
*/
export interface CoinSelectionResult {
exchangeUrl: string;
cds: CoinWithDenom[];
totalFees: AmountJson;
}
/**
* Named tuple of coin and denomination.
*/
export interface CoinWithDenom {
/**
* A coin. Must have the same denomination public key as the associated
* denomination.
*/
coin: CoinRecord;
/**
* An associated denomination.
*/
denom: DenominationRecord;
}
/**
* Status of processing a tip.
*/
export interface TipStatus {
tip: TipRecord;
rci?: ReserveCreationInfo;
}
/**
* Request to the wallet for the status of processing a tip.
*/
@Checkable.Class()
export class TipStatusRequest {
/**
* Identifier of the tip.
*/
@Checkable.String
tipId: string;
/**
* Merchant domain. Within each merchant domain, the tip identifier
* uniquely identifies a tip.
*/
@Checkable.String
merchantDomain: string;
/**
* Create a TipStatusRequest from untyped JSON.
*/
static checked: (obj: any) => TipStatusRequest;
}
/**
* Request to the wallet to accept a tip.
*/
@Checkable.Class()
export class AcceptTipRequest {
/**
* Identifier of the tip.
*/
@Checkable.String
tipId: string;
/**
* Merchant domain. Within each merchant domain, the tip identifier
* uniquely identifies a tip.
*/
@Checkable.String
merchantDomain: string;
/**
* Create an AcceptTipRequest from untyped JSON.
* Validates the schema and throws on error.
*/
static checked: (obj: any) => AcceptTipRequest;
}
/**
* Request for the wallet to process a tip response from a merchant.
*/
@Checkable.Class()
export class ProcessTipResponseRequest {
/**
* Identifier of the tip.
*/
@Checkable.String
tipId: string;
/**
* Merchant domain. Within each merchant domain, the tip identifier
* uniquely identifies a tip.
*/
@Checkable.String
merchantDomain: string;
/**
* Tip response from the merchant.
*/
@Checkable.Value(TipResponse)
tipResponse: TipResponse;
/**
* Create an AcceptTipRequest from untyped JSON.
* Validates the schema and throws on error.
*/
static checked: (obj: any) => ProcessTipResponseRequest;
}
/**
* Request for the wallet to generate tip planchets.
*/
@Checkable.Class()
export class GetTipPlanchetsRequest {
/**
* Identifier of the tip.
*/
@Checkable.String
tipId: string;
/**
* Merchant domain. Within each merchant domain, the tip identifier
* uniquely identifies a tip.
*/
@Checkable.String
merchantDomain: string;
/**
* Amount of the tip.
*/
@Checkable.Optional(Checkable.Value(AmountJson))
amount: AmountJson;
/**
* Deadline for picking up the tip.
*/
@Checkable.Number
deadline: number;
/**
* Exchange URL that must be used to pick up the tip.
*/
@Checkable.String
exchangeUrl: string;
/**
* URL to nagivate to after processing the tip.
*/
@Checkable.String
nextUrl: string;
/**
* Create an AcceptTipRequest from untyped JSON.
* Validates the schema and throws on error.
*/
static checked: (obj: any) => GetTipPlanchetsRequest;
}

View File

@ -21,7 +21,10 @@
// Messages are already documented in wxApi.
/* tslint:disable:completed-docs */
import * as types from "../types";
import { AmountJson } from "../amounts";
import * as dbTypes from "../dbTypes";
import * as talerTypes from "../talerTypes";
import * as walletTypes from "../walletTypes";
/**
* Message type information.
@ -29,7 +32,7 @@ import * as types from "../types";
export interface MessageMap {
"balances": {
request: { };
response: types.WalletBalance;
response: walletTypes.WalletBalance;
};
"dump-db": {
request: { };
@ -55,7 +58,7 @@ export interface MessageMap {
};
"create-reserve": {
request: {
amount: types.AmountJson;
amount: AmountJson;
exchange: string
};
response: void;
@ -70,11 +73,11 @@ export interface MessageMap {
};
"confirm-pay": {
request: { proposalId: number; };
response: types.ConfirmPayResult;
response: walletTypes.ConfirmPayResult;
};
"check-pay": {
request: { proposalId: number; };
response: types.CheckPayResult;
response: walletTypes.CheckPayResult;
};
"query-payment": {
request: { };
@ -82,31 +85,31 @@ export interface MessageMap {
};
"exchange-info": {
request: { baseUrl: string };
response: types.ExchangeRecord;
response: dbTypes.ExchangeRecord;
};
"currency-info": {
request: { name: string };
response: types.CurrencyRecord;
response: dbTypes.CurrencyRecord;
};
"hash-contract": {
request: { contract: object };
response: string;
};
"save-proposal": {
request: { proposal: types.ProposalRecord };
request: { proposal: dbTypes.ProposalRecord };
response: void;
};
"reserve-creation-info": {
request: { baseUrl: string, amount: types.AmountJson };
response: types.ReserveCreationInfo;
request: { baseUrl: string, amount: AmountJson };
response: walletTypes.ReserveCreationInfo;
};
"get-history": {
request: { };
response: types.HistoryRecord[];
response: walletTypes.HistoryRecord[];
};
"get-proposal": {
request: { proposalId: number };
response: types.ProposalRecord | undefined;
response: dbTypes.ProposalRecord | undefined;
};
"get-coins": {
request: { exchangeBaseUrl: string };
@ -118,23 +121,23 @@ export interface MessageMap {
};
"get-currencies": {
request: { };
response: types.CurrencyRecord[];
response: dbTypes.CurrencyRecord[];
};
"update-currency": {
request: { currencyRecord: types.CurrencyRecord };
request: { currencyRecord: dbTypes.CurrencyRecord };
response: void;
};
"get-exchanges": {
request: { };
response: types.ExchangeRecord[];
response: dbTypes.ExchangeRecord[];
};
"get-reserves": {
request: { exchangeBaseUrl: string };
response: types.ReserveRecord[];
response: dbTypes.ReserveRecord[];
};
"get-payback-reserves": {
request: { };
response: types.ReserveRecord[];
response: dbTypes.ReserveRecord[];
};
"withdraw-payback-reserve": {
request: { reservePub: string };
@ -142,11 +145,11 @@ export interface MessageMap {
};
"get-precoins": {
request: { exchangeBaseUrl: string };
response: types.PreCoinRecord[];
response: dbTypes.PreCoinRecord[];
};
"get-denoms": {
request: { exchangeBaseUrl: string };
response: types.DenominationRecord[];
response: dbTypes.DenominationRecord[];
};
"payback-coin": {
request: { coinPub: string };
@ -189,23 +192,23 @@ export interface MessageMap {
response: void;
};
"get-full-refund-fees": {
request: { refundPermissions: types.RefundPermission[] };
request: { refundPermissions: talerTypes.RefundPermission[] };
response: void;
};
"get-tip-planchets": {
request: types.GetTipPlanchetsRequest;
request: walletTypes.GetTipPlanchetsRequest;
response: void;
};
"process-tip-response": {
request: types.ProcessTipResponseRequest;
request: walletTypes.ProcessTipResponseRequest;
response: void;
};
"accept-tip": {
request: types.AcceptTipRequest;
request: walletTypes.AcceptTipRequest;
response: void;
};
"get-tip-status": {
request: types.TipStatusRequest;
request: walletTypes.TipStatusRequest;
response: void;
};
"clear-notification": {

View File

@ -29,7 +29,8 @@ import URI = require("urijs");
import wxApi = require("./wxApi");
import { getTalerStampSec } from "../helpers";
import { TipToken, QueryPaymentResult } from "../types";
import { TipToken } from "../talerTypes";
import { QueryPaymentResult } from "../walletTypes";
import axios from "axios";
@ -272,7 +273,12 @@ function talerPay(msg: any): Promise<any> {
const merchantDomain = new URI(document.location.href).origin();
let walletResp;
try {
walletResp = await wxApi.getTipPlanchets(merchantDomain, tipToken.tip_id, tipToken.amount, deadlineSec, tipToken.exchange_url, tipToken.next_url);
walletResp = await wxApi.getTipPlanchets(merchantDomain,
tipToken.tip_id,
tipToken.amount,
deadlineSec,
tipToken.exchange_url,
tipToken.next_url);
} catch (e) {
wxApi.logAndDisplayError({
message: e.message,
@ -283,12 +289,12 @@ function talerPay(msg: any): Promise<any> {
throw e;
}
let planchets = walletResp;
const planchets = walletResp;
if (!planchets) {
wxApi.logAndDisplayError({
message: "processing tip failed",
detail: walletResp,
message: "processing tip failed",
name: "tipping-failed",
sameTab: true,
});

View File

@ -23,7 +23,7 @@
import {
CurrencyRecord,
} from "../../types";
} from "../../dbTypes";
import { ImplicitStateComponent, StateHolder } from "../components";
import {

View File

@ -25,7 +25,7 @@ import {
AuditorRecord,
CurrencyRecord,
ExchangeForCurrencyRecord,
} from "../../types";
} from "../../dbTypes";
import {
getCurrencies,

View File

@ -24,12 +24,15 @@
* Imports.
*/
import * as i18n from "../../i18n";
import {
CheckPayResult,
ContractTerms,
ExchangeRecord,
ProposalRecord,
} from "../../types";
} from "../../dbTypes";
import { ContractTerms } from "../../talerTypes";
import {
CheckPayResult,
} from "../../walletTypes";
import { renderAmount } from "../renderHtml";
import * as wxApi from "../wxApi";

View File

@ -24,13 +24,17 @@
import { canonicalizeBaseUrl } from "../../helpers";
import * as i18n from "../../i18n";
import { AmountJson } from "../../amounts";
import * as Amounts from "../../amounts";
import {
AmountJson,
Amounts,
CreateReserveResponse,
CurrencyRecord,
} from "../../dbTypes";
import {
CreateReserveResponse,
ReserveCreationInfo,
} from "../../types";
} from "../../walletTypes";
import { ImplicitStateComponent, StateHolder } from "../components";
import {
@ -40,7 +44,10 @@ import {
getReserveCreationInfo,
} from "../wxApi";
import { renderAmount, WithdrawDetailView } from "../renderHtml";
import {
WithdrawDetailView,
renderAmount,
} from "../renderHtml";
import * as React from "react";
import * as ReactDOM from "react-dom";
@ -78,8 +85,6 @@ class EventTrigger {
}
interface ExchangeSelectionProps {
suggestedExchangeUrl: string;
amount: AmountJson;
@ -273,7 +278,8 @@ class ExchangeSelection extends ImplicitStateComponent<ExchangeSelectionProps> {
if (rci.versionMatch.currentCmp === -1) {
return (
<p className="errorbox">
Your wallet (protocol version <span>{rci.walletVersion}</span>) might be outdated. The exchange has a higher, incompatible
Your wallet (protocol version <span>{rci.walletVersion}</span>) might be outdated.<span> </span>
The exchange has a higher, incompatible
protocol version (<span>{rci.exchangeVersion}</span>).
</p>
);
@ -281,7 +287,8 @@ class ExchangeSelection extends ImplicitStateComponent<ExchangeSelectionProps> {
if (rci.versionMatch.currentCmp === 1) {
return (
<p className="errorbox">
The chosen exchange (protocol version <span>{rci.exchangeVersion}</span> might be outdated. The exchange has a lower, incompatible
The chosen exchange (protocol version <span>{rci.exchangeVersion}</span> might be outdated.<span> </span>
The exchange has a lower, incompatible
protocol version than your wallet (protocol version <span>{rci.walletVersion}</span>).
</p>
);
@ -429,8 +436,8 @@ class ExchangeSelection extends ImplicitStateComponent<ExchangeSelectionProps> {
amount_fraction: amount.fraction,
amount_value: amount.value,
exchange: resp.exchange,
reserve_pub: resp.reservePub,
exchange_wire_details: JSON.stringify(filteredWireDetails),
reserve_pub: resp.reservePub,
};
const url = new URI(callback_url).addQuery(q);
if (!url.is("absolute")) {

View File

@ -26,7 +26,7 @@
*/
import {
ReserveRecord,
} from "../../types";
} from "../../dbTypes";
import { ImplicitStateComponent, StateHolder } from "../components";
import { renderAmount } from "../renderHtml";

View File

@ -26,13 +26,15 @@
* Imports.
*/
import * as i18n from "../../i18n";
import { AmountJson } from "../../amounts";
import * as Amounts from "../../amounts";
import {
AmountJson,
Amounts,
HistoryRecord,
WalletBalance,
WalletBalanceEntry,
} from "../../types";
} from "../../walletTypes";
import { abbrev, renderAmount } from "../renderHtml";
import * as wxApi from "../wxApi";
@ -407,7 +409,8 @@ function formatHistoryItem(historyItem: HistoryRecord) {
const url = tipPageUrl.query(params).href();
return (
<i18n.Translate wrap="p">
Merchant <span>{d.merchantDomain}</span> gave a <a href={url} onClick={openTab(url)}> tip</a> of <span>{renderAmount(d.amount)}</span>.
Merchant <span>{d.merchantDomain}</span> gave
a <a href={url} onClick={openTab(url)}> tip</a> of <span>{renderAmount(d.amount)}</span>.
<span> </span>
{ d.accepted ? null : <span>You did not accept the tip yet.</span> }
</i18n.Translate>

View File

@ -26,7 +26,10 @@ import * as React from "react";
import * as ReactDOM from "react-dom";
import URI = require("urijs");
import * as types from "../../types";
import * as dbTypes from "../../dbTypes";
import { AmountJson } from "../../amounts";
import * as Amounts from "../../amounts";
import { AmountDisplay } from "../renderHtml";
import * as wxApi from "../wxApi";
@ -36,14 +39,14 @@ interface RefundStatusViewProps {
}
interface RefundStatusViewState {
purchase?: types.PurchaseRecord;
refundFees?: types.AmountJson;
purchase?: dbTypes.PurchaseRecord;
refundFees?: AmountJson;
gotResult: boolean;
}
interface RefundDetailProps {
purchase: types.PurchaseRecord;
fullRefundFees: types.AmountJson;
purchase: dbTypes.PurchaseRecord;
fullRefundFees: AmountJson;
}
const RefundDetail = ({purchase, fullRefundFees}: RefundDetailProps) => {
@ -59,13 +62,13 @@ const RefundDetail = ({purchase, fullRefundFees}: RefundDetailProps) => {
throw Error("invariant");
}
let amountPending = types.Amounts.getZero(currency);
let amountPending = Amounts.getZero(currency);
for (const k of pendingKeys) {
amountPending = types.Amounts.add(amountPending, purchase.refundsPending[k].refund_amount).amount;
amountPending = Amounts.add(amountPending, purchase.refundsPending[k].refund_amount).amount;
}
let amountDone = types.Amounts.getZero(currency);
let amountDone = Amounts.getZero(currency);
for (const k of doneKeys) {
amountDone = types.Amounts.add(amountDone, purchase.refundsDone[k].refund_amount).amount;
amountDone = Amounts.add(amountDone, purchase.refundsDone[k].refund_amount).amount;
}
const hasPending = amountPending.fraction !== 0 || amountPending.value !== 0;

View File

@ -25,12 +25,13 @@
* Imports.
*/
import { AmountJson } from "../../amounts";
import * as Amounts from "../../amounts";
import {
AmountJson,
Amounts,
SenderWireInfos,
WalletBalance,
} from "../../types";
} from "../../walletTypes";
import * as i18n from "../../i18n";

View File

@ -33,9 +33,13 @@ import {
getTipStatus,
} from "../wxApi";
import { renderAmount, WithdrawDetailView } from "../renderHtml";
import {
WithdrawDetailView,
renderAmount,
} from "../renderHtml";
import { Amounts, TipStatus } from "../../types";
import * as Amounts from "../../amounts";
import { TipStatus } from "../../walletTypes";
interface TipDisplayProps {
merchantDomain: string;
@ -54,7 +58,7 @@ class TipDisplay extends React.Component<TipDisplayProps, TipDisplayState> {
}
async update() {
let tipStatus = await getTipStatus(this.props.merchantDomain, this.props.tipId);
const tipStatus = await getTipStatus(this.props.merchantDomain, this.props.tipId);
this.setState({ tipStatus });
}
@ -73,7 +77,7 @@ class TipDisplay extends React.Component<TipDisplayProps, TipDisplayState> {
renderExchangeInfo(ts: TipStatus) {
const rci = ts.rci;
if (!rci) {
return <p>Waiting for info about exchange ...</p>
return <p>Waiting for info about exchange ...</p>;
}
const totalCost = Amounts.add(rci.overhead, rci.withdrawFee).amount;
return (
@ -102,7 +106,9 @@ class TipDisplay extends React.Component<TipDisplayProps, TipDisplayState> {
className="pure-button pure-button-primary"
type="button"
onClick={() => this.accept()}>
{ this.state.working ? <span><object className="svg-icon svg-baseline" data="/img/spinner-bars.svg" /> </span> : null }
{ this.state.working
? <span><object className="svg-icon svg-baseline" data="/img/spinner-bars.svg" /> </span>
: null }
Accept tip
</button>
{" "}
@ -119,7 +125,8 @@ class TipDisplay extends React.Component<TipDisplayProps, TipDisplayState> {
return (
<div>
<h2>Tip Received!</h2>
<p>You received a tip of <strong>{renderAmount(ts.tip.amount)}</strong> from <strong>{this.props.merchantDomain}</strong>.</p>
<p>You received a tip of <strong>{renderAmount(ts.tip.amount)}</strong> from <span> </span>
<strong>{this.props.merchantDomain}</strong>.</p>
{ts.tip.accepted
? <p>You've accepted this tip! <a href={ts.tip.nextUrl}>Go back to merchant</a></p>
: this.renderButtons()
@ -134,10 +141,10 @@ async function main() {
try {
const url = new URI(document.location.href);
const query: any = URI.parseQuery(url.query());
let merchantDomain = query.merchant_domain;
let tipId = query.tip_id;
let props: TipDisplayProps = { tipId, merchantDomain };
const merchantDomain = query.merchant_domain;
const tipId = query.tip_id;
const props: TipDisplayProps = { tipId, merchantDomain };
ReactDOM.render(<TipDisplay {...props} />,
document.getElementById("container")!);

View File

@ -22,6 +22,7 @@
import { getTalerStampDate } from "../../helpers";
import {
CoinRecord,
CoinStatus,
@ -29,7 +30,7 @@ import {
ExchangeRecord,
PreCoinRecord,
ReserveRecord,
} from "../../types";
} from "../../dbTypes";
import { ImplicitStateComponent, StateHolder } from "../components";
import {

View File

@ -24,12 +24,16 @@
/**
* Imports.
*/
import { AmountJson } from "../amounts";
import * as Amounts from "../amounts";
import {
AmountJson,
Amounts,
DenominationRecord,
} from "../dbTypes";
import {
ReserveCreationInfo,
} from "../types";
} from "../walletTypes";
import { ImplicitStateComponent } from "./components";
@ -239,7 +243,9 @@ function FeeDetailsView(props: {rci: ReserveCreationInfo|null}): JSX.Element {
);
}
/**
* Shows details about a withdraw request.
*/
export function WithdrawDetailView(props: {rci: ReserveCreationInfo | null}): JSX.Element {
const rci = props.rci;
return (
@ -259,6 +265,9 @@ interface ExpanderTextProps {
text: string;
}
/**
* Show a heading with a toggle to show/hide the expandable content.
*/
export class ExpanderText extends ImplicitStateComponent<ExpanderTextProps> {
private expanded = this.makeState<boolean>(false);
private textArea: any = undefined;

View File

@ -22,26 +22,31 @@
/**
* Imports.
*/
import { AmountJson } from "../amounts";
import {
AmountJson,
CheckPayResult,
CoinRecord,
ConfirmPayResult,
CurrencyRecord,
DenominationRecord,
ExchangeRecord,
PreCoinRecord,
PurchaseRecord,
QueryPaymentResult,
RefundPermission,
ReserveCreationInfo,
ReserveRecord,
} from "../dbTypes";
import {
CheckPayResult,
ConfirmPayResult,
QueryPaymentResult,
ReserveCreationInfo,
SenderWireInfos,
TipResponse,
TipPlanchetDetail,
TipStatus,
WalletBalance,
} from "../types";
} from "../walletTypes";
import {
RefundPermission,
TipPlanchetDetail,
TipResponse,
} from "../talerTypes";
import { MessageMap, MessageType } from "./messages";
@ -366,22 +371,39 @@ export function getFullRefundFees(args: { refundPermissions: RefundPermission[]
/**
* Get or generate planchets to give the merchant that wants to tip us.
*/
export function getTipPlanchets(merchantDomain: string, tipId: string, amount: AmountJson, deadline: number, exchangeUrl: string, nextUrl: string): Promise<TipPlanchetDetail[]> {
export function getTipPlanchets(merchantDomain: string,
tipId: string,
amount: AmountJson,
deadline: number,
exchangeUrl: string,
nextUrl: string): Promise<TipPlanchetDetail[]> {
return callBackend("get-tip-planchets", { merchantDomain, tipId, amount, deadline, exchangeUrl, nextUrl });
}
/**
* Get the status of processing a tip.
*/
export function getTipStatus(merchantDomain: string, tipId: string): Promise<TipStatus> {
return callBackend("get-tip-status", { merchantDomain, tipId });
}
/**
* Mark a tip as accepted by the user.
*/
export function acceptTip(merchantDomain: string, tipId: string): Promise<TipStatus> {
return callBackend("accept-tip", { merchantDomain, tipId });
}
/**
* Process a response from the merchant for a tip request.
*/
export function processTipResponse(merchantDomain: string, tipId: string, tipResponse: TipResponse): Promise<void> {
return callBackend("process-tip-response", { merchantDomain, tipId, tipResponse });
}
/**
* Clear notifications that the wallet shows to the user.
*/
export function clearNotification(): Promise<void> {
return callBackend("clear-notification", { });
}

View File

@ -30,18 +30,21 @@ import {
Index,
Store,
} from "../query";
import { AmountJson } from "../amounts";
import { ProposalRecord } from "../dbTypes";
import {
AcceptTipRequest,
AmountJson,
ConfirmReserveRequest,
CreateReserveRequest,
GetTipPlanchetsRequest,
Notifier,
ProcessTipResponseRequest,
ProposalRecord,
ReturnCoinsRequest,
TipStatusRequest,
} from "../types";
} from "../walletTypes";
import {
Stores,
WALLET_DB_VERSION,
@ -335,7 +338,12 @@ function handleMessage(sender: MessageSender,
}
case "get-tip-planchets": {
const req = GetTipPlanchetsRequest.checked(detail);
return needsWallet().getTipPlanchets(req.merchantDomain, req.tipId, req.amount, req.deadline, req.exchangeUrl, req.nextUrl);
return needsWallet().getTipPlanchets(req.merchantDomain,
req.tipId,
req.amount,
req.deadline,
req.exchangeUrl,
req.nextUrl);
}
case "clear-notification": {
return needsWallet().clearNotification();
@ -702,11 +710,10 @@ export async function wxMain() {
});
// Clear notifications both when the popop opens,
// as well when it closes.
chrome.runtime.onConnect.addListener((port) => {
if (port.name == "popup") {
if (port.name === "popup") {
if (currentWallet) {
currentWallet.clearNotification();
}

View File

@ -21,12 +21,10 @@
* @author Florian Dold
*/
/// <reference path="../decl/node.d.ts" />
"use strict";
import {readFileSync} from "fs";
import {execSync} from "child_process";
import { readFileSync } from "fs";
import { execSync } from "child_process";
import * as ts from "typescript";

View File

@ -1,4 +1,5 @@
{
"compileOnSave": true,
"compilerOptions": {
"target": "es6",
"jsx": "react",
@ -7,8 +8,8 @@
"module": "commonjs",
"sourceMap": true,
"lib": [
"ES6",
"DOM"
"es6",
"dom"
],
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
@ -23,6 +24,7 @@
"decl/chrome/chrome.d.ts",
"decl/jed.d.ts",
"decl/urijs.d.ts",
"src/amounts.ts",
"src/checkable.ts",
"src/crypto/cryptoApi-test.ts",
"src/crypto/cryptoApi.ts",
@ -34,6 +36,7 @@
"src/crypto/nodeWorker.ts",
"src/crypto/nodeWorkerEntry.ts",
"src/crypto/startWorker.js",
"src/dbTypes.ts",
"src/helpers-test.ts",
"src/helpers.ts",
"src/http.ts",
@ -42,18 +45,13 @@
"src/libtoolVersion-test.ts",
"src/libtoolVersion.ts",
"src/logging.ts",
"src/memidb/aatree-test.ts",
"src/memidb/aatree.ts",
"src/memidb/memidb-test.ts",
"src/memidb/memidb.ts",
"src/memidb/w3c-wpt/abort-in-initial-upgradeneeded-test.ts",
"src/memidb/w3c-wpt/support.ts",
"src/query.ts",
"src/talerTypes.ts",
"src/timer.ts",
"src/types-test.ts",
"src/types.ts",
"src/wallet-test.ts",
"src/wallet.ts",
"src/walletTypes.ts",
"src/webex/background.ts",
"src/webex/chromeBadge.ts",
"src/webex/components.ts",

View File

@ -5,6 +5,7 @@
],
"jsRules": {},
"rules": {
"arrow-parens": false,
"max-line-length": {
"options": [120]
},