change protocol to string amount network format

This commit is contained in:
Florian Dold 2018-01-29 22:58:47 +01:00
parent 9fe6dc5965
commit 97f6e68ce3
No known key found for this signature in database
GPG Key ID: D2E4F00F29D02A4B
13 changed files with 320 additions and 183 deletions

View File

@ -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;
}
}

View File

@ -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 {
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 {
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 {
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 {
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 });
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 {
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;
}
}

View File

@ -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,

View File

@ -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;
/**

View File

@ -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.

View File

@ -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;
/**

View File

@ -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)",

View File

@ -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,
});
@ -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,

View File

@ -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;
/**

View File

@ -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;

View File

@ -38,11 +38,11 @@ import {
import { ImplicitStateComponent, StateHolder } from "../components";
import {
WalletApiError,
createReserve,
getCurrency,
getExchangeInfo,
getReserveCreationInfo,
WalletApiError,
} from "../wxApi";
import {

View File

@ -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,14 +153,17 @@ 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]);
if (refundsDone.length) {
const refundFees = await wxApi.getFullRefundFees({ refundPermissions: refundsDone });
this.setState({ purchase, gotResult: true, refundFees });
}
}
}
async function main() {

View File

@ -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",