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. * Value, must be an integer.
*/ */
@Checkable.Number @Checkable.Number()
readonly value: number; readonly value: number;
/** /**
* Fraction, must be an integer. Represent 1/1e8 of a unit. * Fraction, must be an integer. Represent 1/1e8 of a unit.
*/ */
@Checkable.Number @Checkable.Number()
readonly fraction: number; readonly fraction: number;
/** /**
* Currency of the amount. * Currency of the amount.
*/ */
@Checkable.String @Checkable.String()
readonly currency: 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. * Parse an amount like 'EUR:20.5' for 20 Euros and 50 ct.
*/ */
export function parse(s: string): AmountJson|undefined { 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) { if (!res) {
return undefined; 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. * Convert the amount to a float.
*/ */
@ -255,3 +263,23 @@ export function fromFloat(floatVal: number, currency: string) {
value: Math.floor(floatVal), 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; elementChecker?: any;
elementProp?: any; elementProp?: any;
keyProp?: any; keyProp?: any;
stringChecker?: (s: string) => boolean;
valueProp?: any; valueProp?: any;
optional?: boolean; optional?: boolean;
extraAllowed?: boolean; extraAllowed?: boolean;
@ -109,6 +110,9 @@ export namespace Checkable {
if (typeof target !== "string") { if (typeof target !== "string") {
throw new SchemaError(`expected string for ${path}, got ${typeof target} instead`); 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; return target;
} }
@ -316,7 +320,7 @@ export namespace Checkable {
/** /**
* Makes another annotation optional, for example `@Checkable.Optional(Checkable.Number)`. * 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 = {}; const stub = {};
type(stub, "(optional-element)"); type(stub, "(optional-element)");
const elementProp = getCheckableInfo(stub).props[0]; const elementProp = getCheckableInfo(stub).props[0];
@ -342,21 +346,27 @@ export namespace Checkable {
/** /**
* Target property must be a number. * 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); const chk = getCheckableInfo(target);
chk.props.push({checker: checkNumber, propertyKey}); chk.props.push({checker: checkNumber, propertyKey});
};
return deco;
} }
/** /**
* Target property must be an arbitary object. * 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); const chk = getCheckableInfo(target);
chk.props.push({ chk.props.push({
checker: checkAnyObject, checker: checkAnyObject,
propertyKey, propertyKey,
}); });
};
return deco;
} }
@ -366,29 +376,40 @@ export namespace Checkable {
* Not useful by itself, but in combination with higher-order annotations * Not useful by itself, but in combination with higher-order annotations
* such as List or Map. * 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); const chk = getCheckableInfo(target);
chk.props.push({ chk.props.push({
checker: checkAny, checker: checkAny,
optional: true, optional: true,
propertyKey, propertyKey,
}); });
};
return deco;
} }
/** /**
* Target property must be a string. * 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); 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. * 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); const chk = getCheckableInfo(target);
chk.props.push({ checker: checkBoolean, propertyKey }); 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); const feeList: AmountJson[] = cds.map((x) => x.denom.feeDeposit);
let fees = Amounts.add(Amounts.getZero(feeList[0].currency), ...feeList).amount; let fees = Amounts.add(Amounts.getZero(feeList[0].currency), ...feeList).amount;
// okay if saturates // 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 total = Amounts.add(fees, totalAmount).amount;
const amountSpent = native.Amount.getZero(cds[0].coin.currentAmount.currency); const amountSpent = native.Amount.getZero(cds[0].coin.currentAmount.currency);
@ -335,7 +335,7 @@ namespace RpcFunctions {
const s: CoinPaySig = { const s: CoinPaySig = {
coin_pub: cd.coin.coinPub, coin_pub: cd.coin.coinPub,
coin_sig: coinSig, coin_sig: coinSig,
contribution: coinSpend.toJson(), contribution: Amounts.toString(coinSpend.toJson()),
denom_pub: cd.coin.denomPub, denom_pub: cd.coin.denomPub,
exchange_url: cd.denom.exchangeBaseUrl, exchange_url: cd.denom.exchangeBaseUrl,
ub_sig: cd.coin.denomSig, ub_sig: cd.coin.denomSig,

View File

@ -49,7 +49,7 @@ import {
* In the future we might consider adding migration functions for * In the future we might consider adding migration functions for
* each version increment. * 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. * The denomination public key.
*/ */
@Checkable.String @Checkable.String()
denomPub: string; denomPub: string;
/** /**
* Hash of the denomination public key. * Hash of the denomination public key.
* Stored in the database for faster lookups. * Stored in the database for faster lookups.
*/ */
@Checkable.String @Checkable.String()
denomPubHash: string; denomPubHash: string;
/** /**
@ -249,38 +249,38 @@ export class DenominationRecord {
/** /**
* Validity start date of the denomination. * Validity start date of the denomination.
*/ */
@Checkable.String @Checkable.String()
stampStart: string; stampStart: string;
/** /**
* Date after which the currency can't be withdrawn anymore. * Date after which the currency can't be withdrawn anymore.
*/ */
@Checkable.String @Checkable.String()
stampExpireWithdraw: string; stampExpireWithdraw: string;
/** /**
* Date after the denomination officially doesn't exist anymore. * Date after the denomination officially doesn't exist anymore.
*/ */
@Checkable.String @Checkable.String()
stampExpireLegal: string; stampExpireLegal: string;
/** /**
* Data after which coins of this denomination can't be deposited anymore. * Data after which coins of this denomination can't be deposited anymore.
*/ */
@Checkable.String @Checkable.String()
stampExpireDeposit: string; stampExpireDeposit: string;
/** /**
* Signature by the exchange's master key over the denomination * Signature by the exchange's master key over the denomination
* information. * information.
*/ */
@Checkable.String @Checkable.String()
masterSig: string; masterSig: string;
/** /**
* Did we verify the signature on the denomination? * Did we verify the signature on the denomination?
*/ */
@Checkable.Number @Checkable.Number()
status: DenominationStatus; status: DenominationStatus;
/** /**
@ -288,13 +288,13 @@ export class DenominationRecord {
* we checked? * we checked?
* Only false when the exchange redacts a previously published denomination. * Only false when the exchange redacts a previously published denomination.
*/ */
@Checkable.Boolean @Checkable.Boolean()
isOffered: boolean; isOffered: boolean;
/** /**
* Base URL of the exchange. * Base URL of the exchange.
*/ */
@Checkable.String @Checkable.String()
exchangeBaseUrl: string; exchangeBaseUrl: string;
/** /**
@ -500,7 +500,7 @@ export class ProposalDownloadRecord {
/** /**
* URL where the proposal was downloaded. * URL where the proposal was downloaded.
*/ */
@Checkable.String @Checkable.String()
url: string; url: string;
/** /**
@ -512,32 +512,32 @@ export class ProposalDownloadRecord {
/** /**
* Signature by the merchant over the contract details. * Signature by the merchant over the contract details.
*/ */
@Checkable.String @Checkable.String()
merchantSig: string; merchantSig: string;
/** /**
* Hash of the contract terms. * Hash of the contract terms.
*/ */
@Checkable.String @Checkable.String()
contractTermsHash: string; contractTermsHash: string;
/** /**
* Serial ID when the offer is stored in the wallet DB. * Serial ID when the offer is stored in the wallet DB.
*/ */
@Checkable.Optional(Checkable.Number) @Checkable.Optional(Checkable.Number())
id?: number; id?: number;
/** /**
* Timestamp (in ms) of when the record * Timestamp (in ms) of when the record
* was created. * was created.
*/ */
@Checkable.Number @Checkable.Number()
timestamp: number; timestamp: number;
/** /**
* Private key for the nonce. * Private key for the nonce.
*/ */
@Checkable.String @Checkable.String()
noncePriv: 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 { export function getTalerStampSec(stamp: string): number | null {
const m = stamp.match(/\/?Date\(([0-9]*)\)\/?/); const m = stamp.match(/\/?Date\(([0-9]*)\)\/?/);
if (!m) { if (!m || !m[1]) {
return null; return null;
} }
return parseInt(m[1], 10); 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. * Get a JavaScript Date object from a Taler date string.

View File

@ -26,9 +26,12 @@
/** /**
* Imports. * Imports.
*/ */
import { AmountJson } from "./amounts";
import { Checkable } from "./checkable"; import { Checkable } from "./checkable";
import * as Amounts from "./amounts";
import { timestampCheck } from "./helpers";
/** /**
* Denomination as found in the /keys response from the exchange. * Denomination as found in the /keys response from the exchange.
@ -38,70 +41,70 @@ export class Denomination {
/** /**
* Value of one coin of the denomination. * Value of one coin of the denomination.
*/ */
@Checkable.Value(() => AmountJson) @Checkable.String(Amounts.check)
value: AmountJson; value: string;
/** /**
* Public signing key of the denomination. * Public signing key of the denomination.
*/ */
@Checkable.String @Checkable.String()
denom_pub: string; denom_pub: string;
/** /**
* Fee for withdrawing. * Fee for withdrawing.
*/ */
@Checkable.Value(() => AmountJson) @Checkable.String(Amounts.check)
fee_withdraw: AmountJson; fee_withdraw: string;
/** /**
* Fee for depositing. * Fee for depositing.
*/ */
@Checkable.Value(() => AmountJson) @Checkable.String(Amounts.check)
fee_deposit: AmountJson; fee_deposit: string;
/** /**
* Fee for refreshing. * Fee for refreshing.
*/ */
@Checkable.Value(() => AmountJson) @Checkable.String(Amounts.check)
fee_refresh: AmountJson; fee_refresh: string;
/** /**
* Fee for refunding. * Fee for refunding.
*/ */
@Checkable.Value(() => AmountJson) @Checkable.String(Amounts.check)
fee_refund: AmountJson; fee_refund: string;
/** /**
* Start date from which withdraw is allowed. * Start date from which withdraw is allowed.
*/ */
@Checkable.String @Checkable.String(timestampCheck)
stamp_start: string; stamp_start: string;
/** /**
* End date for withdrawing. * End date for withdrawing.
*/ */
@Checkable.String @Checkable.String(timestampCheck)
stamp_expire_withdraw: string; stamp_expire_withdraw: string;
/** /**
* Expiration date after which the exchange can forget about * Expiration date after which the exchange can forget about
* the currency. * the currency.
*/ */
@Checkable.String @Checkable.String(timestampCheck)
stamp_expire_legal: string; stamp_expire_legal: string;
/** /**
* Date after which the coins of this denomination can't be * Date after which the coins of this denomination can't be
* deposited anymore. * deposited anymore.
*/ */
@Checkable.String @Checkable.String(timestampCheck)
stamp_expire_deposit: string; stamp_expire_deposit: string;
/** /**
* Signature over the denomination information by the exchange's master * Signature over the denomination information by the exchange's master
* signing key. * signing key.
*/ */
@Checkable.String @Checkable.String()
master_sig: string; master_sig: string;
/** /**
@ -120,13 +123,13 @@ export class AuditorDenomSig {
/** /**
* Denomination public key's hash. * Denomination public key's hash.
*/ */
@Checkable.String @Checkable.String()
denom_pub_h: string; denom_pub_h: string;
/** /**
* The signature. * The signature.
*/ */
@Checkable.String @Checkable.String()
auditor_sig: string; auditor_sig: string;
} }
@ -139,13 +142,13 @@ export class Auditor {
/** /**
* Auditor's public key. * Auditor's public key.
*/ */
@Checkable.String @Checkable.String()
auditor_pub: string; auditor_pub: string;
/** /**
* Base URL of the auditor. * Base URL of the auditor.
*/ */
@Checkable.String @Checkable.String()
auditor_url: string; auditor_url: string;
/** /**
@ -197,20 +200,20 @@ export class PaybackConfirmation {
/** /**
* public key of the reserve that will receive the payback. * public key of the reserve that will receive the payback.
*/ */
@Checkable.String @Checkable.String()
reserve_pub: string; reserve_pub: string;
/** /**
* How much will the exchange pay back (needed by wallet in * How much will the exchange pay back (needed by wallet in
* case coin was partially spent and wallet got restored from backup) * case coin was partially spent and wallet got restored from backup)
*/ */
@Checkable.Value(() => AmountJson) @Checkable.String()
amount: AmountJson; amount: string;
/** /**
* Time by which the exchange received the /payback request. * Time by which the exchange received the /payback request.
*/ */
@Checkable.String @Checkable.String()
timestamp: string; timestamp: string;
/** /**
@ -220,7 +223,7 @@ export class PaybackConfirmation {
* by the date specified (this allows the exchange delaying the transfer * by the date specified (this allows the exchange delaying the transfer
* a bit to aggregate additional payback requests into a larger one). * a bit to aggregate additional payback requests into a larger one).
*/ */
@Checkable.String @Checkable.String()
exchange_sig: string; exchange_sig: string;
/** /**
@ -229,7 +232,7 @@ export class PaybackConfirmation {
* explicitly as the client might otherwise be confused by clock skew as to * explicitly as the client might otherwise be confused by clock skew as to
* which signing key was used. * which signing key was used.
*/ */
@Checkable.String @Checkable.String()
exchange_pub: string; exchange_pub: string;
/** /**
@ -263,7 +266,7 @@ export interface CoinPaySig {
/** /**
* The amount that is subtracted from this coin with this payment. * The amount that is subtracted from this coin with this payment.
*/ */
contribution: AmountJson; contribution: string;
/** /**
* URL of the exchange this coin was withdrawn from. * URL of the exchange this coin was withdrawn from.
@ -281,13 +284,13 @@ export class ExchangeHandle {
/** /**
* Master public signing key of the exchange. * Master public signing key of the exchange.
*/ */
@Checkable.String @Checkable.String()
master_pub: string; master_pub: string;
/** /**
* Base URL of the exchange. * Base URL of the exchange.
*/ */
@Checkable.String @Checkable.String()
url: string; url: string;
/** /**
@ -312,67 +315,67 @@ export class ContractTerms {
/** /**
* Hash of the merchant's wire details. * Hash of the merchant's wire details.
*/ */
@Checkable.String @Checkable.String()
H_wire: string; H_wire: string;
/** /**
* Wire method the merchant wants to use. * Wire method the merchant wants to use.
*/ */
@Checkable.String @Checkable.String()
wire_method: string; wire_method: string;
/** /**
* Human-readable short summary of the contract. * Human-readable short summary of the contract.
*/ */
@Checkable.Optional(Checkable.String) @Checkable.Optional(Checkable.String())
summary?: string; summary?: string;
/** /**
* Nonce used to ensure freshness. * Nonce used to ensure freshness.
*/ */
@Checkable.Optional(Checkable.String) @Checkable.Optional(Checkable.String())
nonce?: string; nonce?: string;
/** /**
* Total amount payable. * Total amount payable.
*/ */
@Checkable.Value(() => AmountJson) @Checkable.String(Amounts.check)
amount: AmountJson; amount: string;
/** /**
* Auditors accepted by the merchant. * Auditors accepted by the merchant.
*/ */
@Checkable.List(Checkable.AnyObject) @Checkable.List(Checkable.AnyObject())
auditors: any[]; auditors: any[];
/** /**
* Deadline to pay for the contract. * Deadline to pay for the contract.
*/ */
@Checkable.Optional(Checkable.String) @Checkable.Optional(Checkable.String())
pay_deadline: string; pay_deadline: string;
/** /**
* Delivery locations. * Delivery locations.
*/ */
@Checkable.Any @Checkable.Any()
locations: any; locations: any;
/** /**
* Maximum deposit fee covered by the merchant. * Maximum deposit fee covered by the merchant.
*/ */
@Checkable.Value(() => AmountJson) @Checkable.String(Amounts.check)
max_fee: AmountJson; max_fee: string;
/** /**
* Information about the merchant. * Information about the merchant.
*/ */
@Checkable.Any @Checkable.Any()
merchant: any; merchant: any;
/** /**
* Public key of the merchant. * Public key of the merchant.
*/ */
@Checkable.String @Checkable.String()
merchant_pub: string; merchant_pub: string;
/** /**
@ -384,57 +387,57 @@ export class ContractTerms {
/** /**
* Products that are sold in this contract. * Products that are sold in this contract.
*/ */
@Checkable.List(Checkable.AnyObject) @Checkable.List(Checkable.AnyObject())
products: any[]; products: any[];
/** /**
* Deadline for refunds. * Deadline for refunds.
*/ */
@Checkable.String @Checkable.String(timestampCheck)
refund_deadline: string; refund_deadline: string;
/** /**
* Time when the contract was generated by the merchant. * Time when the contract was generated by the merchant.
*/ */
@Checkable.String @Checkable.String(timestampCheck)
timestamp: string; timestamp: string;
/** /**
* Order id to uniquely identify the purchase within * Order id to uniquely identify the purchase within
* one merchant instance. * one merchant instance.
*/ */
@Checkable.String @Checkable.String()
order_id: string; order_id: string;
/** /**
* URL to post the payment to. * URL to post the payment to.
*/ */
@Checkable.String @Checkable.String()
pay_url: string; pay_url: string;
/** /**
* Fulfillment URL to view the product or * Fulfillment URL to view the product or
* delivery status. * delivery status.
*/ */
@Checkable.String @Checkable.String()
fulfillment_url: string; fulfillment_url: string;
/** /**
* Share of the wire fee that must be settled with one payment. * Share of the wire fee that must be settled with one payment.
*/ */
@Checkable.Optional(Checkable.Number) @Checkable.Optional(Checkable.Number())
wire_fee_amortization?: number; wire_fee_amortization?: number;
/** /**
* Maximum wire fee that the merchant agrees to pay for. * Maximum wire fee that the merchant agrees to pay for.
*/ */
@Checkable.Optional(Checkable.Value(() => AmountJson)) @Checkable.Optional(Checkable.String())
max_wire_fee?: AmountJson; max_wire_fee?: string;
/** /**
* Extra data, interpreted by the mechant only. * Extra data, interpreted by the mechant only.
*/ */
@Checkable.Any @Checkable.Any()
extra: any; extra: any;
/** /**
@ -480,31 +483,31 @@ export class MerchantRefundPermission {
/** /**
* Amount to be refunded. * Amount to be refunded.
*/ */
@Checkable.Value(() => AmountJson) @Checkable.String(Amounts.check)
refund_amount: AmountJson; refund_amount: string;
/** /**
* Fee for the refund. * Fee for the refund.
*/ */
@Checkable.Value(() => AmountJson) @Checkable.String(Amounts.check)
refund_fee: AmountJson; refund_fee: string;
/** /**
* Public key of the coin being refunded. * Public key of the coin being refunded.
*/ */
@Checkable.String @Checkable.String()
coin_pub: string; coin_pub: string;
/** /**
* Refund transaction ID between merchant and exchange. * Refund transaction ID between merchant and exchange.
*/ */
@Checkable.Number @Checkable.Number()
rtransaction_id: number; rtransaction_id: number;
/** /**
* Signature made by the merchant over the refund permission. * Signature made by the merchant over the refund permission.
*/ */
@Checkable.String @Checkable.String()
merchant_sig: string; merchant_sig: string;
/** /**
@ -523,13 +526,13 @@ export interface RefundRequest {
* coin's total deposit value (including deposit fee); * coin's total deposit value (including deposit fee);
* must be larger than the refund fee. * must be larger than the refund fee.
*/ */
refund_amount: AmountJson; refund_amount: string;
/** /**
* Refund fee associated with the given coin. * Refund fee associated with the given coin.
* must be smaller than the refund amount. * 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. * SHA-512 hash of the contact of the merchant with the customer.
@ -566,14 +569,14 @@ export class MerchantRefundResponse {
/** /**
* Public key of the merchant * Public key of the merchant
*/ */
@Checkable.String @Checkable.String()
merchant_pub: string; merchant_pub: string;
/** /**
* Contract terms hash of the contract that * Contract terms hash of the contract that
* is being refunded. * is being refunded.
*/ */
@Checkable.String @Checkable.String()
h_contract_terms: string; h_contract_terms: string;
/** /**
@ -629,7 +632,7 @@ export class ReserveSigSingleton {
/** /**
* Reserve signature. * Reserve signature.
*/ */
@Checkable.String @Checkable.String()
reserve_sig: string; reserve_sig: string;
/** /**
@ -638,6 +641,30 @@ export class ReserveSigSingleton {
static checked: (obj: any) => 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 * Response of the merchant
* to the TipPickupRequest. * to the TipPickupRequest.
@ -647,7 +674,7 @@ export class TipResponse {
/** /**
* Public key of the reserve * Public key of the reserve
*/ */
@Checkable.String @Checkable.String()
reserve_pub: string; reserve_pub: string;
/** /**
@ -671,37 +698,37 @@ export class TipToken {
/** /**
* Expiration for the tip. * Expiration for the tip.
*/ */
@Checkable.String @Checkable.String(timestampCheck)
expiration: string; expiration: string;
/** /**
* URL of the exchange that the tip can be withdrawn from. * URL of the exchange that the tip can be withdrawn from.
*/ */
@Checkable.String @Checkable.String()
exchange_url: string; exchange_url: string;
/** /**
* Merchant's URL to pick up the tip. * Merchant's URL to pick up the tip.
*/ */
@Checkable.String @Checkable.String()
pickup_url: string; pickup_url: string;
/** /**
* Merchant-chosen tip identifier. * Merchant-chosen tip identifier.
*/ */
@Checkable.String @Checkable.String()
tip_id: string; tip_id: string;
/** /**
* Amount of tip. * Amount of tip.
*/ */
@Checkable.Value(() => AmountJson) @Checkable.String()
amount: AmountJson; amount: string;
/** /**
* URL to navigate after finishing tip processing. * URL to navigate after finishing tip processing.
*/ */
@Checkable.String @Checkable.String()
next_url: string; next_url: string;
/** /**
@ -721,7 +748,7 @@ export class Payback {
/** /**
* The hash of the denomination public key for which the payback is offered. * The hash of the denomination public key for which the payback is offered.
*/ */
@Checkable.String @Checkable.String()
h_denom_pub: string; h_denom_pub: string;
} }
@ -740,7 +767,7 @@ export class KeysJson {
/** /**
* The exchange's master public key. * The exchange's master public key.
*/ */
@Checkable.String @Checkable.String()
master_public_key: string; master_public_key: string;
/** /**
@ -752,7 +779,7 @@ export class KeysJson {
/** /**
* Timestamp when this response was issued. * Timestamp when this response was issued.
*/ */
@Checkable.String @Checkable.String(timestampCheck)
list_issue_date: string; list_issue_date: string;
/** /**
@ -765,13 +792,13 @@ export class KeysJson {
* Short-lived signing keys used to sign online * Short-lived signing keys used to sign online
* responses. * responses.
*/ */
@Checkable.Any @Checkable.Any()
signkeys: any; signkeys: any;
/** /**
* Protocol version. * Protocol version.
*/ */
@Checkable.Optional(Checkable.String) @Checkable.Optional(Checkable.String())
version?: string; version?: string;
/** /**
@ -790,31 +817,31 @@ export class WireFeesJson {
/** /**
* Cost of a wire transfer. * Cost of a wire transfer.
*/ */
@Checkable.Value(() => AmountJson) @Checkable.String(Amounts.check)
wire_fee: AmountJson; wire_fee: string;
/** /**
* Cost of clising a reserve. * Cost of clising a reserve.
*/ */
@Checkable.Value(() => AmountJson) @Checkable.String(Amounts.check)
closing_fee: AmountJson; closing_fee: string;
/** /**
* Signature made with the exchange's master key. * Signature made with the exchange's master key.
*/ */
@Checkable.String @Checkable.String()
sig: string; sig: string;
/** /**
* Date from which the fee applies. * Date from which the fee applies.
*/ */
@Checkable.String @Checkable.String(timestampCheck)
start_date: string; start_date: string;
/** /**
* Data after which the fee doesn't apply anymore. * Data after which the fee doesn't apply anymore.
*/ */
@Checkable.String @Checkable.String(timestampCheck)
end_date: string; end_date: string;
/** /**
@ -834,7 +861,7 @@ export class WireDetailJson {
/** /**
* Name of the wire transfer method. * Name of the wire transfer method.
*/ */
@Checkable.String @Checkable.String()
type: string; type: string;
/** /**
@ -872,7 +899,7 @@ export class Proposal {
@Checkable.Value(() => ContractTerms) @Checkable.Value(() => ContractTerms)
contract_terms: ContractTerms; contract_terms: ContractTerms;
@Checkable.String @Checkable.String()
sig: 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) => { test("contract terms validation", (t) => {
const c = { const c = {
H_wire: "123", H_wire: "123",
amount: amt(1, 2, "EUR"), amount: "EUR:1.5",
auditors: [], auditors: [],
exchanges: [{master_pub: "foo", url: "foo"}], exchanges: [{master_pub: "foo", url: "foo"}],
fulfillment_url: "foo", fulfillment_url: "foo",
max_fee: amt(1, 2, "EUR"), max_fee: "EUR:1.5",
merchant_pub: "12345", merchant_pub: "12345",
order_id: "test_order", order_id: "test_order",
pay_deadline: "Date(12346)", pay_deadline: "Date(12346)",

View File

@ -82,6 +82,7 @@ import {
PaybackConfirmation, PaybackConfirmation,
Proposal, Proposal,
RefundRequest, RefundRequest,
ReserveStatus,
TipPlanchetDetail, TipPlanchetDetail,
TipResponse, TipResponse,
TipToken, TipToken,
@ -825,13 +826,22 @@ export class Wallet {
return this.submitPay(purchase.contractTermsHash, sessionId); 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({ const res = await this.getCoinsForPayment({
allowedAuditors: proposal.contractTerms.auditors, allowedAuditors: proposal.contractTerms.auditors,
allowedExchanges: proposal.contractTerms.exchanges, allowedExchanges: proposal.contractTerms.exchanges,
depositFeeLimit: proposal.contractTerms.max_fee, depositFeeLimit: Amounts.parseOrThrow(proposal.contractTerms.max_fee),
paymentAmount: proposal.contractTerms.amount, paymentAmount: Amounts.parseOrThrow(proposal.contractTerms.amount),
wireFeeAmortization: proposal.contractTerms.wire_fee_amortization || 1, 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, wireFeeTime: getTalerStampSec(proposal.contractTerms.timestamp) || 0,
wireMethod: proposal.contractTerms.wire_method, wireMethod: proposal.contractTerms.wire_method,
}); });
@ -907,14 +917,23 @@ export class Wallet {
return { status: "paid" }; 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. // If not already payed, check if we could pay for it.
const res = await this.getCoinsForPayment({ const res = await this.getCoinsForPayment({
allowedAuditors: proposal.contractTerms.auditors, allowedAuditors: proposal.contractTerms.auditors,
allowedExchanges: proposal.contractTerms.exchanges, allowedExchanges: proposal.contractTerms.exchanges,
depositFeeLimit: proposal.contractTerms.max_fee, depositFeeLimit: Amounts.parseOrThrow(proposal.contractTerms.max_fee),
paymentAmount: proposal.contractTerms.amount, paymentAmount,
wireFeeAmortization: proposal.contractTerms.wire_fee_amortization || 1, 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, wireFeeTime: getTalerStampSec(proposal.contractTerms.timestamp) || 0,
wireMethod: proposal.contractTerms.wire_method, wireMethod: proposal.contractTerms.wire_method,
}); });
@ -1290,11 +1309,11 @@ export class Wallet {
if (resp.status !== 200) { if (resp.status !== 200) {
throw Error(); throw Error();
} }
const reserveInfo = JSON.parse(resp.responseText); const reserveInfo = ReserveStatus.checked(JSON.parse(resp.responseText));
if (!reserveInfo) { if (!reserveInfo) {
throw Error(); throw Error();
} }
reserve.current_amount = reserveInfo.balance; reserve.current_amount = Amounts.parseOrThrow(reserveInfo.balance);
await this.q() await this.q()
.put(Stores.reserves, reserve) .put(Stores.reserves, reserve)
.finish(); .finish();
@ -1613,7 +1632,7 @@ export class Wallet {
exchangeInfo = { exchangeInfo = {
auditors: exchangeKeysJson.auditors, auditors: exchangeKeysJson.auditors,
baseUrl, baseUrl,
currency: exchangeKeysJson.denoms[0].value.currency, currency: Amounts.parseOrThrow(exchangeKeysJson.denoms[0].value).currency,
lastUpdateTime: updateTimeSec, lastUpdateTime: updateTimeSec,
lastUsedTime: 0, lastUsedTime: 0,
masterPublicKey: exchangeKeysJson.master_public_key, masterPublicKey: exchangeKeysJson.master_public_key,
@ -1670,11 +1689,11 @@ export class Wallet {
continue; continue;
} }
const wf: WireFee = { const wf: WireFee = {
closingFee: fee.closing_fee, closingFee: Amounts.parseOrThrow(fee.closing_fee),
endStamp: end, endStamp: end,
sig: fee.sig, sig: fee.sig,
startStamp: start, startStamp: start,
wireFee: fee.wire_fee, wireFee: Amounts.parseOrThrow(fee.wire_fee),
}; };
const valid: boolean = await this.cryptoApi.isValidWireFee(detail.type, wf, exchangeInfo.masterPublicKey); const valid: boolean = await this.cryptoApi.isValidWireFee(detail.type, wf, exchangeInfo.masterPublicKey);
if (!valid) { if (!valid) {
@ -1829,7 +1848,7 @@ export class Wallet {
return balance; return balance;
} }
for (const c of t.payReq.coins) { 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; return balance;
} }
@ -2180,10 +2199,17 @@ export class Wallet {
type: "pay", type: "pay",
}); });
if (p.timestamp_refund) { if (p.timestamp_refund) {
const amountsPending = Object.keys(p.refundsPending).map((x) => p.refundsPending[x].refund_amount); const contractAmount = Amounts.parseOrThrow(p.contractTerms.amount);
const amountsDone = Object.keys(p.refundsDone).map((x) => p.refundsDone[x].refund_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 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({ history.push({
detail: { detail: {
@ -2357,10 +2383,10 @@ export class Wallet {
denomPub: denomIn.denom_pub, denomPub: denomIn.denom_pub,
denomPubHash, denomPubHash,
exchangeBaseUrl, exchangeBaseUrl,
feeDeposit: denomIn.fee_deposit, feeDeposit: Amounts.parseOrThrow(denomIn.fee_deposit),
feeRefresh: denomIn.fee_refresh, feeRefresh: Amounts.parseOrThrow(denomIn.fee_refresh),
feeRefund: denomIn.fee_refund, feeRefund: Amounts.parseOrThrow(denomIn.fee_refund),
feeWithdraw: denomIn.fee_withdraw, feeWithdraw: Amounts.parseOrThrow(denomIn.fee_withdraw),
isOffered: true, isOffered: true,
masterSig: denomIn.master_sig, masterSig: denomIn.master_sig,
stampExpireDeposit: denomIn.stamp_expire_deposit, stampExpireDeposit: denomIn.stamp_expire_deposit,
@ -2368,7 +2394,7 @@ export class Wallet {
stampExpireWithdraw: denomIn.stamp_expire_withdraw, stampExpireWithdraw: denomIn.stamp_expire_withdraw,
stampStart: denomIn.stamp_start, stampStart: denomIn.stamp_start,
status: DenominationStatus.Unverified, status: DenominationStatus.Unverified,
value: denomIn.value, value: Amounts.parseOrThrow(denomIn.value),
}; };
return d; return d;
} }
@ -2449,13 +2475,13 @@ export class Wallet {
const contractTerms: ContractTerms = { const contractTerms: ContractTerms = {
H_wire: wireHash, H_wire: wireHash,
amount: req.amount, amount: Amounts.toString(req.amount),
auditors: [], auditors: [],
exchanges: [ { master_pub: exchange.masterPublicKey, url: exchange.baseUrl } ], exchanges: [ { master_pub: exchange.masterPublicKey, url: exchange.baseUrl } ],
extra: {}, extra: {},
fulfillment_url: "", fulfillment_url: "",
locations: [], locations: [],
max_fee: req.amount, max_fee: Amounts.toString(req.amount),
merchant: {}, merchant: {},
merchant_pub: pub, merchant_pub: pub,
order_id: "none", order_id: "none",
@ -2469,7 +2495,9 @@ export class Wallet {
const contractTermsHash = await this.cryptoApi.hashString(canonicalJson(contractTerms)); 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); console.log("pci", payCoinInfo);
@ -2660,9 +2688,11 @@ export class Wallet {
console.warn("coin not found, can't apply refund"); console.warn("coin not found, can't apply refund");
return; return;
} }
const refundAmount = Amounts.parseOrThrow(perm.refund_amount);
const refundFee = Amounts.parseOrThrow(perm.refund_fee);
c.status = CoinStatus.Dirty; c.status = CoinStatus.Dirty;
c.currentAmount = Amounts.add(c.currentAmount, perm.refund_amount).amount; c.currentAmount = Amounts.add(c.currentAmount, refundAmount).amount;
c.currentAmount = Amounts.sub(c.currentAmount, perm.refund_fee).amount; c.currentAmount = Amounts.sub(c.currentAmount, refundFee).amount;
return c; return c;
}; };
@ -2690,7 +2720,7 @@ export class Wallet {
if (!coin0) { if (!coin0) {
throw Error("coin not found"); 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(); const denoms = await this.q().iterIndex(Stores.denominations.exchangeBaseUrlIndex, coin0.exchangeBaseUrl).toArray();
for (const rp of refundPermissions) { for (const rp of refundPermissions) {
@ -2706,8 +2736,10 @@ export class Wallet {
// When it hasn't, the refresh cost is inaccurate. To fix this, // 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 // we need introduce a flag to tell if a coin was refunded or
// refreshed normally (and what about incremental refunds?) // refreshed normally (and what about incremental refunds?)
const refreshCost = getTotalRefreshCost(denoms, denom, Amounts.sub(rp.refund_amount, rp.refund_fee).amount); const refundAmount = Amounts.parseOrThrow(rp.refund_amount);
feeAcc = Amounts.add(feeAcc, refreshCost, rp.refund_fee).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; return feeAcc;
} }
@ -2731,14 +2763,15 @@ export class Wallet {
if (tipRecord && tipRecord.pickedUp) { if (tipRecord && tipRecord.pickedUp) {
return tipRecord; return tipRecord;
} }
const tipAmount = Amounts.parseOrThrow(tipToken.amount);
await this.updateExchangeFromUrl(tipToken.exchange_url); 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 planchets = await Promise.all(denomsForWithdraw.map(d => this.cryptoApi.createTipPlanchet(d)));
const coinPubs: string[] = planchets.map(x => x.coinPub); const coinPubs: string[] = planchets.map(x => x.coinPub);
const now = (new Date()).getTime(); const now = (new Date()).getTime();
tipRecord = { tipRecord = {
accepted: false, accepted: false,
amount: tipToken.amount, amount: Amounts.parseOrThrow(tipToken.amount),
coinPubs, coinPubs,
deadline: deadlineSec, deadline: deadlineSec,
exchangeUrl: tipToken.exchange_url, exchangeUrl: tipToken.exchange_url,

View File

@ -53,13 +53,13 @@ export class CreateReserveResponse {
* Exchange URL where the bank should create the reserve. * Exchange URL where the bank should create the reserve.
* The URL is canonicalized in the response. * The URL is canonicalized in the response.
*/ */
@Checkable.String @Checkable.String()
exchange: string; exchange: string;
/** /**
* Reserve public key of the newly created reserve. * Reserve public key of the newly created reserve.
*/ */
@Checkable.String @Checkable.String()
reservePub: string; reservePub: string;
/** /**
@ -333,13 +333,13 @@ export class CreateReserveRequest {
/** /**
* Exchange URL where the bank should create the reserve. * Exchange URL where the bank should create the reserve.
*/ */
@Checkable.String @Checkable.String()
exchange: string; exchange: string;
/** /**
* Wire details for the bank account that sent the funds to the exchange. * Wire details for the bank account that sent the funds to the exchange.
*/ */
@Checkable.Optional(Checkable.Any) @Checkable.Optional(Checkable.Any())
senderWire?: object; senderWire?: object;
/** /**
@ -359,7 +359,7 @@ export class ConfirmReserveRequest {
* Public key of then reserve that should be marked * Public key of then reserve that should be marked
* as confirmed. * as confirmed.
*/ */
@Checkable.String @Checkable.String()
reservePub: string; reservePub: string;
/** /**
@ -384,14 +384,14 @@ export class ReturnCoinsRequest {
/** /**
* The exchange to take the coins from. * The exchange to take the coins from.
*/ */
@Checkable.String @Checkable.String()
exchange: string; exchange: string;
/** /**
* Wire details for the bank account of the customer that will * Wire details for the bank account of the customer that will
* receive the funds. * receive the funds.
*/ */
@Checkable.Any @Checkable.Any()
senderWire?: object; senderWire?: object;
/** /**

View File

@ -42,6 +42,8 @@ import * as ReactDOM from "react-dom";
import URI = require("urijs"); import URI = require("urijs");
import { WalletApiError } from "../wxApi"; import { WalletApiError } from "../wxApi";
import * as Amounts from "../../amounts";
interface DetailState { interface DetailState {
collapsed: boolean; collapsed: boolean;
@ -294,7 +296,7 @@ class ContractPrompt extends React.Component<ContractPromptProps, ContractPrompt
} else { } else {
merchantName = <strong>(pub: {c.merchant_pub})</strong>; 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); console.log("payStatus", this.state.payStatus);
let products = null; let products = null;

View File

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

View File

@ -59,18 +59,24 @@ const RefundDetail = ({purchase, fullRefundFees}: RefundDetailProps) => {
} }
const firstRefundKey = [...pendingKeys, ...doneKeys][0]; 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) { if (!currency) {
throw Error("invariant"); throw Error("invariant");
} }
let amountPending = Amounts.getZero(currency); let amountPending = Amounts.getZero(currency);
for (const k of pendingKeys) { 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); let amountDone = Amounts.getZero(currency);
for (const k of doneKeys) { 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; 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>{" "} Status of purchase <strong>{summary}</strong> from merchant <strong>{merchantName}</strong>{" "}
(order id {purchase.contractTerms.order_id}). (order id {purchase.contractTerms.order_id}).
</p> </p>
<p>Total amount: <AmountDisplay amount={purchase.contractTerms.amount} /></p> <p>Total amount: <AmountDisplay amount={Amounts.parseOrThrow(purchase.contractTerms.amount)} /></p>
{purchase.finished {purchase.finished
? <RefundDetail purchase={purchase} fullRefundFees={this.state.refundFees!} /> ? <RefundDetail purchase={purchase} fullRefundFees={this.state.refundFees!} />
: <p>Purchase not completed.</p>} : <p>Purchase not completed.</p>}
@ -147,13 +153,16 @@ class RefundStatusView extends React.Component<RefundStatusViewProps, RefundStat
return; return;
} }
contractTermsHash = await wxApi.acceptRefund(refundUrl); contractTermsHash = await wxApi.acceptRefund(refundUrl);
this.setState({ contractTermsHash });
} }
const purchase = await wxApi.getPurchase(contractTermsHash); const purchase = await wxApi.getPurchase(contractTermsHash);
console.log("got purchase", purchase); console.log("got purchase", purchase);
const refundsDone = Object.keys(purchase.refundsDone).map((x) => purchase.refundsDone[x]); const refundsDone = Object.keys(purchase.refundsDone).map((x) => purchase.refundsDone[x]);
const refundFees = await wxApi.getFullRefundFees( {refundPermissions: refundsDone }); if (refundsDone.length) {
const refundFees = await wxApi.getFullRefundFees({ refundPermissions: refundsDone });
this.setState({ purchase, gotResult: true, refundFees }); this.setState({ purchase, gotResult: true, refundFees });
} }
}
} }

View File

@ -54,6 +54,7 @@
"src/walletTypes.ts", "src/walletTypes.ts",
"src/webex/background.ts", "src/webex/background.ts",
"src/webex/chromeBadge.ts", "src/webex/chromeBadge.ts",
"src/webex/compat.ts",
"src/webex/components.ts", "src/webex/components.ts",
"src/webex/messages.ts", "src/webex/messages.ts",
"src/webex/notify.ts", "src/webex/notify.ts",