diff options
-rw-r--r-- | src/amounts.ts | 36 | ||||
-rw-r--r-- | src/checkable.ts | 67 | ||||
-rw-r--r-- | src/crypto/cryptoWorker.ts | 4 | ||||
-rw-r--r-- | src/dbTypes.ts | 34 | ||||
-rw-r--r-- | src/helpers.ts | 9 | ||||
-rw-r--r-- | src/talerTypes.ts | 201 | ||||
-rw-r--r-- | src/types-test.ts | 13 | ||||
-rw-r--r-- | src/wallet.ts | 97 | ||||
-rw-r--r-- | src/walletTypes.ts | 14 | ||||
-rw-r--r-- | src/webex/pages/confirm-contract.tsx | 4 | ||||
-rw-r--r-- | src/webex/pages/confirm-create-reserve.tsx | 2 | ||||
-rw-r--r-- | src/webex/pages/refund.tsx | 21 | ||||
-rw-r--r-- | tsconfig.json | 1 |
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", |