aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorFlorian Dold <florian.dold@gmail.com>2018-01-29 22:58:47 +0100
committerFlorian Dold <florian.dold@gmail.com>2018-01-29 22:58:47 +0100
commit97f6e68ce3a515938228b9a4d3e41b5f4b25a015 (patch)
tree371331acc56b7c56abec502126c2a40cdb23a95f
parent9fe6dc596573f38b13f0b15c946b8bc16013fdd9 (diff)
change protocol to string amount network format
-rw-r--r--src/amounts.ts36
-rw-r--r--src/checkable.ts67
-rw-r--r--src/crypto/cryptoWorker.ts4
-rw-r--r--src/dbTypes.ts34
-rw-r--r--src/helpers.ts9
-rw-r--r--src/talerTypes.ts201
-rw-r--r--src/types-test.ts13
-rw-r--r--src/wallet.ts97
-rw-r--r--src/walletTypes.ts14
-rw-r--r--src/webex/pages/confirm-contract.tsx4
-rw-r--r--src/webex/pages/confirm-create-reserve.tsx2
-rw-r--r--src/webex/pages/refund.tsx21
-rw-r--r--tsconfig.json1
13 files changed, 320 insertions, 183 deletions
diff --git a/src/amounts.ts b/src/amounts.ts
index a31bec3da..fafbcb3ef 100644
--- a/src/amounts.ts
+++ b/src/amounts.ts
@@ -38,19 +38,19 @@ export class AmountJson {
/**
* Value, must be an integer.
*/
- @Checkable.Number
+ @Checkable.Number()
readonly value: number;
/**
* Fraction, must be an integer. Represent 1/1e8 of a unit.
*/
- @Checkable.Number
+ @Checkable.Number()
readonly fraction: number;
/**
* Currency of the amount.
*/
- @Checkable.String
+ @Checkable.String()
readonly currency: string;
/**
@@ -226,7 +226,7 @@ export function isNonZero(a: AmountJson): boolean {
* 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]+)?/);
+ const res = s.match(/([a-zA-Z0-9_*-]+):([0-9]+)([.][0-9]+)?/);
if (!res) {
return undefined;
}
@@ -237,6 +237,14 @@ export function parse(s: string): AmountJson|undefined {
};
}
+export function parseOrThrow(s: string): AmountJson {
+ const res = parse(s);
+ if (!res) {
+ throw Error(`Can't parse amount: "${s}"`);
+ }
+ return res;
+}
+
/**
* Convert the amount to a float.
*/
@@ -255,3 +263,23 @@ export function fromFloat(floatVal: number, currency: string) {
value: Math.floor(floatVal),
};
}
+
+/**
+ * Convert to standard human-readable string representation that's
+ * also used in JSON formats.
+ */
+export function toString(a: AmountJson) {
+ return `${a.currency}:${a.value + (a.fraction / fractionalBase)}`;
+}
+
+export function check(a: any) {
+ if (typeof a !== "string") {
+ return false;
+ }
+ try {
+ const parsedAmount = parse(a);
+ return !!parsedAmount;
+ } catch {
+ return false;
+ }
+}
diff --git a/src/checkable.ts b/src/checkable.ts
index 159e5a85e..52eb54120 100644
--- a/src/checkable.ts
+++ b/src/checkable.ts
@@ -57,6 +57,7 @@ export namespace Checkable {
elementChecker?: any;
elementProp?: any;
keyProp?: any;
+ stringChecker?: (s: string) => boolean;
valueProp?: any;
optional?: boolean;
extraAllowed?: boolean;
@@ -109,6 +110,9 @@ export namespace Checkable {
if (typeof target !== "string") {
throw new SchemaError(`expected string for ${path}, got ${typeof target} instead`);
}
+ if (prop.stringChecker && !prop.stringChecker(target)) {
+ throw new SchemaError(`string property ${path} malformed`);
+ }
return target;
}
@@ -316,7 +320,7 @@ export namespace Checkable {
/**
* Makes another annotation optional, for example `@Checkable.Optional(Checkable.Number)`.
*/
- export function Optional(type: any) {
+ export function Optional(type: (target: object, propertyKey: string | symbol) => void | any) {
const stub = {};
type(stub, "(optional-element)");
const elementProp = getCheckableInfo(stub).props[0];
@@ -342,21 +346,27 @@ export namespace Checkable {
/**
* Target property must be a number.
*/
- export function Number(target: object, propertyKey: string | symbol): void {
- const chk = getCheckableInfo(target);
- chk.props.push({checker: checkNumber, propertyKey});
+ export function Number(): (target: object, propertyKey: string | symbol) => void {
+ const deco = (target: object, propertyKey: string | symbol) => {
+ const chk = getCheckableInfo(target);
+ chk.props.push({checker: checkNumber, propertyKey});
+ };
+ return deco;
}
/**
* Target property must be an arbitary object.
*/
- export function AnyObject(target: object, propertyKey: string | symbol): void {
- const chk = getCheckableInfo(target);
- chk.props.push({
- checker: checkAnyObject,
- propertyKey,
- });
+ export function AnyObject(): (target: object, propertyKey: string | symbol) => void {
+ const deco = (target: object, propertyKey: string | symbol) => {
+ const chk = getCheckableInfo(target);
+ chk.props.push({
+ checker: checkAnyObject,
+ propertyKey,
+ });
+ };
+ return deco;
}
@@ -366,29 +376,40 @@ export namespace Checkable {
* Not useful by itself, but in combination with higher-order annotations
* such as List or Map.
*/
- export function Any(target: object, propertyKey: string | symbol): void {
- const chk = getCheckableInfo(target);
- chk.props.push({
- checker: checkAny,
- optional: true,
- propertyKey,
- });
+ export function Any(): (target: object, propertyKey: string | symbol) => void {
+ const deco = (target: object, propertyKey: string | symbol) => {
+ const chk = getCheckableInfo(target);
+ chk.props.push({
+ checker: checkAny,
+ optional: true,
+ propertyKey,
+ });
+ };
+ return deco;
}
/**
* Target property must be a string.
*/
- export function String(target: object, propertyKey: string | symbol): void {
- const chk = getCheckableInfo(target);
- chk.props.push({ checker: checkString, propertyKey });
+ export function String(
+ stringChecker?: (s: string) => boolean): (target: object, propertyKey: string | symbol,
+ ) => void {
+ const deco = (target: object, propertyKey: string | symbol) => {
+ const chk = getCheckableInfo(target);
+ chk.props.push({ checker: checkString, propertyKey, stringChecker });
+ };
+ return deco;
}
/**
* Target property must be a boolean value.
*/
- export function Boolean(target: object, propertyKey: string | symbol): void {
- const chk = getCheckableInfo(target);
- chk.props.push({ checker: checkBoolean, propertyKey });
+ export function Boolean(): (target: object, propertyKey: string | symbol) => void {
+ const deco = (target: object, propertyKey: string | symbol) => {
+ const chk = getCheckableInfo(target);
+ chk.props.push({ checker: checkBoolean, propertyKey });
+ };
+ return deco;
}
}
diff --git a/src/crypto/cryptoWorker.ts b/src/crypto/cryptoWorker.ts
index 6b82f6bc2..88e30e55b 100644
--- a/src/crypto/cryptoWorker.ts
+++ b/src/crypto/cryptoWorker.ts
@@ -282,7 +282,7 @@ namespace RpcFunctions {
const feeList: AmountJson[] = cds.map((x) => x.denom.feeDeposit);
let fees = Amounts.add(Amounts.getZero(feeList[0].currency), ...feeList).amount;
// okay if saturates
- fees = Amounts.sub(fees, contractTerms.max_fee).amount;
+ fees = Amounts.sub(fees, Amounts.parseOrThrow(contractTerms.max_fee)).amount;
const total = Amounts.add(fees, totalAmount).amount;
const amountSpent = native.Amount.getZero(cds[0].coin.currentAmount.currency);
@@ -335,7 +335,7 @@ namespace RpcFunctions {
const s: CoinPaySig = {
coin_pub: cd.coin.coinPub,
coin_sig: coinSig,
- contribution: coinSpend.toJson(),
+ contribution: Amounts.toString(coinSpend.toJson()),
denom_pub: cd.coin.denomPub,
exchange_url: cd.denom.exchangeBaseUrl,
ub_sig: cd.coin.denomSig,
diff --git a/src/dbTypes.ts b/src/dbTypes.ts
index 6c467ce74..6369cd92a 100644
--- a/src/dbTypes.ts
+++ b/src/dbTypes.ts
@@ -49,7 +49,7 @@ import {
* In the future we might consider adding migration functions for
* each version increment.
*/
-export const WALLET_DB_VERSION = 25;
+export const WALLET_DB_VERSION = 26;
/**
@@ -212,14 +212,14 @@ export class DenominationRecord {
/**
* The denomination public key.
*/
- @Checkable.String
+ @Checkable.String()
denomPub: string;
/**
* Hash of the denomination public key.
* Stored in the database for faster lookups.
*/
- @Checkable.String
+ @Checkable.String()
denomPubHash: string;
/**
@@ -249,38 +249,38 @@ export class DenominationRecord {
/**
* Validity start date of the denomination.
*/
- @Checkable.String
+ @Checkable.String()
stampStart: string;
/**
* Date after which the currency can't be withdrawn anymore.
*/
- @Checkable.String
+ @Checkable.String()
stampExpireWithdraw: string;
/**
* Date after the denomination officially doesn't exist anymore.
*/
- @Checkable.String
+ @Checkable.String()
stampExpireLegal: string;
/**
* Data after which coins of this denomination can't be deposited anymore.
*/
- @Checkable.String
+ @Checkable.String()
stampExpireDeposit: string;
/**
* Signature by the exchange's master key over the denomination
* information.
*/
- @Checkable.String
+ @Checkable.String()
masterSig: string;
/**
* Did we verify the signature on the denomination?
*/
- @Checkable.Number
+ @Checkable.Number()
status: DenominationStatus;
/**
@@ -288,13 +288,13 @@ export class DenominationRecord {
* we checked?
* Only false when the exchange redacts a previously published denomination.
*/
- @Checkable.Boolean
+ @Checkable.Boolean()
isOffered: boolean;
/**
* Base URL of the exchange.
*/
- @Checkable.String
+ @Checkable.String()
exchangeBaseUrl: string;
/**
@@ -500,7 +500,7 @@ export class ProposalDownloadRecord {
/**
* URL where the proposal was downloaded.
*/
- @Checkable.String
+ @Checkable.String()
url: string;
/**
@@ -512,32 +512,32 @@ export class ProposalDownloadRecord {
/**
* Signature by the merchant over the contract details.
*/
- @Checkable.String
+ @Checkable.String()
merchantSig: string;
/**
* Hash of the contract terms.
*/
- @Checkable.String
+ @Checkable.String()
contractTermsHash: string;
/**
* Serial ID when the offer is stored in the wallet DB.
*/
- @Checkable.Optional(Checkable.Number)
+ @Checkable.Optional(Checkable.Number())
id?: number;
/**
* Timestamp (in ms) of when the record
* was created.
*/
- @Checkable.Number
+ @Checkable.Number()
timestamp: number;
/**
* Private key for the nonce.
*/
- @Checkable.String
+ @Checkable.String()
noncePriv: string;
/**
diff --git a/src/helpers.ts b/src/helpers.ts
index 3b7cd36f5..7cd176498 100644
--- a/src/helpers.ts
+++ b/src/helpers.ts
@@ -119,12 +119,19 @@ export function flatMap<T, U>(xs: T[], f: (x: T) => U[]): U[] {
*/
export function getTalerStampSec(stamp: string): number | null {
const m = stamp.match(/\/?Date\(([0-9]*)\)\/?/);
- if (!m) {
+ if (!m || !m[1]) {
return null;
}
return parseInt(m[1], 10);
}
+/**
+ * Check if a timestamp is in the right format.
+ */
+export function timestampCheck(stamp: string): boolean {
+ return getTalerStampSec(stamp) !== null;
+}
+
/**
* Get a JavaScript Date object from a Taler date string.
diff --git a/src/talerTypes.ts b/src/talerTypes.ts
index 62e001a99..15e0009fd 100644
--- a/src/talerTypes.ts
+++ b/src/talerTypes.ts
@@ -26,9 +26,12 @@
/**
* Imports.
*/
-import { AmountJson } from "./amounts";
import { Checkable } from "./checkable";
+import * as Amounts from "./amounts";
+
+import { timestampCheck } from "./helpers";
+
/**
* Denomination as found in the /keys response from the exchange.
@@ -38,70 +41,70 @@ export class Denomination {
/**
* Value of one coin of the denomination.
*/
- @Checkable.Value(() => AmountJson)
- value: AmountJson;
+ @Checkable.String(Amounts.check)
+ value: string;
/**
* Public signing key of the denomination.
*/
- @Checkable.String
+ @Checkable.String()
denom_pub: string;
/**
* Fee for withdrawing.
*/
- @Checkable.Value(() => AmountJson)
- fee_withdraw: AmountJson;
+ @Checkable.String(Amounts.check)
+ fee_withdraw: string;
/**
* Fee for depositing.
*/
- @Checkable.Value(() => AmountJson)
- fee_deposit: AmountJson;
+ @Checkable.String(Amounts.check)
+ fee_deposit: string;
/**
* Fee for refreshing.
*/
- @Checkable.Value(() => AmountJson)
- fee_refresh: AmountJson;
+ @Checkable.String(Amounts.check)
+ fee_refresh: string;
/**
* Fee for refunding.
*/
- @Checkable.Value(() => AmountJson)
- fee_refund: AmountJson;
+ @Checkable.String(Amounts.check)
+ fee_refund: string;
/**
* Start date from which withdraw is allowed.
*/
- @Checkable.String
+ @Checkable.String(timestampCheck)
stamp_start: string;
/**
* End date for withdrawing.
*/
- @Checkable.String
+ @Checkable.String(timestampCheck)
stamp_expire_withdraw: string;
/**
* Expiration date after which the exchange can forget about
* the currency.
*/
- @Checkable.String
+ @Checkable.String(timestampCheck)
stamp_expire_legal: string;
/**
* Date after which the coins of this denomination can't be
* deposited anymore.
*/
- @Checkable.String
+ @Checkable.String(timestampCheck)
stamp_expire_deposit: string;
/**
* Signature over the denomination information by the exchange's master
* signing key.
*/
- @Checkable.String
+ @Checkable.String()
master_sig: string;
/**
@@ -120,13 +123,13 @@ export class AuditorDenomSig {
/**
* Denomination public key's hash.
*/
- @Checkable.String
+ @Checkable.String()
denom_pub_h: string;
/**
* The signature.
*/
- @Checkable.String
+ @Checkable.String()
auditor_sig: string;
}
@@ -139,13 +142,13 @@ export class Auditor {
/**
* Auditor's public key.
*/
- @Checkable.String
+ @Checkable.String()
auditor_pub: string;
/**
* Base URL of the auditor.
*/
- @Checkable.String
+ @Checkable.String()
auditor_url: string;
/**
@@ -197,20 +200,20 @@ export class PaybackConfirmation {
/**
* public key of the reserve that will receive the payback.
*/
- @Checkable.String
+ @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;
+ @Checkable.String()
+ amount: string;
/**
* Time by which the exchange received the /payback request.
*/
- @Checkable.String
+ @Checkable.String()
timestamp: string;
/**
@@ -220,7 +223,7 @@ export class PaybackConfirmation {
* by the date specified (this allows the exchange delaying the transfer
* a bit to aggregate additional payback requests into a larger one).
*/
- @Checkable.String
+ @Checkable.String()
exchange_sig: string;
/**
@@ -229,7 +232,7 @@ export class PaybackConfirmation {
* explicitly as the client might otherwise be confused by clock skew as to
* which signing key was used.
*/
- @Checkable.String
+ @Checkable.String()
exchange_pub: string;
/**
@@ -263,7 +266,7 @@ export interface CoinPaySig {
/**
* The amount that is subtracted from this coin with this payment.
*/
- contribution: AmountJson;
+ contribution: string;
/**
* URL of the exchange this coin was withdrawn from.
@@ -281,13 +284,13 @@ export class ExchangeHandle {
/**
* Master public signing key of the exchange.
*/
- @Checkable.String
+ @Checkable.String()
master_pub: string;
/**
* Base URL of the exchange.
*/
- @Checkable.String
+ @Checkable.String()
url: string;
/**
@@ -312,67 +315,67 @@ export class ContractTerms {
/**
* Hash of the merchant's wire details.
*/
- @Checkable.String
+ @Checkable.String()
H_wire: string;
/**
* Wire method the merchant wants to use.
*/
- @Checkable.String
+ @Checkable.String()
wire_method: string;
/**
* Human-readable short summary of the contract.
*/
- @Checkable.Optional(Checkable.String)
+ @Checkable.Optional(Checkable.String())
summary?: string;
/**
* Nonce used to ensure freshness.
*/
- @Checkable.Optional(Checkable.String)
+ @Checkable.Optional(Checkable.String())
nonce?: string;
/**
* Total amount payable.
*/
- @Checkable.Value(() => AmountJson)
- amount: AmountJson;
+ @Checkable.String(Amounts.check)
+ amount: string;
/**
* Auditors accepted by the merchant.
*/
- @Checkable.List(Checkable.AnyObject)
+ @Checkable.List(Checkable.AnyObject())
auditors: any[];
/**
* Deadline to pay for the contract.
*/
- @Checkable.Optional(Checkable.String)
+ @Checkable.Optional(Checkable.String())
pay_deadline: string;
/**
* Delivery locations.
*/
- @Checkable.Any
+ @Checkable.Any()
locations: any;
/**
* Maximum deposit fee covered by the merchant.
*/
- @Checkable.Value(() => AmountJson)
- max_fee: AmountJson;
+ @Checkable.String(Amounts.check)
+ max_fee: string;
/**
* Information about the merchant.
*/
- @Checkable.Any
+ @Checkable.Any()
merchant: any;
/**
* Public key of the merchant.
*/
- @Checkable.String
+ @Checkable.String()
merchant_pub: string;
/**
@@ -384,57 +387,57 @@ export class ContractTerms {
/**
* Products that are sold in this contract.
*/
- @Checkable.List(Checkable.AnyObject)
+ @Checkable.List(Checkable.AnyObject())
products: any[];
/**
* Deadline for refunds.
*/
- @Checkable.String
+ @Checkable.String(timestampCheck)
refund_deadline: string;
/**
* Time when the contract was generated by the merchant.
*/
- @Checkable.String
+ @Checkable.String(timestampCheck)
timestamp: string;
/**
* Order id to uniquely identify the purchase within
* one merchant instance.
*/
- @Checkable.String
+ @Checkable.String()
order_id: string;
/**
* URL to post the payment to.
*/
- @Checkable.String
+ @Checkable.String()
pay_url: string;
/**
* Fulfillment URL to view the product or
* delivery status.
*/
- @Checkable.String
+ @Checkable.String()
fulfillment_url: string;
/**
* Share of the wire fee that must be settled with one payment.
*/
- @Checkable.Optional(Checkable.Number)
+ @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;
+ @Checkable.Optional(Checkable.String())
+ max_wire_fee?: string;
/**
* Extra data, interpreted by the mechant only.
*/
- @Checkable.Any
+ @Checkable.Any()
extra: any;
/**
@@ -480,31 +483,31 @@ export class MerchantRefundPermission {
/**
* Amount to be refunded.
*/
- @Checkable.Value(() => AmountJson)
- refund_amount: AmountJson;
+ @Checkable.String(Amounts.check)
+ refund_amount: string;
/**
* Fee for the refund.
*/
- @Checkable.Value(() => AmountJson)
- refund_fee: AmountJson;
+ @Checkable.String(Amounts.check)
+ refund_fee: string;
/**
* Public key of the coin being refunded.
*/
- @Checkable.String
+ @Checkable.String()
coin_pub: string;
/**
* Refund transaction ID between merchant and exchange.
*/
- @Checkable.Number
+ @Checkable.Number()
rtransaction_id: number;
/**
* Signature made by the merchant over the refund permission.
*/
- @Checkable.String
+ @Checkable.String()
merchant_sig: string;
/**
@@ -523,13 +526,13 @@ export interface RefundRequest {
* coin's total deposit value (including deposit fee);
* must be larger than the refund fee.
*/
- refund_amount: AmountJson;
+ refund_amount: string;
/**
* Refund fee associated with the given coin.
* must be smaller than the refund amount.
*/
- refund_fee: AmountJson;
+ refund_fee: string;
/**
* SHA-512 hash of the contact of the merchant with the customer.
@@ -566,14 +569,14 @@ export class MerchantRefundResponse {
/**
* Public key of the merchant
*/
- @Checkable.String
+ @Checkable.String()
merchant_pub: string;
/**
* Contract terms hash of the contract that
* is being refunded.
*/
- @Checkable.String
+ @Checkable.String()
h_contract_terms: string;
/**
@@ -629,7 +632,7 @@ export class ReserveSigSingleton {
/**
* Reserve signature.
*/
- @Checkable.String
+ @Checkable.String()
reserve_sig: string;
/**
@@ -638,6 +641,30 @@ export class ReserveSigSingleton {
static checked: (obj: any) => ReserveSigSingleton;
}
+
+/**
+ * Response to /reserve/status
+ */
+@Checkable.Class()
+export class ReserveStatus {
+ /**
+ * Reserve signature.
+ */
+ @Checkable.String()
+ balance: string;
+
+ /**
+ * Reserve history, currently not used by the wallet.
+ */
+ @Checkable.Any()
+ history: any;
+
+ /**
+ * Create a ReserveSigSingleton from untyped JSON.
+ */
+ static checked: (obj: any) => ReserveStatus;
+}
+
/**
* Response of the merchant
* to the TipPickupRequest.
@@ -647,7 +674,7 @@ export class TipResponse {
/**
* Public key of the reserve
*/
- @Checkable.String
+ @Checkable.String()
reserve_pub: string;
/**
@@ -671,37 +698,37 @@ export class TipToken {
/**
* Expiration for the tip.
*/
- @Checkable.String
+ @Checkable.String(timestampCheck)
expiration: string;
/**
* URL of the exchange that the tip can be withdrawn from.
*/
- @Checkable.String
+ @Checkable.String()
exchange_url: string;
/**
* Merchant's URL to pick up the tip.
*/
- @Checkable.String
+ @Checkable.String()
pickup_url: string;
/**
* Merchant-chosen tip identifier.
*/
- @Checkable.String
+ @Checkable.String()
tip_id: string;
/**
* Amount of tip.
*/
- @Checkable.Value(() => AmountJson)
- amount: AmountJson;
+ @Checkable.String()
+ amount: string;
/**
* URL to navigate after finishing tip processing.
*/
- @Checkable.String
+ @Checkable.String()
next_url: string;
/**
@@ -721,7 +748,7 @@ export class Payback {
/**
* The hash of the denomination public key for which the payback is offered.
*/
- @Checkable.String
+ @Checkable.String()
h_denom_pub: string;
}
@@ -740,7 +767,7 @@ export class KeysJson {
/**
* The exchange's master public key.
*/
- @Checkable.String
+ @Checkable.String()
master_public_key: string;
/**
@@ -752,7 +779,7 @@ export class KeysJson {
/**
* Timestamp when this response was issued.
*/
- @Checkable.String
+ @Checkable.String(timestampCheck)
list_issue_date: string;
/**
@@ -765,13 +792,13 @@ export class KeysJson {
* Short-lived signing keys used to sign online
* responses.
*/
- @Checkable.Any
+ @Checkable.Any()
signkeys: any;
/**
* Protocol version.
*/
- @Checkable.Optional(Checkable.String)
+ @Checkable.Optional(Checkable.String())
version?: string;
/**
@@ -790,31 +817,31 @@ export class WireFeesJson {
/**
* Cost of a wire transfer.
*/
- @Checkable.Value(() => AmountJson)
- wire_fee: AmountJson;
+ @Checkable.String(Amounts.check)
+ wire_fee: string;
/**
* Cost of clising a reserve.
*/
- @Checkable.Value(() => AmountJson)
- closing_fee: AmountJson;
+ @Checkable.String(Amounts.check)
+ closing_fee: string;
/**
* Signature made with the exchange's master key.
*/
- @Checkable.String
+ @Checkable.String()
sig: string;
/**
* Date from which the fee applies.
*/
- @Checkable.String
+ @Checkable.String(timestampCheck)
start_date: string;
/**
* Data after which the fee doesn't apply anymore.
*/
- @Checkable.String
+ @Checkable.String(timestampCheck)
end_date: string;
/**
@@ -834,7 +861,7 @@ export class WireDetailJson {
/**
* Name of the wire transfer method.
*/
- @Checkable.String
+ @Checkable.String()
type: string;
/**
@@ -872,7 +899,7 @@ export class Proposal {
@Checkable.Value(() => ContractTerms)
contract_terms: ContractTerms;
- @Checkable.String
+ @Checkable.String()
sig: string;
/**
diff --git a/src/types-test.ts b/src/types-test.ts
index 097235a77..d65daeade 100644
--- a/src/types-test.ts
+++ b/src/types-test.ts
@@ -54,14 +54,23 @@ test("amount subtraction (saturation)", (t) => {
});
+test("amount parsing", (t) => {
+ const a1 = Amounts.parseOrThrow("TESTKUDOS:10");
+ t.is(a1.currency, "TESTKUDOS");
+ t.is(a1.value, 10);
+ t.is(a1.fraction, 0);
+ t.pass();
+});
+
+
test("contract terms validation", (t) => {
const c = {
H_wire: "123",
- amount: amt(1, 2, "EUR"),
+ amount: "EUR:1.5",
auditors: [],
exchanges: [{master_pub: "foo", url: "foo"}],
fulfillment_url: "foo",
- max_fee: amt(1, 2, "EUR"),
+ max_fee: "EUR:1.5",
merchant_pub: "12345",
order_id: "test_order",
pay_deadline: "Date(12346)",
diff --git a/src/wallet.ts b/src/wallet.ts
index 34b2388e3..c4308b8d1 100644
--- a/src/wallet.ts
+++ b/src/wallet.ts
@@ -82,6 +82,7 @@ import {
PaybackConfirmation,
Proposal,
RefundRequest,
+ ReserveStatus,
TipPlanchetDetail,
TipResponse,
TipToken,
@@ -825,13 +826,22 @@ export class Wallet {
return this.submitPay(purchase.contractTermsHash, sessionId);
}
+ const contractAmount = Amounts.parseOrThrow(proposal.contractTerms.amount);
+
+ let wireFeeLimit;
+ if (!proposal.contractTerms.max_wire_fee) {
+ wireFeeLimit = Amounts.getZero(contractAmount.currency);
+ } else {
+ wireFeeLimit = Amounts.parseOrThrow(proposal.contractTerms.max_wire_fee);
+ }
+
const res = await this.getCoinsForPayment({
allowedAuditors: proposal.contractTerms.auditors,
allowedExchanges: proposal.contractTerms.exchanges,
- depositFeeLimit: proposal.contractTerms.max_fee,
- paymentAmount: proposal.contractTerms.amount,
+ depositFeeLimit: Amounts.parseOrThrow(proposal.contractTerms.max_fee),
+ paymentAmount: Amounts.parseOrThrow(proposal.contractTerms.amount),
wireFeeAmortization: proposal.contractTerms.wire_fee_amortization || 1,
- wireFeeLimit: proposal.contractTerms.max_wire_fee || Amounts.getZero(proposal.contractTerms.amount.currency),
+ wireFeeLimit,
wireFeeTime: getTalerStampSec(proposal.contractTerms.timestamp) || 0,
wireMethod: proposal.contractTerms.wire_method,
});
@@ -907,14 +917,23 @@ export class Wallet {
return { status: "paid" };
}
+ const paymentAmount = Amounts.parseOrThrow(proposal.contractTerms.amount);
+
+ let wireFeeLimit;
+ if (proposal.contractTerms.max_wire_fee) {
+ wireFeeLimit = Amounts.parseOrThrow(proposal.contractTerms.max_wire_fee);
+ } else {
+ wireFeeLimit = Amounts.getZero(paymentAmount.currency);
+ }
+
// If not already payed, check if we could pay for it.
const res = await this.getCoinsForPayment({
allowedAuditors: proposal.contractTerms.auditors,
allowedExchanges: proposal.contractTerms.exchanges,
- depositFeeLimit: proposal.contractTerms.max_fee,
- paymentAmount: proposal.contractTerms.amount,
+ depositFeeLimit: Amounts.parseOrThrow(proposal.contractTerms.max_fee),
+ paymentAmount,
wireFeeAmortization: proposal.contractTerms.wire_fee_amortization || 1,
- wireFeeLimit: proposal.contractTerms.max_wire_fee || Amounts.getZero(proposal.contractTerms.amount.currency),
+ wireFeeLimit,
wireFeeTime: getTalerStampSec(proposal.contractTerms.timestamp) || 0,
wireMethod: proposal.contractTerms.wire_method,
});
@@ -1243,8 +1262,8 @@ export class Wallet {
denom.value,
denom.feeWithdraw).amount;
const result = Amounts.sub(currentAmount,
- denom.value,
- denom.feeWithdraw);
+ denom.value,
+ denom.feeWithdraw);
if (result.saturated) {
console.error("can't create precoin, saturated");
throw AbortTransaction;
@@ -1290,11 +1309,11 @@ export class Wallet {
if (resp.status !== 200) {
throw Error();
}
- const reserveInfo = JSON.parse(resp.responseText);
+ const reserveInfo = ReserveStatus.checked(JSON.parse(resp.responseText));
if (!reserveInfo) {
throw Error();
}
- reserve.current_amount = reserveInfo.balance;
+ reserve.current_amount = Amounts.parseOrThrow(reserveInfo.balance);
await this.q()
.put(Stores.reserves, reserve)
.finish();
@@ -1613,7 +1632,7 @@ export class Wallet {
exchangeInfo = {
auditors: exchangeKeysJson.auditors,
baseUrl,
- currency: exchangeKeysJson.denoms[0].value.currency,
+ currency: Amounts.parseOrThrow(exchangeKeysJson.denoms[0].value).currency,
lastUpdateTime: updateTimeSec,
lastUsedTime: 0,
masterPublicKey: exchangeKeysJson.master_public_key,
@@ -1670,11 +1689,11 @@ export class Wallet {
continue;
}
const wf: WireFee = {
- closingFee: fee.closing_fee,
+ closingFee: Amounts.parseOrThrow(fee.closing_fee),
endStamp: end,
sig: fee.sig,
startStamp: start,
- wireFee: fee.wire_fee,
+ wireFee: Amounts.parseOrThrow(fee.wire_fee),
};
const valid: boolean = await this.cryptoApi.isValidWireFee(detail.type, wf, exchangeInfo.masterPublicKey);
if (!valid) {
@@ -1829,7 +1848,7 @@ export class Wallet {
return balance;
}
for (const c of t.payReq.coins) {
- addTo(balance, "pendingIncoming", c.contribution, c.exchange_url);
+ addTo(balance, "pendingIncoming", Amounts.parseOrThrow(c.contribution), c.exchange_url);
}
return balance;
}
@@ -2180,10 +2199,17 @@ export class Wallet {
type: "pay",
});
if (p.timestamp_refund) {
- const amountsPending = Object.keys(p.refundsPending).map((x) => p.refundsPending[x].refund_amount);
- const amountsDone = Object.keys(p.refundsDone).map((x) => p.refundsDone[x].refund_amount);
+ const contractAmount = Amounts.parseOrThrow(p.contractTerms.amount);
+ const amountsPending = (
+ Object.keys(p.refundsPending)
+ .map((x) => Amounts.parseOrThrow(p.refundsPending[x].refund_amount))
+ );
+ const amountsDone = (
+ Object.keys(p.refundsDone)
+ .map((x) => Amounts.parseOrThrow(p.refundsDone[x].refund_amount))
+ );
const amounts: AmountJson[] = amountsPending.concat(amountsDone);
- const amount = Amounts.add(Amounts.getZero(p.contractTerms.amount.currency), ...amounts).amount;
+ const amount = Amounts.add(Amounts.getZero(contractAmount.currency), ...amounts).amount;
history.push({
detail: {
@@ -2357,10 +2383,10 @@ export class Wallet {
denomPub: denomIn.denom_pub,
denomPubHash,
exchangeBaseUrl,
- feeDeposit: denomIn.fee_deposit,
- feeRefresh: denomIn.fee_refresh,
- feeRefund: denomIn.fee_refund,
- feeWithdraw: denomIn.fee_withdraw,
+ feeDeposit: Amounts.parseOrThrow(denomIn.fee_deposit),
+ feeRefresh: Amounts.parseOrThrow(denomIn.fee_refresh),
+ feeRefund: Amounts.parseOrThrow(denomIn.fee_refund),
+ feeWithdraw: Amounts.parseOrThrow(denomIn.fee_withdraw),
isOffered: true,
masterSig: denomIn.master_sig,
stampExpireDeposit: denomIn.stamp_expire_deposit,
@@ -2368,7 +2394,7 @@ export class Wallet {
stampExpireWithdraw: denomIn.stamp_expire_withdraw,
stampStart: denomIn.stamp_start,
status: DenominationStatus.Unverified,
- value: denomIn.value,
+ value: Amounts.parseOrThrow(denomIn.value),
};
return d;
}
@@ -2449,13 +2475,13 @@ export class Wallet {
const contractTerms: ContractTerms = {
H_wire: wireHash,
- amount: req.amount,
+ amount: Amounts.toString(req.amount),
auditors: [],
exchanges: [ { master_pub: exchange.masterPublicKey, url: exchange.baseUrl } ],
extra: {},
fulfillment_url: "",
locations: [],
- max_fee: req.amount,
+ max_fee: Amounts.toString(req.amount),
merchant: {},
merchant_pub: pub,
order_id: "none",
@@ -2469,7 +2495,9 @@ export class Wallet {
const contractTermsHash = await this.cryptoApi.hashString(canonicalJson(contractTerms));
- const payCoinInfo = await this.cryptoApi.signDeposit(contractTerms, cds, contractTerms.amount);
+ const payCoinInfo = await (
+ this.cryptoApi.signDeposit(contractTerms, cds, Amounts.parseOrThrow(contractTerms.amount))
+ );
console.log("pci", payCoinInfo);
@@ -2660,9 +2688,11 @@ export class Wallet {
console.warn("coin not found, can't apply refund");
return;
}
+ const refundAmount = Amounts.parseOrThrow(perm.refund_amount);
+ const refundFee = Amounts.parseOrThrow(perm.refund_fee);
c.status = CoinStatus.Dirty;
- c.currentAmount = Amounts.add(c.currentAmount, perm.refund_amount).amount;
- c.currentAmount = Amounts.sub(c.currentAmount, perm.refund_fee).amount;
+ c.currentAmount = Amounts.add(c.currentAmount, refundAmount).amount;
+ c.currentAmount = Amounts.sub(c.currentAmount, refundFee).amount;
return c;
};
@@ -2690,7 +2720,7 @@ export class Wallet {
if (!coin0) {
throw Error("coin not found");
}
- let feeAcc = Amounts.getZero(refundPermissions[0].refund_amount.currency);
+ let feeAcc = Amounts.getZero(Amounts.parseOrThrow(refundPermissions[0].refund_amount).currency);
const denoms = await this.q().iterIndex(Stores.denominations.exchangeBaseUrlIndex, coin0.exchangeBaseUrl).toArray();
for (const rp of refundPermissions) {
@@ -2706,8 +2736,10 @@ export class Wallet {
// When it hasn't, the refresh cost is inaccurate. To fix this,
// we need introduce a flag to tell if a coin was refunded or
// refreshed normally (and what about incremental refunds?)
- const refreshCost = getTotalRefreshCost(denoms, denom, Amounts.sub(rp.refund_amount, rp.refund_fee).amount);
- feeAcc = Amounts.add(feeAcc, refreshCost, rp.refund_fee).amount;
+ const refundAmount = Amounts.parseOrThrow(rp.refund_amount);
+ const refundFee = Amounts.parseOrThrow(rp.refund_fee);
+ const refreshCost = getTotalRefreshCost(denoms, denom, Amounts.sub(refundAmount, refundFee).amount);
+ feeAcc = Amounts.add(feeAcc, refreshCost, refundFee).amount;
}
return feeAcc;
}
@@ -2731,14 +2763,15 @@ export class Wallet {
if (tipRecord && tipRecord.pickedUp) {
return tipRecord;
}
+ const tipAmount = Amounts.parseOrThrow(tipToken.amount);
await this.updateExchangeFromUrl(tipToken.exchange_url);
- const denomsForWithdraw = await this.getVerifiedWithdrawDenomList(tipToken.exchange_url, tipToken.amount);
+ const denomsForWithdraw = await this.getVerifiedWithdrawDenomList(tipToken.exchange_url, tipAmount);
const planchets = await Promise.all(denomsForWithdraw.map(d => this.cryptoApi.createTipPlanchet(d)));
const coinPubs: string[] = planchets.map(x => x.coinPub);
const now = (new Date()).getTime();
tipRecord = {
accepted: false,
- amount: tipToken.amount,
+ amount: Amounts.parseOrThrow(tipToken.amount),
coinPubs,
deadline: deadlineSec,
exchangeUrl: tipToken.exchange_url,
diff --git a/src/walletTypes.ts b/src/walletTypes.ts
index aba7dbfba..edcf65830 100644
--- a/src/walletTypes.ts
+++ b/src/walletTypes.ts
@@ -53,13 +53,13 @@ export class CreateReserveResponse {
* Exchange URL where the bank should create the reserve.
* The URL is canonicalized in the response.
*/
- @Checkable.String
+ @Checkable.String()
exchange: string;
/**
* Reserve public key of the newly created reserve.
*/
- @Checkable.String
+ @Checkable.String()
reservePub: string;
/**
@@ -333,13 +333,13 @@ export class CreateReserveRequest {
/**
* Exchange URL where the bank should create the reserve.
*/
- @Checkable.String
+ @Checkable.String()
exchange: string;
/**
* Wire details for the bank account that sent the funds to the exchange.
*/
- @Checkable.Optional(Checkable.Any)
+ @Checkable.Optional(Checkable.Any())
senderWire?: object;
/**
@@ -359,7 +359,7 @@ export class ConfirmReserveRequest {
* Public key of then reserve that should be marked
* as confirmed.
*/
- @Checkable.String
+ @Checkable.String()
reservePub: string;
/**
@@ -384,14 +384,14 @@ export class ReturnCoinsRequest {
/**
* The exchange to take the coins from.
*/
- @Checkable.String
+ @Checkable.String()
exchange: string;
/**
* Wire details for the bank account of the customer that will
* receive the funds.
*/
- @Checkable.Any
+ @Checkable.Any()
senderWire?: object;
/**
diff --git a/src/webex/pages/confirm-contract.tsx b/src/webex/pages/confirm-contract.tsx
index 21f05d5d6..78e90ee0e 100644
--- a/src/webex/pages/confirm-contract.tsx
+++ b/src/webex/pages/confirm-contract.tsx
@@ -42,6 +42,8 @@ import * as ReactDOM from "react-dom";
import URI = require("urijs");
import { WalletApiError } from "../wxApi";
+import * as Amounts from "../../amounts";
+
interface DetailState {
collapsed: boolean;
@@ -294,7 +296,7 @@ class ContractPrompt extends React.Component<ContractPromptProps, ContractPrompt
} else {
merchantName = <strong>(pub: {c.merchant_pub})</strong>;
}
- const amount = <strong>{renderAmount(c.amount)}</strong>;
+ const amount = <strong>{renderAmount(Amounts.parseOrThrow(c.amount))}</strong>;
console.log("payStatus", this.state.payStatus);
let products = null;
diff --git a/src/webex/pages/confirm-create-reserve.tsx b/src/webex/pages/confirm-create-reserve.tsx
index bd21280c3..d83c56665 100644
--- a/src/webex/pages/confirm-create-reserve.tsx
+++ b/src/webex/pages/confirm-create-reserve.tsx
@@ -38,11 +38,11 @@ import {
import { ImplicitStateComponent, StateHolder } from "../components";
import {
+ WalletApiError,
createReserve,
getCurrency,
getExchangeInfo,
getReserveCreationInfo,
- WalletApiError,
} from "../wxApi";
import {
diff --git a/src/webex/pages/refund.tsx b/src/webex/pages/refund.tsx
index 8164eb664..b2f5948d7 100644
--- a/src/webex/pages/refund.tsx
+++ b/src/webex/pages/refund.tsx
@@ -59,18 +59,24 @@ const RefundDetail = ({purchase, fullRefundFees}: RefundDetailProps) => {
}
const firstRefundKey = [...pendingKeys, ...doneKeys][0];
- const currency = { ...purchase.refundsDone, ...purchase.refundsPending }[firstRefundKey].refund_amount.currency;
+ if (!firstRefundKey) {
+ return <p>Waiting for refunds ...</p>;
+ }
+ const allRefunds = { ...purchase.refundsDone, ...purchase.refundsPending };
+ const currency = Amounts.parseOrThrow(allRefunds[firstRefundKey].refund_amount).currency;
if (!currency) {
throw Error("invariant");
}
let amountPending = Amounts.getZero(currency);
for (const k of pendingKeys) {
- amountPending = Amounts.add(amountPending, purchase.refundsPending[k].refund_amount).amount;
+ const refundAmount = Amounts.parseOrThrow(purchase.refundsPending[k].refund_amount);
+ amountPending = Amounts.add(amountPending, refundAmount).amount;
}
let amountDone = Amounts.getZero(currency);
for (const k of doneKeys) {
- amountDone = Amounts.add(amountDone, purchase.refundsDone[k].refund_amount).amount;
+ const refundAmount = Amounts.parseOrThrow(purchase.refundsDone[k].refund_amount);
+ amountDone = Amounts.add(amountDone, refundAmount).amount;
}
const hasPending = amountPending.fraction !== 0 || amountPending.value !== 0;
@@ -130,7 +136,7 @@ class RefundStatusView extends React.Component<RefundStatusViewProps, RefundStat
Status of purchase <strong>{summary}</strong> from merchant <strong>{merchantName}</strong>{" "}
(order id {purchase.contractTerms.order_id}).
</p>
- <p>Total amount: <AmountDisplay amount={purchase.contractTerms.amount} /></p>
+ <p>Total amount: <AmountDisplay amount={Amounts.parseOrThrow(purchase.contractTerms.amount)} /></p>
{purchase.finished
? <RefundDetail purchase={purchase} fullRefundFees={this.state.refundFees!} />
: <p>Purchase not completed.</p>}
@@ -147,12 +153,15 @@ class RefundStatusView extends React.Component<RefundStatusViewProps, RefundStat
return;
}
contractTermsHash = await wxApi.acceptRefund(refundUrl);
+ this.setState({ contractTermsHash });
}
const purchase = await wxApi.getPurchase(contractTermsHash);
console.log("got purchase", purchase);
const refundsDone = Object.keys(purchase.refundsDone).map((x) => purchase.refundsDone[x]);
- const refundFees = await wxApi.getFullRefundFees( {refundPermissions: refundsDone });
- this.setState({ purchase, gotResult: true, refundFees });
+ if (refundsDone.length) {
+ const refundFees = await wxApi.getFullRefundFees({ refundPermissions: refundsDone });
+ this.setState({ purchase, gotResult: true, refundFees });
+ }
}
}
diff --git a/tsconfig.json b/tsconfig.json
index ae77fb27c..819c54276 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -54,6 +54,7 @@
"src/walletTypes.ts",
"src/webex/background.ts",
"src/webex/chromeBadge.ts",
+ "src/webex/compat.ts",
"src/webex/components.ts",
"src/webex/messages.ts",
"src/webex/notify.ts",