diff options
author | Florian Dold <florian.dold@gmail.com> | 2019-12-19 20:42:49 +0100 |
---|---|---|
committer | Florian Dold <florian.dold@gmail.com> | 2019-12-19 20:42:49 +0100 |
commit | 0c9358c1b2bd80e25940022e86bd8daef8184ad7 (patch) | |
tree | a8c8ca0134bd886d8151633aff4c85e9513ad32c | |
parent | 49e3b3e5b9bbf1ce356ef68f301d50c689ceecb9 (diff) |
new date format, replace checkable annotations with codecs
35 files changed, 907 insertions, 1483 deletions
diff --git a/src/crypto/workers/cryptoApi.ts b/src/crypto/workers/cryptoApi.ts index da807cce0..1c54d286a 100644 --- a/src/crypto/workers/cryptoApi.ts +++ b/src/crypto/workers/cryptoApi.ts @@ -30,6 +30,7 @@ import { RefreshSessionRecord, TipPlanchet, WireFee, + WalletContractData, } from "../../types/dbTypes"; import { CryptoWorker } from "./cryptoWorker"; @@ -384,14 +385,16 @@ export class CryptoApi { } signDeposit( - contractTerms: ContractTerms, + contractTermsRaw: string, + contractData: WalletContractData, cds: CoinWithDenom[], totalAmount: AmountJson, ): Promise<PaySigInfo> { return this.doRpc<PaySigInfo>( "signDeposit", 3, - contractTerms, + contractTermsRaw, + contractData, cds, totalAmount, ); diff --git a/src/crypto/workers/cryptoImplementation.ts b/src/crypto/workers/cryptoImplementation.ts index d745f497c..043711864 100644 --- a/src/crypto/workers/cryptoImplementation.ts +++ b/src/crypto/workers/cryptoImplementation.ts @@ -33,6 +33,7 @@ import { TipPlanchet, WireFee, initRetryInfo, + WalletContractData, } from "../../types/dbTypes"; import { CoinPaySig, ContractTerms, PaybackRequest } from "../../types/talerTypes"; @@ -40,13 +41,11 @@ import { BenchmarkResult, CoinWithDenom, PaySigInfo, - Timestamp, PlanchetCreationResult, PlanchetCreationRequest, - getTimestampNow, CoinPayInfo, } from "../../types/walletTypes"; -import { canonicalJson, getTalerStampSec } from "../../util/helpers"; +import { canonicalJson } from "../../util/helpers"; import { AmountJson } from "../../util/amounts"; import * as Amounts from "../../util/amounts"; import * as timer from "../../util/timer"; @@ -70,6 +69,7 @@ import { } from "../talerCrypto"; import { randomBytes } from "../primitives/nacl-fast"; import { kdf } from "../primitives/kdf"; +import { Timestamp, getTimestampNow } from "../../util/time"; enum SignaturePurpose { RESERVE_WITHDRAW = 1200, @@ -104,20 +104,6 @@ function timestampToBuffer(ts: Timestamp): Uint8Array { v.setBigUint64(0, s); return new Uint8Array(b); } - -function talerTimestampStringToBuffer(ts: string): Uint8Array { - const t_sec = getTalerStampSec(ts); - if (t_sec === null || t_sec === undefined) { - // Should have been validated before! - throw Error("invalid timestamp"); - } - const buffer = new ArrayBuffer(8); - const dvbuf = new DataView(buffer); - const s = BigInt(t_sec) * BigInt(1000 * 1000); - dvbuf.setBigUint64(0, s); - return new Uint8Array(buffer); -} - class SignaturePurposeBuilder { private chunks: Uint8Array[] = []; @@ -346,7 +332,8 @@ export class CryptoImplementation { * and deposit permissions for each given coin. */ signDeposit( - contractTerms: ContractTerms, + contractTermsRaw: string, + contractData: WalletContractData, cds: CoinWithDenom[], totalAmount: AmountJson, ): PaySigInfo { @@ -354,14 +341,13 @@ export class CryptoImplementation { coinInfo: [], }; - const contractTermsHash = this.hashString(canonicalJson(contractTerms)); + const contractTermsHash = this.hashString(canonicalJson(JSON.parse(contractTermsRaw))); 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, Amounts.parseOrThrow(contractTerms.max_fee)) - .amount; + fees = Amounts.sub(fees, contractData.maxDepositFee).amount; const total = Amounts.add(fees, totalAmount).amount; let amountSpent = Amounts.getZero(cds[0].coin.currentAmount.currency); @@ -395,12 +381,12 @@ export class CryptoImplementation { const d = buildSigPS(SignaturePurpose.WALLET_COIN_DEPOSIT) .put(decodeCrock(contractTermsHash)) - .put(decodeCrock(contractTerms.H_wire)) - .put(talerTimestampStringToBuffer(contractTerms.timestamp)) - .put(talerTimestampStringToBuffer(contractTerms.refund_deadline)) + .put(decodeCrock(contractData.wireInfoHash)) + .put(timestampToBuffer(contractData.timestamp)) + .put(timestampToBuffer(contractData.refundDeadline)) .put(amountToBuffer(coinSpend)) .put(amountToBuffer(cd.denom.feeDeposit)) - .put(decodeCrock(contractTerms.merchant_pub)) + .put(decodeCrock(contractData.merchantPub)) .put(decodeCrock(cd.coin.coinPub)) .build(); const coinSig = eddsaSign(d, decodeCrock(cd.coin.coinPriv)); diff --git a/src/headless/merchant.ts b/src/headless/merchant.ts index 6a2d0ad2e..1da5d5f03 100644 --- a/src/headless/merchant.ts +++ b/src/headless/merchant.ts @@ -23,7 +23,7 @@ * Imports. */ import axios from "axios"; -import { CheckPaymentResponse } from "../types/talerTypes"; +import { CheckPaymentResponse, codecForCheckPaymentResponse } from "../types/talerTypes"; /** * Connection to the *internal* merchant backend. @@ -96,8 +96,8 @@ export class MerchantBackendConnection { amount, summary, fulfillment_url: fulfillmentUrl, - refund_deadline: `/Date(${t})/`, - wire_transfer_deadline: `/Date(${t})/`, + refund_deadline: { t_ms: t * 1000 }, + wire_transfer_deadline: { t_ms: t * 1000 }, }, }; const resp = await axios({ @@ -133,6 +133,7 @@ export class MerchantBackendConnection { if (resp.status != 200) { throw Error("failed to check payment"); } - return CheckPaymentResponse.checked(resp.data); + + return codecForCheckPaymentResponse().decode(resp.data); } } diff --git a/src/headless/taler-wallet-cli.ts b/src/headless/taler-wallet-cli.ts index 610990ae4..12f729be4 100644 --- a/src/headless/taler-wallet-cli.ts +++ b/src/headless/taler-wallet-cli.ts @@ -50,7 +50,7 @@ async function doPay( return; } if (result.status === "insufficient-balance") { - console.log("contract", result.contractTerms!); + console.log("contract", result.contractTermsRaw); console.error("insufficient balance"); process.exit(1); return; @@ -65,7 +65,7 @@ async function doPay( } else { throw Error("not reached"); } - console.log("contract", result.contractTerms!); + console.log("contract", result.contractTermsRaw); let pay; if (options.alwaysYes) { pay = true; diff --git a/src/operations/exchanges.ts b/src/operations/exchanges.ts index d9adc7c52..741be31ba 100644 --- a/src/operations/exchanges.ts +++ b/src/operations/exchanges.ts @@ -15,8 +15,8 @@ */ import { InternalWalletState } from "./state"; -import { KeysJson, Denomination, ExchangeWireJson } from "../types/talerTypes"; -import { getTimestampNow, OperationError } from "../types/walletTypes"; +import { ExchangeKeysJson, Denomination, ExchangeWireJson, codecForExchangeKeysJson, codecForExchangeWireJson } from "../types/talerTypes"; +import { OperationError } from "../types/walletTypes"; import { ExchangeRecord, ExchangeUpdateStatus, @@ -29,8 +29,6 @@ import { } from "../types/dbTypes"; import { canonicalizeBaseUrl, - extractTalerStamp, - extractTalerStampOrThrow, } from "../util/helpers"; import { Database } from "../util/query"; import * as Amounts from "../util/amounts"; @@ -40,6 +38,7 @@ import { guardOperationException, } from "./errors"; import { WALLET_CACHE_BREAKER_CLIENT_VERSION } from "./versions"; +import { getTimestampNow } from "../util/time"; async function denominationRecordFromKeys( ws: InternalWalletState, @@ -57,12 +56,10 @@ async function denominationRecordFromKeys( feeWithdraw: Amounts.parseOrThrow(denomIn.fee_withdraw), isOffered: true, masterSig: denomIn.master_sig, - stampExpireDeposit: extractTalerStampOrThrow(denomIn.stamp_expire_deposit), - stampExpireLegal: extractTalerStampOrThrow(denomIn.stamp_expire_legal), - stampExpireWithdraw: extractTalerStampOrThrow( - denomIn.stamp_expire_withdraw, - ), - stampStart: extractTalerStampOrThrow(denomIn.stamp_start), + stampExpireDeposit: denomIn.stamp_expire_deposit, + stampExpireLegal: denomIn.stamp_expire_legal, + stampExpireWithdraw: denomIn.stamp_expire_withdraw, + stampStart: denomIn.stamp_start, status: DenominationStatus.Unverified, value: Amounts.parseOrThrow(denomIn.value), }; @@ -117,9 +114,9 @@ async function updateExchangeWithKeys( }); throw new OperationFailedAndReportedError(m); } - let exchangeKeysJson: KeysJson; + let exchangeKeysJson: ExchangeKeysJson; try { - exchangeKeysJson = KeysJson.checked(keysResp); + exchangeKeysJson = codecForExchangeKeysJson().decode(keysResp); } catch (e) { const m = `Parsing /keys response failed: ${e.message}`; await setExchangeError(ws, baseUrl, { @@ -130,9 +127,7 @@ async function updateExchangeWithKeys( throw new OperationFailedAndReportedError(m); } - const lastUpdateTimestamp = extractTalerStamp( - exchangeKeysJson.list_issue_date, - ); + const lastUpdateTimestamp = exchangeKeysJson.list_issue_date if (!lastUpdateTimestamp) { const m = `Parsing /keys response failed: invalid list_issue_date.`; await setExchangeError(ws, baseUrl, { @@ -329,7 +324,7 @@ async function updateExchangeWithWireInfo( if (!wiJson) { throw Error("/wire response malformed"); } - const wireInfo = ExchangeWireJson.checked(wiJson); + const wireInfo = codecForExchangeWireJson().decode(wiJson); for (const a of wireInfo.accounts) { console.log("validating exchange acct"); const isValid = await ws.cryptoApi.isValidWireAccount( @@ -345,14 +340,8 @@ async function updateExchangeWithWireInfo( for (const wireMethod of Object.keys(wireInfo.fees)) { const feeList: WireFee[] = []; for (const x of wireInfo.fees[wireMethod]) { - const startStamp = extractTalerStamp(x.start_date); - if (!startStamp) { - throw Error("wrong date format"); - } - const endStamp = extractTalerStamp(x.end_date); - if (!endStamp) { - throw Error("wrong date format"); - } + const startStamp = x.start_date; + const endStamp = x.end_date; const fee: WireFee = { closingFee: Amounts.parseOrThrow(x.closing_fee), endStamp, diff --git a/src/operations/history.ts b/src/operations/history.ts index bb57a9c60..f02894b6b 100644 --- a/src/operations/history.ts +++ b/src/operations/history.ts @@ -37,6 +37,7 @@ import { import { assertUnreachable } from "../util/assertUnreachable"; import { TransactionHandle, Store } from "../util/query"; import { ReserveTransactionType } from "../types/ReserveTransaction"; +import { timestampCmp } from "../util/time"; /** * Create an event ID from the type and the primary key for the event. @@ -53,11 +54,11 @@ function getOrderShortInfo( return undefined; } return { - amount: download.contractTerms.amount, - orderId: download.contractTerms.order_id, - merchantBaseUrl: download.contractTerms.merchant_base_url, + amount: Amounts.toString(download.contractData.amount), + orderId: download.contractData.orderId, + merchantBaseUrl: download.contractData.merchantBaseUrl, proposalId: proposal.proposalId, - summary: download.contractTerms.summary || "", + summary: download.contractData.summary, }; } @@ -356,9 +357,7 @@ export async function getHistory( if (!orderShortInfo) { return; } - const purchaseAmount = Amounts.parseOrThrow( - purchase.contractTerms.amount, - ); + const purchaseAmount = purchase.contractData.amount; let amountRefundedRaw = Amounts.getZero(purchaseAmount.currency); let amountRefundedInvalid = Amounts.getZero(purchaseAmount.currency); let amountRefundedEffective = Amounts.getZero(purchaseAmount.currency); @@ -408,7 +407,7 @@ export async function getHistory( }, ); - history.sort((h1, h2) => Math.sign(h1.timestamp.t_ms - h2.timestamp.t_ms)); + history.sort((h1, h2) => timestampCmp(h1.timestamp, h2.timestamp)); return { history }; } diff --git a/src/operations/pay.ts b/src/operations/pay.ts index adbf6bb87..db2a24310 100644 --- a/src/operations/pay.ts +++ b/src/operations/pay.ts @@ -37,6 +37,7 @@ import { Stores, updateRetryInfoTimeout, PayEventRecord, + WalletContractData, } from "../types/dbTypes"; import { NotificationType } from "../types/notifications"; import { @@ -46,33 +47,29 @@ import { MerchantRefundResponse, PayReq, Proposal, + codecForMerchantRefundResponse, + codecForProposal, + codecForContractTerms, } from "../types/talerTypes"; import { CoinSelectionResult, CoinWithDenom, ConfirmPayResult, - getTimestampNow, OperationError, PaySigInfo, PreparePayResult, RefreshReason, - Timestamp, } from "../types/walletTypes"; import * as Amounts from "../util/amounts"; import { AmountJson } from "../util/amounts"; -import { - amountToPretty, - canonicalJson, - extractTalerDuration, - extractTalerStampOrThrow, - strcmp, -} from "../util/helpers"; +import { amountToPretty, canonicalJson, strcmp } from "../util/helpers"; import { Logger } from "../util/logging"; import { getOrderDownloadUrl, parsePayUri } from "../util/taleruri"; import { guardOperationException } from "./errors"; import { createRefreshGroup, getTotalRefreshCost } from "./refresh"; import { acceptRefundResponse } from "./refund"; import { InternalWalletState } from "./state"; +import { Timestamp, getTimestampNow, timestampAddDuration } from "../util/time"; interface CoinsForPaymentArgs { allowedAuditors: Auditor[]; @@ -177,20 +174,20 @@ export function selectPayCoins( */ async function getCoinsForPayment( ws: InternalWalletState, - args: CoinsForPaymentArgs, + args: WalletContractData, ): Promise<CoinSelectionResult | undefined> { const { allowedAuditors, allowedExchanges, - depositFeeLimit, - paymentAmount, + maxDepositFee, + amount, wireFeeAmortization, - wireFeeLimit, - wireFeeTime, + maxWireFee, + timestamp, wireMethod, } = args; - let remainingAmount = paymentAmount; + let remainingAmount = amount; const exchanges = await ws.db.iter(Stores.exchanges).toArray(); @@ -207,7 +204,7 @@ async function getCoinsForPayment( // is the exchange explicitly allowed? for (const allowedExchange of allowedExchanges) { - if (allowedExchange.master_pub === exchangeDetails.masterPublicKey) { + if (allowedExchange.exchangePub === exchangeDetails.masterPublicKey) { isOkay = true; break; } @@ -217,7 +214,7 @@ async function getCoinsForPayment( if (!isOkay) { for (const allowedAuditor of allowedAuditors) { for (const auditor of exchangeDetails.auditors) { - if (auditor.auditor_pub === allowedAuditor.auditor_pub) { + if (auditor.auditor_pub === allowedAuditor.auditorPub) { isOkay = true; break; } @@ -281,7 +278,7 @@ async function getCoinsForPayment( let totalFees = Amounts.getZero(currency); let wireFee: AmountJson | undefined; for (const fee of exchangeFees.feesForType[wireMethod] || []) { - if (fee.startStamp <= wireFeeTime && fee.endStamp >= wireFeeTime) { + if (fee.startStamp <= timestamp && fee.endStamp >= timestamp) { wireFee = fee.wireFee; break; } @@ -289,13 +286,13 @@ async function getCoinsForPayment( if (wireFee) { const amortizedWireFee = Amounts.divide(wireFee, wireFeeAmortization); - if (Amounts.cmp(wireFeeLimit, amortizedWireFee) < 0) { + if (Amounts.cmp(maxWireFee, amortizedWireFee) < 0) { totalFees = Amounts.add(amortizedWireFee, totalFees).amount; remainingAmount = Amounts.add(amortizedWireFee, remainingAmount).amount; } } - const res = selectPayCoins(denoms, cds, remainingAmount, depositFeeLimit); + const res = selectPayCoins(denoms, cds, remainingAmount, maxDepositFee); if (res) { totalFees = Amounts.add(totalFees, res.totalFees).amount; @@ -332,18 +329,17 @@ async function recordConfirmPay( } logger.trace(`recording payment with session ID ${sessionId}`); const payReq: PayReq = { - coins: payCoinInfo.coinInfo.map((x) => x.sig), - merchant_pub: d.contractTerms.merchant_pub, + coins: payCoinInfo.coinInfo.map(x => x.sig), + merchant_pub: d.contractData.merchantPub, mode: "pay", - order_id: d.contractTerms.order_id, + order_id: d.contractData.orderId, }; const t: PurchaseRecord = { abortDone: false, abortRequested: false, - contractTerms: d.contractTerms, - contractTermsHash: d.contractTermsHash, + contractTermsRaw: d.contractTermsRaw, + contractData: d.contractData, lastSessionId: sessionId, - merchantSig: d.merchantSig, payReq, timestampAccept: getTimestampNow(), timestampLastRefundStatus: undefined, @@ -383,14 +379,19 @@ async function recordConfirmPay( throw Error("coin allocated for payment doesn't exist anymore"); } coin.status = CoinStatus.Dormant; - const remaining = Amounts.sub(coin.currentAmount, coinInfo.subtractedAmount); + const remaining = Amounts.sub( + coin.currentAmount, + coinInfo.subtractedAmount, + ); if (remaining.saturated) { throw Error("not enough remaining balance on coin for payment"); } coin.currentAmount = remaining.amount; await tx.put(Stores.coins, coin); } - const refreshCoinPubs = payCoinInfo.coinInfo.map((x) => ({coinPub: x.coinPub})); + const refreshCoinPubs = payCoinInfo.coinInfo.map(x => ({ + coinPub: x.coinPub, + })); await createRefreshGroup(tx, refreshCoinPubs, RefreshReason.Pay); }, ); @@ -402,11 +403,11 @@ async function recordConfirmPay( return t; } -function getNextUrl(contractTerms: ContractTerms): string { - const f = contractTerms.fulfillment_url; +function getNextUrl(contractData: WalletContractData): string { + const f = contractData.fulfillmentUrl; if (f.startsWith("http://") || f.startsWith("https://")) { - const fu = new URL(contractTerms.fulfillment_url); - fu.searchParams.set("order_id", contractTerms.order_id); + const fu = new URL(contractData.fulfillmentUrl); + fu.searchParams.set("order_id", contractData.orderId); return fu.href; } else { return f; @@ -440,7 +441,7 @@ export async function abortFailedPayment( const abortReq = { ...purchase.payReq, mode: "abort-refund" }; - const payUrl = new URL("pay", purchase.contractTerms.merchant_base_url).href; + const payUrl = new URL("pay", purchase.contractData.merchantBaseUrl).href; try { resp = await ws.http.postJson(payUrl, abortReq); @@ -454,7 +455,9 @@ export async function abortFailedPayment( throw Error(`unexpected status for /pay (${resp.status})`); } - const refundResponse = MerchantRefundResponse.checked(await resp.json()); + const refundResponse = codecForMerchantRefundResponse().decode( + await resp.json(), + ); await acceptRefundResponse( ws, purchase.proposalId, @@ -574,13 +577,16 @@ async function processDownloadProposalImpl( throw Error(`contract download failed with status ${resp.status}`); } - const proposalResp = Proposal.checked(await resp.json()); + const proposalResp = codecForProposal().decode(await resp.json()); const contractTermsHash = await ws.cryptoApi.hashString( canonicalJson(proposalResp.contract_terms), ); - const fulfillmentUrl = proposalResp.contract_terms.fulfillment_url; + const parsedContractTerms = codecForContractTerms().decode( + proposalResp.contract_terms, + ); + const fulfillmentUrl = parsedContractTerms.fulfillment_url; await ws.db.runWithWriteTransaction( [Stores.proposals, Stores.purchases], @@ -592,10 +598,42 @@ async function processDownloadProposalImpl( if (p.proposalStatus !== ProposalStatus.DOWNLOADING) { return; } + const amount = Amounts.parseOrThrow(parsedContractTerms.amount); + let maxWireFee: AmountJson; + if (parsedContractTerms.max_wire_fee) { + maxWireFee = Amounts.parseOrThrow(parsedContractTerms.max_wire_fee); + } else { + maxWireFee = Amounts.getZero(amount.currency); + } p.download = { - contractTerms: proposalResp.contract_terms, - merchantSig: proposalResp.sig, - contractTermsHash, + contractData: { + amount, + contractTermsHash: contractTermsHash, + fulfillmentUrl: parsedContractTerms.fulfillment_url, + merchantBaseUrl: parsedContractTerms.merchant_base_url, + merchantPub: parsedContractTerms.merchant_pub, + merchantSig: proposalResp.sig, + orderId: parsedContractTerms.order_id, + summary: parsedContractTerms.summary, + autoRefund: parsedContractTerms.auto_refund, + maxWireFee, + payDeadline: parsedContractTerms.pay_deadline, + refundDeadline: parsedContractTerms.refund_deadline, + wireFeeAmortization: parsedContractTerms.wire_fee_amortization || 1, + allowedAuditors: parsedContractTerms.auditors.map(x => ({ + auditorBaseUrl: x.url, + auditorPub: x.master_pub, + })), + allowedExchanges: parsedContractTerms.exchanges.map(x => ({ + exchangeBaseUrl: x.url, + exchangePub: x.master_pub, + })), + timestamp: parsedContractTerms.timestamp, + wireMethod: parsedContractTerms.wire_method, + wireInfoHash: parsedContractTerms.H_wire, + maxDepositFee: Amounts.parseOrThrow(parsedContractTerms.max_fee), + }, + contractTermsRaw: JSON.stringify(proposalResp.contract_terms), }; if ( fulfillmentUrl.startsWith("http://") || @@ -697,7 +735,7 @@ export async function submitPay( console.log("paying with session ID", sessionId); - const payUrl = new URL("pay", purchase.contractTerms.merchant_base_url).href; + const payUrl = new URL("pay", purchase.contractData.merchantBaseUrl).href; try { resp = await ws.http.postJson(payUrl, payReq); @@ -714,10 +752,10 @@ export async function submitPay( const now = getTimestampNow(); - const merchantPub = purchase.contractTerms.merchant_pub; + const merchantPub = purchase.contractData.merchantPub; const valid: boolean = await ws.cryptoApi.isValidPaymentSignature( merchantResp.sig, - purchase.contractTermsHash, + purchase.contractData.contractTermsHash, merchantPub, ); if (!valid) { @@ -731,19 +769,13 @@ export async function submitPay( purchase.lastPayError = undefined; purchase.payRetryInfo = initRetryInfo(false); if (isFirst) { - const ar = purchase.contractTerms.auto_refund; + const ar = purchase.contractData.autoRefund; if (ar) { console.log("auto_refund present"); - const autoRefundDelay = extractTalerDuration(ar); - console.log("auto_refund valid", autoRefundDelay); - if (autoRefundDelay) { - purchase.refundStatusRequested = true; - purchase.refundStatusRetryInfo = initRetryInfo(); - purchase.lastRefundStatusError = undefined; - purchase.autoRefundDeadline = { - t_ms: now.t_ms + autoRefundDelay.d_ms, - }; - } + purchase.refundStatusRequested = true; + purchase.refundStatusRetryInfo = initRetryInfo(); + purchase.lastRefundStatusError = undefined; + purchase.autoRefundDeadline = timestampAddDuration(now, ar); } } @@ -761,8 +793,8 @@ export async function submitPay( }, ); - const nextUrl = getNextUrl(purchase.contractTerms); - ws.cachedNextUrl[purchase.contractTerms.fulfillment_url] = { + const nextUrl = getNextUrl(purchase.contractData); + ws.cachedNextUrl[purchase.contractData.fulfillmentUrl] = { nextUrl, lastSessionId: sessionId, }; @@ -816,9 +848,9 @@ export async function preparePay( console.error("bad proposal", proposal); throw Error("proposal is in invalid state"); } - const contractTerms = d.contractTerms; - const merchantSig = d.merchantSig; - if (!contractTerms || !merchantSig) { + const contractData = d.contractData; + const merchantSig = d.contractData.merchantSig; + if (!merchantSig) { throw Error("BUG: proposal is in invalid state"); } @@ -828,45 +860,31 @@ export async function preparePay( const purchase = await ws.db.get(Stores.purchases, proposalId); if (!purchase) { - const paymentAmount = Amounts.parseOrThrow(contractTerms.amount); - let wireFeeLimit; - if (contractTerms.max_wire_fee) { - wireFeeLimit = Amounts.parseOrThrow(contractTerms.max_wire_fee); - } else { - wireFeeLimit = Amounts.getZero(paymentAmount.currency); - } - // If not already payed, check if we could pay for it. - const res = await getCoinsForPayment(ws, { - allowedAuditors: contractTerms.auditors, - allowedExchanges: contractTerms.exchanges, - depositFeeLimit: Amounts.parseOrThrow(contractTerms.max_fee), - paymentAmount, - wireFeeAmortization: contractTerms.wire_fee_amortization || 1, - wireFeeLimit, - wireFeeTime: extractTalerStampOrThrow(contractTerms.timestamp), - wireMethod: contractTerms.wire_method, - }); + // If not already paid, check if we could pay for it. + const res = await getCoinsForPayment(ws, contractData); if (!res) { console.log("not confirming payment, insufficient coins"); return { status: "insufficient-balance", - contractTerms: contractTerms, + contractTermsRaw: d.contractTermsRaw, proposalId: proposal.proposalId, }; } return { status: "payment-possible", - contractTerms: contractTerms, + contractTermsRaw: d.contractTermsRaw, proposalId: proposal.proposalId, totalFees: res.totalFees, }; } if (uriResult.sessionId && purchase.lastSessionId !== uriResult.sessionId) { - console.log("automatically re-submitting payment with different session ID") - await ws.db.runWithWriteTransaction([Stores.purchases], async (tx) => { + console.log( + "automatically re-submitting payment with different session ID", + ); + await ws.db.runWithWriteTransaction([Stores.purchases], async tx => { const p = await tx.get(Stores.purchases, proposalId); if (!p) { return; @@ -879,8 +897,8 @@ export async function preparePay( return { status: "paid", - contractTerms: purchase.contractTerms, - nextUrl: getNextUrl(purchase.contractTerms), + contractTermsRaw: purchase.contractTermsRaw, + nextUrl: getNextUrl(purchase.contractData), }; } @@ -906,7 +924,10 @@ export async function confirmPay( throw Error("proposal is in invalid state"); } - let purchase = await ws.db.get(Stores.purchases, d.contractTermsHash); + let purchase = await ws.db.get( + Stores.purchases, + d.contractData.contractTermsHash, + ); if (purchase) { if ( @@ -926,25 +947,7 @@ export async function confirmPay( logger.trace("confirmPay: purchase record does not exist yet"); - const contractAmount = Amounts.parseOrThrow(d.contractTerms.amount); - - let wireFeeLimit; - if (!d.contractTerms.max_wire_fee) { - wireFeeLimit = Amounts.getZero(contractAmount.currency); - } else { - wireFeeLimit = Amounts.parseOrThrow(d.contractTerms.max_wire_fee); - } - - const res = await getCoinsForPayment(ws, { - allowedAuditors: d.contractTerms.auditors, - allowedExchanges: d.contractTerms.exchanges, - depositFeeLimit: Amounts.parseOrThrow(d.contractTerms.max_fee), - paymentAmount: Amounts.parseOrThrow(d.contractTerms.amount), - wireFeeAmortization: d.contractTerms.wire_fee_amortization || 1, - wireFeeLimit, - wireFeeTime: extractTalerStampOrThrow(d.contractTerms.timestamp), - wireMethod: d.contractTerms.wire_method, - }); + const res = await getCoinsForPayment(ws, d.contractData); logger.trace("coin selection result", res); @@ -956,7 +959,8 @@ export async function confirmPay( const { cds, totalAmount } = res; const payCoinInfo = await ws.cryptoApi.signDeposit( - d.contractTerms, + d.contractTermsRaw, + d.contractData, cds, totalAmount, ); @@ -964,7 +968,7 @@ export async function confirmPay( ws, proposal, payCoinInfo, - sessionIdOverride + sessionIdOverride, ); logger.trace("confirmPay: submitting payment after creating purchase record"); diff --git a/src/operations/payback.ts b/src/operations/payback.ts index 51adb6ad3..181527693 100644 --- a/src/operations/payback.ts +++ b/src/operations/payback.ts @@ -24,7 +24,7 @@ import { InternalWalletState } from "./state"; import { Stores, TipRecord, CoinStatus } from "../types/dbTypes"; import { Logger } from "../util/logging"; -import { PaybackConfirmation } from "../types/talerTypes"; +import { RecoupConfirmation, codecForRecoupConfirmation } from "../types/talerTypes"; import { updateExchangeFromUrl } from "./exchanges"; import { NotificationType } from "../types/notifications"; @@ -72,7 +72,7 @@ export async function payback( if (resp.status !== 200) { throw Error(); } - const paybackConfirmation = PaybackConfirmation.checked(await resp.json()); + const paybackConfirmation = codecForRecoupConfirmation().decode(await resp.json()); if (paybackConfirmation.reserve_pub !== coin.reservePub) { throw Error(`Coin's reserve doesn't match reserve on payback`); } diff --git a/src/operations/pending.ts b/src/operations/pending.ts index 360180854..ed3b59d71 100644 --- a/src/operations/pending.ts +++ b/src/operations/pending.ts @@ -27,7 +27,7 @@ import { PendingOperationsResponse, PendingOperationType, } from "../types/pending"; -import { Duration, getTimestampNow, Timestamp } from "../types/walletTypes"; +import { Duration, getTimestampNow, Timestamp, getDurationRemaining, durationMin } from "../util/time"; import { TransactionHandle } from "../util/query"; import { InternalWalletState } from "./state"; @@ -36,10 +36,8 @@ function updateRetryDelay( now: Timestamp, retryTimestamp: Timestamp, ): Duration { - if (retryTimestamp.t_ms <= now.t_ms) { - return { d_ms: 0 }; - } - return { d_ms: Math.min(oldDelay.d_ms, retryTimestamp.t_ms - now.t_ms) }; + const remaining = getDurationRemaining(retryTimestamp, now); + return durationMin(oldDelay, remaining); } async function gatherExchangePending( @@ -278,7 +276,7 @@ async function gatherProposalPending( resp.pendingOperations.push({ type: PendingOperationType.ProposalChoice, givesLifeness: false, - merchantBaseUrl: proposal.download!!.contractTerms.merchant_base_url, + merchantBaseUrl: proposal.download!!.contractData.merchantBaseUrl, proposalId: proposal.proposalId, proposalTimestamp: proposal.timestamp, }); diff --git a/src/operations/refresh.ts b/src/operations/refresh.ts index 8390cac54..87d81cb2d 100644 --- a/src/operations/refresh.ts +++ b/src/operations/refresh.ts @@ -34,7 +34,6 @@ import { Logger } from "../util/logging"; import { getWithdrawDenomList } from "./withdraw"; import { updateExchangeFromUrl } from "./exchanges"; import { - getTimestampNow, OperationError, CoinPublicKey, RefreshReason, @@ -43,6 +42,7 @@ import { import { guardOperationException } from "./errors"; import { NotificationType } from "../types/notifications"; import { getRandomBytes, encodeCrock } from "../crypto/talerCrypto"; +import { getTimestampNow } from "../util/time"; const logger = new Logger("refresh.ts"); diff --git a/src/operations/refund.ts b/src/operations/refund.ts index b4139c991..6a96868a3 100644 --- a/src/operations/refund.ts +++ b/src/operations/refund.ts @@ -26,7 +26,6 @@ import { InternalWalletState } from "./state"; import { OperationError, - getTimestampNow, RefreshReason, CoinPublicKey, } from "../types/walletTypes"; @@ -47,12 +46,14 @@ import { MerchantRefundPermission, MerchantRefundResponse, RefundRequest, + codecForMerchantRefundResponse, } from "../types/talerTypes"; import { AmountJson } from "../util/amounts"; import { guardOperationException, OperationFailedError } from "./errors"; import { randomBytes } from "../crypto/primitives/nacl-fast"; import { encodeCrock } from "../crypto/talerCrypto"; import { HttpResponseStatus } from "../util/http"; +import { getTimestampNow } from "../util/time"; async function incrementPurchaseQueryRefundRetry( ws: InternalWalletState, @@ -288,7 +289,7 @@ export async function applyRefund( console.log("processing purchase for refund"); await startRefundQuery(ws, purchase.proposalId); - return purchase.contractTermsHash; + return purchase.contractData.contractTermsHash; } export async function processPurchaseQueryRefund( @@ -334,9 +335,9 @@ async function processPurchaseQueryRefundImpl( const refundUrlObj = new URL( "refund", - purchase.contractTerms.merchant_base_url, + purchase.contractData.merchantBaseUrl, ); - refundUrlObj.searchParams.set("order_id", purchase.contractTerms.order_id); + refundUrlObj.searchParams.set("order_id", purchase.contractData.orderId); const refundUrl = refundUrlObj.href; let resp; try { @@ -349,7 +350,7 @@ async function processPurchaseQueryRefundImpl( throw Error(`unexpected status code (${resp.status}) for /refund`); } - const refundResponse = MerchantRefundResponse.checked(await resp.json()); + const refundResponse = codecForMerchantRefundResponse().decode(await resp.json()); await acceptRefundResponse( ws, proposalId, @@ -409,8 +410,8 @@ async function processPurchaseApplyRefundImpl( const perm = info.perm; const req: RefundRequest = { coin_pub: perm.coin_pub, - h_contract_terms: purchase.contractTermsHash, - merchant_pub: purchase.contractTerms.merchant_pub, + h_contract_terms: purchase.contractData.contractTermsHash, + merchant_pub: purchase.contractData.merchantPub, merchant_sig: perm.merchant_sig, refund_amount: perm.refund_amount, refund_fee: perm.refund_fee, diff --git a/src/operations/reserves.ts b/src/operations/reserves.ts index 7be927824..2dedf17de 100644 --- a/src/operations/reserves.ts +++ b/src/operations/reserves.ts @@ -17,7 +17,6 @@ import { CreateReserveRequest, CreateReserveResponse, - getTimestampNow, ConfirmReserveRequest, OperationError, AcceptWithdrawalResponse, @@ -42,7 +41,7 @@ import { getExchangeTrust, getExchangePaytoUri, } from "./exchanges"; -import { WithdrawOperationStatusResponse } from "../types/talerTypes"; +import { WithdrawOperationStatusResponse, codecForWithdrawOperationStatusResponse } from "../types/talerTypes"; import { assertUnreachable } from "../util/assertUnreachable"; import { encodeCrock, getRandomBytes } from "../crypto/talerCrypto"; import { randomBytes } from "../crypto/primitives/nacl-fast"; @@ -57,6 +56,7 @@ import { } from "./errors"; import { NotificationType } from "../types/notifications"; import { codecForReserveStatus } from "../types/ReserveStatus"; +import { getTimestampNow } from "../util/time"; const logger = new Logger("reserves.ts"); @@ -289,7 +289,7 @@ async function processReserveBankStatusImpl( `unexpected status ${statusResp.status} for bank status query`, ); } - status = WithdrawOperationStatusResponse.checked(await statusResp.json()); + status = codecForWithdrawOperationStatusResponse().decode(await statusResp.json()); } catch (e) { throw e; } @@ -390,6 +390,7 @@ async function updateReserve( let resp; try { resp = await ws.http.get(reqUrl.href); + console.log("got reserve/status response", await resp.json()); if (resp.status === 404) { const m = "The exchange does not know about this reserve (yet)."; await incrementReserveRetry(ws, reservePub, undefined); @@ -408,7 +409,7 @@ async function updateReserve( throw new OperationFailedAndReportedError(m); } const respJson = await resp.json(); - const reserveInfo = codecForReserveStatus.decode(respJson); + const reserveInfo = codecForReserveStatus().decode(respJson); const balance = Amounts.parseOrThrow(reserveInfo.balance); await ws.db.runWithWriteTransaction( [Stores.reserves, Stores.reserveUpdatedEvents], diff --git a/src/operations/return.ts b/src/operations/return.ts deleted file mode 100644 index 4238f6cd2..000000000 --- a/src/operations/return.ts +++ /dev/null @@ -1,271 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2019 GNUnet e.V. - - GNU Taler is free software; you can redistribute it and/or modify it under the - terms of the GNU General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> - */ - -/** - * Imports. - */ -import { - ReturnCoinsRequest, - CoinWithDenom, -} from "../types/walletTypes"; -import { Database } from "../util/query"; -import { InternalWalletState } from "./state"; -import { Stores, TipRecord, CoinStatus, CoinsReturnRecord, CoinRecord } from "../types/dbTypes"; -import * as Amounts from "../util/amounts"; -import { AmountJson } from "../util/amounts"; -import { Logger } from "../util/logging"; -import { canonicalJson } from "../util/helpers"; -import { ContractTerms } from "../types/talerTypes"; -import { selectPayCoins } from "./pay"; - -const logger = new Logger("return.ts"); - -async function getCoinsForReturn( - ws: InternalWalletState, - exchangeBaseUrl: string, - amount: AmountJson, -): Promise<CoinWithDenom[] | undefined> { - const exchange = await ws.db.get( - Stores.exchanges, - exchangeBaseUrl, - ); - if (!exchange) { - throw Error(`Exchange ${exchangeBaseUrl} not known to the wallet`); - } - - const coins: CoinRecord[] = await ws.db.iterIndex( - Stores.coins.exchangeBaseUrlIndex, - exchange.baseUrl, - ).toArray(); - - if (!coins || !coins.length) { - return []; - } - - const denoms = await ws.db.iterIndex( - Stores.denominations.exchangeBaseUrlIndex, - exchange.baseUrl, - ).toArray(); - - // Denomination of the first coin, we assume that all other - // coins have the same currency - const firstDenom = await ws.db.get(Stores.denominations, [ - exchange.baseUrl, - coins[0].denomPub, - ]); - if (!firstDenom) { - throw Error("db inconsistent"); - } - const currency = firstDenom.value.currency; - - const cds: CoinWithDenom[] = []; - for (const coin of coins) { - const denom = await ws.db.get(Stores.denominations, [ - exchange.baseUrl, - coin.denomPub, - ]); - if (!denom) { - throw Error("db inconsistent"); - } - if (denom.value.currency !== currency) { - console.warn( - `same pubkey for different currencies at exchange ${exchange.baseUrl}`, - ); - continue; - } - if (coin.suspended) { - continue; - } - if (coin.status !== CoinStatus.Fresh) { - continue; - } - cds.push({ coin, denom }); - } - - const res = selectPayCoins(denoms, cds, amount, amount); - if (res) { - return res.cds; - } - return undefined; -} - - -/** - * Trigger paying coins back into the user's account. - */ -export async function returnCoins( - ws: InternalWalletState, - req: ReturnCoinsRequest, -): Promise<void> { - logger.trace("got returnCoins request", req); - const wireType = (req.senderWire as any).type; - logger.trace("wireType", wireType); - if (!wireType || typeof wireType !== "string") { - console.error(`wire type must be a non-empty string, not ${wireType}`); - return; - } - const stampSecNow = Math.floor(new Date().getTime() / 1000); - const exchange = await ws.db.get(Stores.exchanges, req.exchange); - if (!exchange) { - console.error(`Exchange ${req.exchange} not known to the wallet`); - return; - } - const exchangeDetails = exchange.details; - if (!exchangeDetails) { - throw Error("exchange information needs to be updated first."); - } - logger.trace("selecting coins for return:", req); - const cds = await getCoinsForReturn(ws, req.exchange, req.amount); - logger.trace(cds); - - if (!cds) { - throw Error("coin return impossible, can't select coins"); - } - - const { priv, pub } = await ws.cryptoApi.createEddsaKeypair(); - - const wireHash = await ws.cryptoApi.hashString( - canonicalJson(req.senderWire), - ); - - const contractTerms: ContractTerms = { - H_wire: wireHash, - amount: Amounts.toString(req.amount), - auditors: [], - exchanges: [ - { master_pub: exchangeDetails.masterPublicKey, url: exchange.baseUrl }, - ], - extra: {}, - fulfillment_url: "", - locations: [], - max_fee: Amounts.toString(req.amount), - merchant: {}, - merchant_pub: pub, - order_id: "none", - pay_deadline: `/Date(${stampSecNow + 30 * 5})/`, - wire_transfer_deadline: `/Date(${stampSecNow + 60 * 5})/`, - merchant_base_url: "taler://return-to-account", - products: [], - refund_deadline: `/Date(${stampSecNow + 60 * 5})/`, - timestamp: `/Date(${stampSecNow})/`, - wire_method: wireType, - }; - - const contractTermsHash = await ws.cryptoApi.hashString( - canonicalJson(contractTerms), - ); - - const payCoinInfo = await ws.cryptoApi.signDeposit( - contractTerms, - cds, - Amounts.parseOrThrow(contractTerms.amount), - ); - - logger.trace("pci", payCoinInfo); - - const coins = payCoinInfo.coinInfo.map(s => ({ coinPaySig: s.sig })); - - const coinsReturnRecord: CoinsReturnRecord = { - coins, - contractTerms, - contractTermsHash, - exchange: exchange.baseUrl, - merchantPriv: priv, - wire: req.senderWire, - }; - - await ws.db.runWithWriteTransaction( - [Stores.coinsReturns, Stores.coins], - async tx => { - await tx.put(Stores.coinsReturns, coinsReturnRecord); - for (let coinInfo of payCoinInfo.coinInfo) { - const coin = await tx.get(Stores.coins, coinInfo.coinPub); - if (!coin) { - throw Error("coin allocated for deposit not in database anymore"); - } - const remaining = Amounts.sub(coin.currentAmount, coinInfo.subtractedAmount); - if (remaining.saturated) { - throw Error("coin allocated for deposit does not have enough balance"); - } - coin.currentAmount = remaining.amount; - await tx.put(Stores.coins, coin); - } - }, - ); - - depositReturnedCoins(ws, coinsReturnRecord); -} - -async function depositReturnedCoins( - ws: InternalWalletState, - coinsReturnRecord: CoinsReturnRecord, -): Promise<void> { - for (const c of coinsReturnRecord.coins) { - if (c.depositedSig) { - continue; - } - const req = { - H_wire: coinsReturnRecord.contractTerms.H_wire, - coin_pub: c.coinPaySig.coin_pub, - coin_sig: c.coinPaySig.coin_sig, - contribution: c.coinPaySig.contribution, - denom_pub: c.coinPaySig.denom_pub, - h_contract_terms: coinsReturnRecord.contractTermsHash, - merchant_pub: coinsReturnRecord.contractTerms.merchant_pub, - pay_deadline: coinsReturnRecord.contractTerms.pay_deadline, - refund_deadline: coinsReturnRecord.contractTerms.refund_deadline, - timestamp: coinsReturnRecord.contractTerms.timestamp, - ub_sig: c.coinPaySig.ub_sig, - wire: coinsReturnRecord.wire, - wire_transfer_deadline: coinsReturnRecord.contractTerms.pay_deadline, - }; - logger.trace("req", req); - const reqUrl = new URL("deposit", coinsReturnRecord.exchange); - const resp = await ws.http.postJson(reqUrl.href, req); - if (resp.status !== 200) { - console.error("deposit failed due to status code", resp); - continue; - } - const respJson = await resp.json(); - if (respJson.status !== "DEPOSIT_OK") { - console.error("deposit failed", resp); - continue; - } - - if (!respJson.sig) { - console.error("invalid 'sig' field", resp); - continue; - } - - // FIXME: verify signature - - // For every successful deposit, we replace the old record with an updated one - const currentCrr = await ws.db.get( - Stores.coinsReturns, - coinsReturnRecord.contractTermsHash, - ); - if (!currentCrr) { - console.error("database inconsistent"); - continue; - } - for (const nc of currentCrr.coins) { - if (nc.coinPaySig.coin_pub === c.coinPaySig.coin_pub) { - nc.depositedSig = respJson.sig; - } - } - await ws.db.put(Stores.coinsReturns, currentCrr); - } -} diff --git a/src/operations/tip.ts b/src/operations/tip.ts index 76d0df22d..fcdda0763 100644 --- a/src/operations/tip.ts +++ b/src/operations/tip.ts @@ -18,13 +18,14 @@ import { InternalWalletState } from "./state"; import { parseTipUri } from "../util/taleruri"; import { TipStatus, - getTimestampNow, OperationError, } from "../types/walletTypes"; import { TipPickupGetResponse, TipPlanchetDetail, TipResponse, + codecForTipPickupGetResponse, + codecForTipResponse, } from "../types/talerTypes"; import * as Amounts from "../util/amounts"; import { @@ -39,11 +40,11 @@ import { getVerifiedWithdrawDenomList, processWithdrawSession, } from "./withdraw"; -import { getTalerStampSec, extractTalerStampOrThrow } from "../util/helpers"; import { updateExchangeFromUrl } from "./exchanges"; import { getRandomBytes, encodeCrock } from "../crypto/talerCrypto"; import { guardOperationException } from "./errors"; import { NotificationType } from "../types/notifications"; +import { getTimestampNow } from "../util/time"; export async function getTipStatus( ws: InternalWalletState, @@ -63,7 +64,7 @@ export async function getTipStatus( } const respJson = await merchantResp.json(); console.log("resp:", respJson); - const tipPickupStatus = TipPickupGetResponse.checked(respJson); + const tipPickupStatus = codecForTipPickupGetResponse().decode(respJson); console.log("status", tipPickupStatus); @@ -88,7 +89,7 @@ export async function getTipStatus( acceptedTimestamp: undefined, rejectedTimestamp: undefined, amount, - deadline: extractTalerStampOrThrow(tipPickupStatus.stamp_expire), + deadline: tipPickupStatus.stamp_expire, exchangeUrl: tipPickupStatus.exchange_url, merchantBaseUrl: res.merchantBaseUrl, nextUrl: undefined, @@ -115,8 +116,8 @@ export async function getTipStatus( nextUrl: tipPickupStatus.extra.next_url, merchantOrigin: res.merchantOrigin, merchantTipId: res.merchantTipId, - expirationTimestamp: getTalerStampSec(tipPickupStatus.stamp_expire)!, - timestamp: getTalerStampSec(tipPickupStatus.stamp_created)!, + expirationTimestamp: tipPickupStatus.stamp_expire, + timestamp: tipPickupStatus.stamp_created, totalFees: tipRecord.totalFees, tipId: tipRecord.tipId, }; @@ -240,7 +241,7 @@ async function processTipImpl( throw e; } - const response = TipResponse.checked(await merchantResp.json()); + const response = codecForTipResponse().decode(await merchantResp.json()); if (response.reserve_sigs.length !== tipRecord.planchets.length) { throw Error("number of tip responses does not match requested planchets"); diff --git a/src/operations/withdraw.ts b/src/operations/withdraw.ts index 38baffa8d..27156015d 100644 --- a/src/operations/withdraw.ts +++ b/src/operations/withdraw.ts @@ -27,33 +27,39 @@ import { } from "../types/dbTypes"; import * as Amounts from "../util/amounts"; import { - getTimestampNow, - AcceptWithdrawalResponse, BankWithdrawDetails, ExchangeWithdrawDetails, WithdrawDetails, OperationError, } from "../types/walletTypes"; -import { WithdrawOperationStatusResponse } from "../types/talerTypes"; +import { WithdrawOperationStatusResponse, codecForWithdrawOperationStatusResponse } from "../types/talerTypes"; import { InternalWalletState } from "./state"; import { parseWithdrawUri } from "../util/taleruri"; import { Logger } from "../util/logging"; -import { - updateExchangeFromUrl, - getExchangeTrust, -} from "./exchanges"; +import { updateExchangeFromUrl, getExchangeTrust } from "./exchanges"; import { WALLET_EXCHANGE_PROTOCOL_VERSION } from "./versions"; import * as LibtoolVersion from "../util/libtoolVersion"; import { guardOperationException } from "./errors"; import { NotificationType } from "../types/notifications"; +import { + getTimestampNow, + getDurationRemaining, + timestampCmp, + timestampSubtractDuraction, +} from "../util/time"; const logger = new Logger("withdraw.ts"); function isWithdrawableDenom(d: DenominationRecord) { const now = getTimestampNow(); - const started = now.t_ms >= d.stampStart.t_ms; - const stillOkay = d.stampExpireWithdraw.t_ms + 60 * 1000 > now.t_ms; + const started = timestampCmp(now, d.stampStart) >= 0; + const lastPossibleWithdraw = timestampSubtractDuraction( + d.stampExpireWithdraw, + { d_ms: 50 * 1000 }, + ); + const remaining = getDurationRemaining(lastPossibleWithdraw, now); + const stillOkay = remaining.d_ms !== 0; return started && stillOkay; } @@ -108,11 +114,14 @@ export async function getBankWithdrawalInfo( } const resp = await ws.http.get(uriResult.statusUrl); if (resp.status !== 200) { - throw Error(`unexpected status (${resp.status}) from bank for ${uriResult.statusUrl}`); + throw Error( + `unexpected status (${resp.status}) from bank for ${uriResult.statusUrl}`, + ); } const respJson = await resp.json(); console.log("resp:", respJson); - const status = WithdrawOperationStatusResponse.checked(respJson); + + const status = codecForWithdrawOperationStatusResponse().decode(respJson); return { amount: Amounts.parseOrThrow(status.amount), confirmTransferUrl: status.confirm_transfer_url, @@ -125,20 +134,18 @@ export async function getBankWithdrawalInfo( }; } - async function getPossibleDenoms( ws: InternalWalletState, exchangeBaseUrl: string, ): Promise<DenominationRecord[]> { - return await ws.db.iterIndex( - Stores.denominations.exchangeBaseUrlIndex, - exchangeBaseUrl, - ).filter(d => { - return ( - d.status === DenominationStatus.Unverified || - d.status === DenominationStatus.VerifiedGood - ); - }); + return await ws.db + .iterIndex(Stores.denominations.exchangeBaseUrlIndex, exchangeBaseUrl) + .filter(d => { + return ( + d.status === DenominationStatus.Unverified || + d.status === DenominationStatus.VerifiedGood + ); + }); } /** @@ -204,8 +211,11 @@ async function processPlanchet( planchet.denomPub, ); - - const isValid = await ws.cryptoApi.rsaVerify(planchet.coinPub, denomSig, planchet.denomPub); + const isValid = await ws.cryptoApi.rsaVerify( + planchet.coinPub, + denomSig, + planchet.denomPub, + ); if (!isValid) { throw Error("invalid RSA signature by the exchange"); } @@ -261,7 +271,10 @@ async function processPlanchet( r.amountWithdrawCompleted, Amounts.add(denom.value, denom.feeWithdraw).amount, ).amount; - if (Amounts.cmp(r.amountWithdrawCompleted, r.amountWithdrawAllocated) == 0) { + if ( + Amounts.cmp(r.amountWithdrawCompleted, r.amountWithdrawAllocated) == + 0 + ) { reserveDepleted = true; } await tx.put(Stores.reserves, r); @@ -273,9 +286,9 @@ async function processPlanchet( ); if (success) { - ws.notify( { + ws.notify({ type: NotificationType.CoinWithdrawn, - } ); + }); } if (withdrawSessionFinished) { @@ -436,10 +449,10 @@ async function processWithdrawCoin( return; } - const coin = await ws.db.getIndexed( - Stores.coins.byWithdrawalWithIdx, - [withdrawalSessionId, coinIndex], - ); + const coin = await ws.db.getIndexed(Stores.coins.byWithdrawalWithIdx, [ + withdrawalSessionId, + coinIndex, + ]); if (coin) { console.log("coin already exists"); @@ -494,7 +507,7 @@ async function resetWithdrawSessionRetry( ws: InternalWalletState, withdrawalSessionId: string, ) { - await ws.db.mutate(Stores.withdrawalSession, withdrawalSessionId, (x) => { + await ws.db.mutate(Stores.withdrawalSession, withdrawalSessionId, x => { if (x.retryInfo.active) { x.retryInfo = initRetryInfo(); } @@ -570,16 +583,12 @@ export async function getExchangeWithdrawalInfo( } } - const possibleDenoms = await ws.db.iterIndex( - Stores.denominations.exchangeBaseUrlIndex, - baseUrl, - ).filter(d => d.isOffered); + const possibleDenoms = await ws.db + .iterIndex(Stores.denominations.exchangeBaseUrlIndex, baseUrl) + .filter(d => d.isOffered); const trustedAuditorPubs = []; - const currencyRecord = await ws.db.get( - Stores.currencies, - amount.currency, - ); + const currencyRecord = await ws.db.get(Stores.currencies, amount.currency); if (currencyRecord) { trustedAuditorPubs.push(...currencyRecord.auditors.map(a => a.auditorPub)); } @@ -606,7 +615,10 @@ export async function getExchangeWithdrawalInfo( let tosAccepted = false; if (exchangeInfo.termsOfServiceAcceptedTimestamp) { - if (exchangeInfo.termsOfServiceAcceptedEtag == exchangeInfo.termsOfServiceLastEtag) { + if ( + exchangeInfo.termsOfServiceAcceptedEtag == + exchangeInfo.termsOfServiceLastEtag + ) { tosAccepted = true; } } diff --git a/src/types/ReserveStatus.ts b/src/types/ReserveStatus.ts index d9b5d9496..8ab7225e8 100644 --- a/src/types/ReserveStatus.ts +++ b/src/types/ReserveStatus.ts @@ -29,14 +29,15 @@ import { makeCodecForUnion, makeCodecForList, } from "../util/codec"; -import { runBlock } from "../util/helpers"; import { AmountString } from "./talerTypes"; -import { ReserveTransaction, codecForReserveTransaction } from "./ReserveTransaction"; - +import { + ReserveTransaction, + codecForReserveTransaction, +} from "./ReserveTransaction"; /** * Status of a reserve. - * + * * Schema type for the exchange's response to "/reserve/status". */ export interface ReserveStatus { @@ -51,11 +52,10 @@ export interface ReserveStatus { history: ReserveTransaction[]; } -export const codecForReserveStatus = runBlock(() => ( +export const codecForReserveStatus = () => typecheckedCodec<ReserveStatus>( makeCodecForObject<ReserveStatus>() .property("balance", codecForString) - .property("history", makeCodecForList(codecForReserveTransaction)) - .build("ReserveStatus") - ) -));
\ No newline at end of file + .property("history", makeCodecForList(codecForReserveTransaction())) + .build("ReserveStatus"), + ); diff --git a/src/types/ReserveTransaction.ts b/src/types/ReserveTransaction.ts index 2ec859498..e889f36a8 100644 --- a/src/types/ReserveTransaction.ts +++ b/src/types/ReserveTransaction.ts @@ -28,15 +28,14 @@ import { makeCodecForConstString, makeCodecForUnion, } from "../util/codec"; -import { runBlock } from "../util/helpers"; import { AmountString, Base32String, EddsaSignatureString, - TimestampString, EddsaPublicKeyString, CoinPublicKeyString, } from "./talerTypes"; +import { Timestamp, codecForTimestamp } from "../util/time"; export const enum ReserveTransactionType { Withdraw = "WITHDRAW", @@ -96,7 +95,7 @@ export interface ReserveDepositTransaction { /** * Timestamp of the incoming wire transfer. */ - timestamp: TimestampString; + timestamp: Timestamp; } export interface ReserveClosingTransaction { @@ -137,7 +136,7 @@ export interface ReserveClosingTransaction { /** * Time when the reserve was closed. */ - timestamp: TimestampString; + timestamp: Timestamp; } export interface ReservePaybackTransaction { @@ -173,7 +172,7 @@ export interface ReservePaybackTransaction { /** * Time when the funds were paid back into the reserve. */ - timestamp: TimestampString; + timestamp: Timestamp; /** * Public key of the coin that was paid back. @@ -190,7 +189,7 @@ export type ReserveTransaction = | ReserveClosingTransaction | ReservePaybackTransaction; -export const codecForReserveWithdrawTransaction = runBlock(() => +export const codecForReserveWithdrawTransaction = () => typecheckedCodec<ReserveWithdrawTransaction>( makeCodecForObject<ReserveWithdrawTransaction>() .property("amount", codecForString) @@ -203,22 +202,20 @@ export const codecForReserveWithdrawTransaction = runBlock(() => ) .property("withdraw_fee", codecForString) .build("ReserveWithdrawTransaction"), - ), -); + ); -export const codecForReserveDepositTransaction = runBlock(() => +export const codecForReserveDepositTransaction = () => typecheckedCodec<ReserveDepositTransaction>( makeCodecForObject<ReserveDepositTransaction>() .property("amount", codecForString) .property("sender_account_url", codecForString) - .property("timestamp", codecForString) + .property("timestamp", codecForTimestamp) .property("wire_reference", codecForString) .property("type", makeCodecForConstString(ReserveTransactionType.Deposit)) .build("ReserveDepositTransaction"), - ), -); + ); -export const codecForReserveClosingTransaction = runBlock(() => +export const codecForReserveClosingTransaction = () => typecheckedCodec<ReserveClosingTransaction>( makeCodecForObject<ReserveClosingTransaction>() .property("amount", codecForString) @@ -226,14 +223,13 @@ export const codecForReserveClosingTransaction = runBlock(() => .property("exchange_pub", codecForString) .property("exchange_sig", codecForString) .property("h_wire", codecForString) - .property("timestamp", codecForString) + .property("timestamp", codecForTimestamp) .property("type", makeCodecForConstString(ReserveTransactionType.Closing)) .property("wtid", codecForString) .build("ReserveClosingTransaction"), - ), -); + ); -export const codecForReservePaybackTransaction = runBlock(() => +export const codecForReservePaybackTransaction = () => typecheckedCodec<ReservePaybackTransaction>( makeCodecForObject<ReservePaybackTransaction>() .property("amount", codecForString) @@ -241,33 +237,31 @@ export const codecForReservePaybackTransaction = runBlock(() => .property("exchange_pub", codecForString) .property("exchange_sig", codecForString) .property("receiver_account_details", codecForString) - .property("timestamp", codecForString) + .property("timestamp", codecForTimestamp) .property("type", makeCodecForConstString(ReserveTransactionType.Payback)) .property("wire_transfer", codecForString) .build("ReservePaybackTransaction"), - ), -); + ); -export const codecForReserveTransaction = runBlock(() => +export const codecForReserveTransaction = () => typecheckedCodec<ReserveTransaction>( makeCodecForUnion<ReserveTransaction>() .discriminateOn("type") .alternative( ReserveTransactionType.Withdraw, - codecForReserveWithdrawTransaction, + codecForReserveWithdrawTransaction(), ) .alternative( ReserveTransactionType.Closing, - codecForReserveClosingTransaction, + codecForReserveClosingTransaction(), ) .alternative( ReserveTransactionType.Payback, - codecForReservePaybackTransaction, + codecForReservePaybackTransaction(), ) .alternative( ReserveTransactionType.Deposit, - codecForReserveDepositTransaction, + codecForReserveDepositTransaction(), ) .build<ReserveTransaction>("ReserveTransaction"), - ), -); + ); diff --git a/src/types/dbTypes.ts b/src/types/dbTypes.ts index f8f9880dd..55559ab57 100644 --- a/src/types/dbTypes.ts +++ b/src/types/dbTypes.ts @@ -24,7 +24,6 @@ * Imports. */ import { AmountJson } from "../util/amounts"; -import { Checkable } from "../util/checkable"; import { Auditor, CoinPaySig, @@ -33,17 +32,16 @@ import { MerchantRefundPermission, PayReq, TipResponse, + ExchangeHandle, } from "./talerTypes"; import { Index, Store } from "../util/query"; import { - Timestamp, OperationError, - Duration, - getTimestampNow, RefreshReason, } from "./walletTypes"; import { ReserveTransaction } from "./ReserveTransaction"; +import { Timestamp, Duration, getTimestampNow } from "../util/time"; export enum ReserveRecordStatus { /** @@ -104,6 +102,13 @@ export function updateRetryInfoTimeout( p: RetryPolicy = defaultRetryPolicy, ): void { const now = getTimestampNow(); + if (now.t_ms === "never") { + throw Error("assertion failed"); + } + if (p.backoffDelta.d_ms === "forever") { + r.nextRetry = { t_ms: "never" }; + return; + } const t = now.t_ms + p.backoffDelta.d_ms * Math.pow(p.backoffBase, r.retryCounter); r.nextRetry = { t_ms: t }; @@ -319,86 +324,72 @@ export enum DenominationStatus { /** * Denomination record as stored in the wallet's database. */ -@Checkable.Class() -export class DenominationRecord { +export interface DenominationRecord { /** * Value of one coin of the denomination. */ - @Checkable.Value(() => AmountJson) value: AmountJson; /** * The denomination public key. */ - @Checkable.String() denomPub: string; /** * Hash of the denomination public key. * Stored in the database for faster lookups. */ - @Checkable.String() denomPubHash: string; /** * Fee for withdrawing. */ - @Checkable.Value(() => AmountJson) feeWithdraw: AmountJson; /** * Fee for depositing. */ - @Checkable.Value(() => AmountJson) feeDeposit: AmountJson; /** * Fee for refreshing. */ - @Checkable.Value(() => AmountJson) feeRefresh: AmountJson; /** * Fee for refunding. */ - @Checkable.Value(() => AmountJson) feeRefund: AmountJson; /** * Validity start date of the denomination. */ - @Checkable.Value(() => Timestamp) stampStart: Timestamp; /** * Date after which the currency can't be withdrawn anymore. */ - @Checkable.Value(() => Timestamp) stampExpireWithdraw: Timestamp; /** * Date after the denomination officially doesn't exist anymore. */ - @Checkable.Value(() => Timestamp) stampExpireLegal: Timestamp; /** * Data after which coins of this denomination can't be deposited anymore. */ - @Checkable.Value(() => Timestamp) stampExpireDeposit: Timestamp; /** * Signature by the exchange's master key over the denomination * information. */ - @Checkable.String() masterSig: string; /** * Did we verify the signature on the denomination? */ - @Checkable.Number() status: DenominationStatus; /** @@ -406,20 +397,12 @@ export class DenominationRecord { * we checked? * Only false when the exchange redacts a previously published denomination. */ - @Checkable.Boolean() isOffered: boolean; /** * Base URL of the exchange. */ - @Checkable.String() exchangeBaseUrl: string; - - /** - * Verify that a value matches the schema of this class and convert it into a - * member. - */ - static checked: (obj: any) => Denomination; } /** @@ -713,36 +696,21 @@ export const enum ProposalStatus { REPURCHASE = "repurchase", } -@Checkable.Class() -export class ProposalDownload { +export interface ProposalDownload { /** * The contract that was offered by the merchant. */ - @Checkable.Value(() => ContractTerms) - contractTerms: ContractTerms; + contractTermsRaw: string; - /** - * Signature by the merchant over the contract details. - */ - @Checkable.String() - merchantSig: string; - - /** - * Signature by the merchant over the contract details. - */ - @Checkable.String() - contractTermsHash: string; + contractData: WalletContractData; } /** * Record for a downloaded order, stored in the wallet's database. */ -@Checkable.Class() -export class ProposalRecord { - @Checkable.String() +export interface ProposalRecord { orderId: string; - @Checkable.String() merchantBaseUrl: string; /** @@ -753,38 +721,31 @@ export class ProposalRecord { /** * Unique ID when the order is stored in the wallet DB. */ - @Checkable.String() proposalId: string; /** * Timestamp (in ms) of when the record * was created. */ - @Checkable.Number() timestamp: Timestamp; /** * Private key for the nonce. */ - @Checkable.String() noncePriv: string; /** * Public key for the nonce. */ - @Checkable.String() noncePub: string; - @Checkable.String() proposalStatus: ProposalStatus; - @Checkable.String() repurchaseProposalId: string | undefined; /** * Session ID we got when downloading the contract. */ - @Checkable.Optional(Checkable.String()) downloadSessionId?: string; /** @@ -793,12 +754,6 @@ export class ProposalRecord { */ retryInfo: RetryInfo; - /** - * Verify that a value matches the schema of this class and convert it into a - * member. - */ - static checked: (obj: any) => ProposalRecord; - lastError: OperationError | undefined; } @@ -1120,6 +1075,38 @@ export interface ReserveUpdatedEventRecord { newHistoryTransactions: ReserveTransaction[]; } +export interface AllowedAuditorInfo { + auditorBaseUrl: string; + auditorPub: string; +} + +export interface AllowedExchangeInfo { + exchangeBaseUrl: string; + exchangePub: string; +} + +export interface WalletContractData { + fulfillmentUrl: string; + contractTermsHash: string; + merchantSig: string; + merchantPub: string; + amount: AmountJson; + orderId: string; + merchantBaseUrl: string; + summary: string; + autoRefund: Duration | undefined; + maxWireFee: AmountJson; + wireFeeAmortization: number; + payDeadline: Timestamp; + refundDeadline: Timestamp; + allowedAuditors: AllowedAuditorInfo[]; + allowedExchanges: AllowedExchangeInfo[]; + timestamp: Timestamp; + wireMethod: string; + wireInfoHash: string; + maxDepositFee: AmountJson; +} + /** * Record that stores status information about one purchase, starting from when * the customer accepts a proposal. Includes refund status if applicable. @@ -1132,14 +1119,11 @@ export interface PurchaseRecord { proposalId: string; /** - * Hash of the contract terms. - */ - contractTermsHash: string; - - /** * Contract terms we got from the merchant. */ - contractTerms: ContractTerms; + contractTermsRaw: string; + + contractData: WalletContractData; /** * The payment request, ready to be send to the merchant's @@ -1148,11 +1132,6 @@ export interface PurchaseRecord { payReq: PayReq; /** - * Signature from the merchant over the contract terms. - */ - merchantSig: string; - - /** * Timestamp of the first time that sending a payment to the merchant * for this purchase was successful. */ @@ -1266,12 +1245,9 @@ export interface DepositCoin { * the wallet itself, where the wallet acts as a "merchant" for the customer. */ export interface CoinsReturnRecord { - /** - * Hash of the contract for sending coins to our own bank account. - */ - contractTermsHash: string; + contractTermsRaw: string; - contractTerms: ContractTerms; + contractData: WalletContractData; /** * Private key where corresponding @@ -1446,11 +1422,11 @@ export namespace Stores { fulfillmentUrlIndex = new Index<string, PurchaseRecord>( this, "fulfillmentUrlIndex", - "contractTerms.fulfillment_url", + "contractData.fulfillmentUrl", ); orderIdIndex = new Index<string, PurchaseRecord>(this, "orderIdIndex", [ - "contractTerms.merchant_base_url", - "contractTerms.order_id", + "contractData.merchantBaseUrl", + "contractData.orderId", ]); } diff --git a/src/types/history.ts b/src/types/history.ts index c49afd476..783b55913 100644 --- a/src/types/history.ts +++ b/src/types/history.ts @@ -18,9 +18,10 @@ * Type and schema definitions for the wallet's history. */ -import { Timestamp, RefreshReason } from "./walletTypes"; +import { RefreshReason } from "./walletTypes"; import { ReserveTransaction } from "./ReserveTransaction"; import { WithdrawalSource } from "./dbTypes"; +import { Timestamp } from "../util/time"; /** diff --git a/src/types/pending.ts b/src/types/pending.ts index efb97f536..f3979ac81 100644 --- a/src/types/pending.ts +++ b/src/types/pending.ts @@ -21,8 +21,9 @@ /** * Imports. */ -import { OperationError, Timestamp, Duration } from "./walletTypes"; +import { OperationError } from "./walletTypes"; import { WithdrawalSource, RetryInfo } from "./dbTypes"; +import { Timestamp, Duration } from "../util/time"; export const enum PendingOperationType { Bug = "bug", diff --git a/src/types/talerTypes.ts b/src/types/talerTypes.ts index bb286b648..f8e2b1c64 100644 --- a/src/types/talerTypes.ts +++ b/src/types/talerTypes.ts @@ -26,132 +26,115 @@ /** * Imports. */ -import { Checkable } from "../util/checkable"; -import * as Amounts from "../util/amounts"; - -import { timestampCheck } from "../util/helpers"; +import { + typecheckedCodec, + makeCodecForObject, + codecForString, + makeCodecForList, + makeCodecOptional, + codecForAny, + codecForNumber, + codecForBoolean, + makeCodecForMap, +} from "../util/codec"; +import { Timestamp, codecForTimestamp, Duration, codecForDuration } from "../util/time"; /** * Denomination as found in the /keys response from the exchange. */ -@Checkable.Class() export class Denomination { /** * Value of one coin of the denomination. */ - @Checkable.String(Amounts.check) value: string; /** * Public signing key of the denomination. */ - @Checkable.String() denom_pub: string; /** * Fee for withdrawing. */ - @Checkable.String(Amounts.check) fee_withdraw: string; /** * Fee for depositing. */ - @Checkable.String(Amounts.check) fee_deposit: string; /** * Fee for refreshing. */ - @Checkable.String(Amounts.check) fee_refresh: string; /** * Fee for refunding. */ - @Checkable.String(Amounts.check) fee_refund: string; /** * Start date from which withdraw is allowed. */ - @Checkable.String(timestampCheck) - stamp_start: string; + stamp_start: Timestamp; /** * End date for withdrawing. */ - @Checkable.String(timestampCheck) - stamp_expire_withdraw: string; + stamp_expire_withdraw: Timestamp; /** * Expiration date after which the exchange can forget about * the currency. */ - @Checkable.String(timestampCheck) - stamp_expire_legal: string; + stamp_expire_legal: Timestamp; /** * Date after which the coins of this denomination can't be * deposited anymore. */ - @Checkable.String(timestampCheck) - stamp_expire_deposit: string; + stamp_expire_deposit: Timestamp; /** * Signature over the denomination information by the exchange's master * signing key. */ - @Checkable.String() master_sig: string; - - /** - * Verify that a value matches the schema of this class and convert it into a - * member. - */ - static checked: (obj: any) => Denomination; } /** * Signature by the auditor that a particular denomination key is audited. */ -@Checkable.Class() export class AuditorDenomSig { /** * Denomination public key's hash. */ - @Checkable.String() denom_pub_h: string; /** * The signature. */ - @Checkable.String() auditor_sig: string; } /** * Auditor information as given by the exchange in /keys. */ -@Checkable.Class() export class Auditor { /** * Auditor's public key. */ - @Checkable.String() auditor_pub: string; /** * Base URL of the auditor. */ - @Checkable.String() auditor_url: string; /** * List of signatures for denominations by the auditor. */ - @Checkable.List(Checkable.Value(() => AuditorDenomSig)) denomination_keys: AuditorDenomSig[]; } @@ -190,26 +173,22 @@ export interface PaybackRequest { /** * Response that we get from the exchange for a payback request. */ -@Checkable.Class() -export class PaybackConfirmation { +export class RecoupConfirmation { /** * public key of the reserve that will receive the payback. */ - @Checkable.String() reserve_pub: string; /** * How much will the exchange pay back (needed by wallet in * case coin was partially spent and wallet got restored from backup) */ - @Checkable.String() amount: string; /** * Time by which the exchange received the /payback request. */ - @Checkable.String() - timestamp: string; + timestamp: Timestamp; /** * the EdDSA signature of TALER_PaybackConfirmationPS using a current @@ -218,7 +197,6 @@ 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() exchange_sig: string; /** @@ -227,14 +205,7 @@ export class PaybackConfirmation { * explicitly as the client might otherwise be confused by clock skew as to * which signing key was used. */ - @Checkable.String() exchange_pub: string; - - /** - * Verify that a value matches the schema of this class and convert it into a - * member. - */ - static checked: (obj: any) => PaybackConfirmation; } /** @@ -272,183 +243,155 @@ export interface CoinPaySig { * Information about an exchange as stored inside a * merchant's contract terms. */ -@Checkable.Class() export class ExchangeHandle { /** * Master public signing key of the exchange. */ - @Checkable.String() master_pub: string; /** * Base URL of the exchange. */ - @Checkable.String() url: string; +} +export class AuditorHandle { /** - * Verify that a value matches the schema of this class and convert it into a - * member. + * Official name of the auditor. */ - static checked: (obj: any) => ExchangeHandle; + name: string; + + /** + * Master public signing key of the auditor. + */ + master_pub: string; + + /** + * Base URL of the auditor. + */ + url: string; } /** * Contract terms from a merchant. */ -@Checkable.Class({ validate: true }) export class ContractTerms { - static validate(x: ContractTerms) { - if (x.exchanges.length === 0) { - throw Error("no exchanges in contract terms"); - } - } - /** * Hash of the merchant's wire details. */ - @Checkable.String() H_wire: string; /** * Hash of the merchant's wire details. */ - @Checkable.Optional(Checkable.String()) - auto_refund?: string; + auto_refund?: Duration; /** * Wire method the merchant wants to use. */ - @Checkable.String() wire_method: string; /** * Human-readable short summary of the contract. */ - @Checkable.Optional(Checkable.String()) - summary?: string; + summary: string; /** * Nonce used to ensure freshness. */ - @Checkable.Optional(Checkable.String()) - nonce?: string; + nonce: string; /** * Total amount payable. */ - @Checkable.String(Amounts.check) amount: string; /** * Auditors accepted by the merchant. */ - @Checkable.List(Checkable.AnyObject()) - auditors: any[]; + auditors: AuditorHandle[]; /** * Deadline to pay for the contract. */ - @Checkable.Optional(Checkable.String()) - pay_deadline: string; + pay_deadline: Timestamp; /** * Delivery locations. */ - @Checkable.Any() locations: any; /** * Maximum deposit fee covered by the merchant. */ - @Checkable.String(Amounts.check) max_fee: string; /** * Information about the merchant. */ - @Checkable.Any() merchant: any; /** * Public key of the merchant. */ - @Checkable.String() merchant_pub: string; /** * List of accepted exchanges. */ - @Checkable.List(Checkable.Value(() => ExchangeHandle)) exchanges: ExchangeHandle[]; /** * Products that are sold in this contract. */ - @Checkable.List(Checkable.AnyObject()) - products: any[]; + products?: any[]; /** * Deadline for refunds. */ - @Checkable.String(timestampCheck) - refund_deadline: string; + refund_deadline: Timestamp; /** * Deadline for the wire transfer. */ - @Checkable.String() - wire_transfer_deadline: string; + wire_transfer_deadline: Timestamp; /** * Time when the contract was generated by the merchant. */ - @Checkable.String(timestampCheck) - timestamp: string; + timestamp: Timestamp; /** * Order id to uniquely identify the purchase within * one merchant instance. */ - @Checkable.String() order_id: string; /** * Base URL of the merchant's backend. */ - @Checkable.String() merchant_base_url: string; /** * Fulfillment URL to view the product or * delivery status. */ - @Checkable.String() fulfillment_url: string; /** * Share of the wire fee that must be settled with one payment. */ - @Checkable.Optional(Checkable.Number()) wire_fee_amortization?: number; /** * Maximum wire fee that the merchant agrees to pay for. */ - @Checkable.Optional(Checkable.String()) max_wire_fee?: string; /** * Extra data, interpreted by the mechant only. */ - @Checkable.Any() extra: any; - - /** - * Verify that a value matches the schema of this class and convert it into a - * member. - */ - static checked: (obj: any) => ContractTerms; } /** @@ -480,42 +423,31 @@ export interface PayReq { /** * Refund permission in the format that the merchant gives it to us. */ -@Checkable.Class() export class MerchantRefundPermission { /** * Amount to be refunded. */ - @Checkable.String(Amounts.check) refund_amount: string; /** * Fee for the refund. */ - @Checkable.String(Amounts.check) refund_fee: string; /** * Public key of the coin being refunded. */ - @Checkable.String() coin_pub: string; /** * Refund transaction ID between merchant and exchange. */ - @Checkable.Number() rtransaction_id: number; /** * Signature made by the merchant over the refund permission. */ - @Checkable.String() merchant_sig: string; - - /** - * Create a MerchantRefundPermission from untyped JSON. - */ - static checked: (obj: any) => MerchantRefundPermission; } /** @@ -564,31 +496,22 @@ export interface RefundRequest { /** * Response for a refund pickup or a /pay in abort mode. */ -@Checkable.Class() export class MerchantRefundResponse { /** * Public key of the merchant */ - @Checkable.String() merchant_pub: string; /** * Contract terms hash of the contract that * is being refunded. */ - @Checkable.String() h_contract_terms: string; /** * The signed refund permissions, to be sent to the exchange. */ - @Checkable.List(Checkable.Value(() => MerchantRefundPermission)) refund_permissions: MerchantRefundPermission[]; - - /** - * Create a MerchantRefundReponse from untyped JSON. - */ - static checked: (obj: any) => MerchantRefundResponse; } /** @@ -625,306 +548,411 @@ export interface TipPickupRequest { * Reserve signature, defined as separate class to facilitate * schema validation with "@Checkable". */ -@Checkable.Class() export class ReserveSigSingleton { /** * Reserve signature. */ - @Checkable.String() reserve_sig: string; - - /** - * Create a ReserveSigSingleton from untyped JSON. - */ - static checked: (obj: any) => ReserveSigSingleton; } - /** * Response of the merchant * to the TipPickupRequest. */ -@Checkable.Class() export class TipResponse { /** * Public key of the reserve */ - @Checkable.String() reserve_pub: string; /** * The order of the signatures matches the planchets list. */ - @Checkable.List(Checkable.Value(() => ReserveSigSingleton)) reserve_sigs: ReserveSigSingleton[]; - - /** - * Create a TipResponse from untyped JSON. - */ - static checked: (obj: any) => TipResponse; } /** * Element of the payback list that the * exchange gives us in /keys. */ -@Checkable.Class() export class Payback { /** * The hash of the denomination public key for which the payback is offered. */ - @Checkable.String() h_denom_pub: string; } /** * Structure that the exchange gives us in /keys. */ -@Checkable.Class({ extra: true }) -export class KeysJson { +export class ExchangeKeysJson { /** * List of offered denominations. */ - @Checkable.List(Checkable.Value(() => Denomination)) denoms: Denomination[]; /** * The exchange's master public key. */ - @Checkable.String() master_public_key: string; /** * The list of auditors (partially) auditing the exchange. */ - @Checkable.List(Checkable.Value(() => Auditor)) auditors: Auditor[]; /** * Timestamp when this response was issued. */ - @Checkable.String(timestampCheck) - list_issue_date: string; + list_issue_date: Timestamp; /** * List of paybacks for compromised denominations. */ - @Checkable.Optional(Checkable.List(Checkable.Value(() => Payback))) payback?: Payback[]; /** * Short-lived signing keys used to sign online * responses. */ - @Checkable.Any() signkeys: any; /** * Protocol version. */ - @Checkable.Optional(Checkable.String()) - version?: string; - - /** - * Verify that a value matches the schema of this class and convert it into a - * member. - */ - static checked: (obj: any) => KeysJson; + version: string; } /** * Wire fees as anounced by the exchange. */ -@Checkable.Class() export class WireFeesJson { /** * Cost of a wire transfer. */ - @Checkable.String(Amounts.check) wire_fee: string; /** * Cost of clising a reserve. */ - @Checkable.String(Amounts.check) closing_fee: string; /** * Signature made with the exchange's master key. */ - @Checkable.String() sig: string; /** * Date from which the fee applies. */ - @Checkable.String(timestampCheck) - start_date: string; + start_date: Timestamp; /** * Data after which the fee doesn't apply anymore. */ - @Checkable.String(timestampCheck) - end_date: string; - - /** - * Verify that a value matches the schema of this class and convert it into a - * member. - */ - static checked: (obj: any) => WireFeesJson; + end_date: Timestamp; } -@Checkable.Class({ extra: true }) export class AccountInfo { - @Checkable.String() url: string; - - @Checkable.String() master_sig: string; } -@Checkable.Class({ extra: true }) export class ExchangeWireJson { - @Checkable.Map( - Checkable.String(), - Checkable.List(Checkable.Value(() => WireFeesJson)), - ) - fees: { [methodName: string]: WireFeesJson[] }; - - @Checkable.List(Checkable.Value(() => AccountInfo)) accounts: AccountInfo[]; - - static checked: (obj: any) => ExchangeWireJson; + fees: { [methodName: string]: WireFeesJson[] }; } /** - * Wire detail, arbitrary object that must at least - * contain a "type" key. - */ -export type WireDetail = object & { type: string }; - -/** * Proposal returned from the contract URL. */ -@Checkable.Class({ extra: true }) export class Proposal { /** * Contract terms for the propoal. + * Raw, un-decoded JSON object. */ - @Checkable.Value(() => ContractTerms) - contract_terms: ContractTerms; + contract_terms: any; /** * Signature over contract, made by the merchant. The public key used for signing * must be contract_terms.merchant_pub. */ - @Checkable.String() sig: string; - - /** - * Verify that a value matches the schema of this class and convert it into a - * member. - */ - static checked: (obj: any) => Proposal; } /** * Response from the internal merchant API. */ -@Checkable.Class({ extra: true }) export class CheckPaymentResponse { - @Checkable.Boolean() paid: boolean; - - @Checkable.Optional(Checkable.Boolean()) refunded: boolean | undefined; - - @Checkable.Optional(Checkable.String()) refunded_amount: string | undefined; - - @Checkable.Optional(Checkable.Value(() => ContractTerms)) - contract_terms: ContractTerms | undefined; - - @Checkable.Optional(Checkable.String()) + contract_terms: any | undefined; taler_pay_uri: string | undefined; - - @Checkable.Optional(Checkable.String()) contract_url: string | undefined; - - /** - * Verify that a value matches the schema of this class and convert it into a - * member. - */ - static checked: (obj: any) => CheckPaymentResponse; } /** * Response from the bank. */ -@Checkable.Class({ extra: true }) export class WithdrawOperationStatusResponse { - @Checkable.Boolean() selection_done: boolean; - @Checkable.Boolean() transfer_done: boolean; - @Checkable.String() amount: string; - @Checkable.Optional(Checkable.String()) sender_wire?: string; - @Checkable.Optional(Checkable.String()) suggested_exchange?: string; - @Checkable.Optional(Checkable.String()) confirm_transfer_url?: string; - @Checkable.List(Checkable.String()) wire_types: string[]; - - /** - * Verify that a value matches the schema of this class and convert it into a - * member. - */ - static checked: (obj: any) => WithdrawOperationStatusResponse; } /** * Response from the merchant. */ -@Checkable.Class({ extra: true }) export class TipPickupGetResponse { - @Checkable.AnyObject() extra: any; - @Checkable.String() amount: string; - @Checkable.String() amount_left: string; - @Checkable.String() exchange_url: string; - @Checkable.String() - stamp_expire: string; - - @Checkable.String() - stamp_created: string; + stamp_expire: Timestamp; - /** - * Verify that a value matches the schema of this class and convert it into a - * member. - */ - static checked: (obj: any) => TipPickupGetResponse; + stamp_created: Timestamp; } - export type AmountString = string; export type Base32String = string; export type EddsaSignatureString = string; export type EddsaPublicKeyString = string; export type CoinPublicKeyString = string; -export type TimestampString = string;
\ No newline at end of file + +export const codecForDenomination = () => + typecheckedCodec<Denomination>( + makeCodecForObject<Denomination>() + .property("value", codecForString) + .property("denom_pub", codecForString) + .property("fee_withdraw", codecForString) + .property("fee_deposit", codecForString) + .property("fee_refresh", codecForString) + .property("fee_refund", codecForString) + .property("stamp_start", codecForTimestamp) + .property("stamp_expire_withdraw", codecForTimestamp) + .property("stamp_expire_legal", codecForTimestamp) + .property("stamp_expire_deposit", codecForTimestamp) + .property("master_sig", codecForString) + .build("Denomination"), + ); + +export const codecForAuditorDenomSig = () => + typecheckedCodec<AuditorDenomSig>( + makeCodecForObject<AuditorDenomSig>() + .property("denom_pub_h", codecForString) + .property("auditor_sig", codecForString) + .build("AuditorDenomSig"), + ); + +export const codecForAuditor = () => + typecheckedCodec<Auditor>( + makeCodecForObject<Auditor>() + .property("auditor_pub", codecForString) + .property("auditor_url", codecForString) + .property("denomination_keys", makeCodecForList(codecForAuditorDenomSig())) + .build("Auditor"), + ); + +export const codecForExchangeHandle = () => + typecheckedCodec<ExchangeHandle>( + makeCodecForObject<ExchangeHandle>() + .property("master_pub", codecForString) + .property("url", codecForString) + .build("ExchangeHandle"), + ); + +export const codecForAuditorHandle = () => + typecheckedCodec<AuditorHandle>( + makeCodecForObject<AuditorHandle>() + .property("name", codecForString) + .property("master_pub", codecForString) + .property("url", codecForString) + .build("AuditorHandle"), + ); + +export const codecForContractTerms = () => + typecheckedCodec<ContractTerms>( + makeCodecForObject<ContractTerms>() + .property("order_id", codecForString) + .property("fulfillment_url", codecForString) + .property("merchant_base_url", codecForString) + .property("H_wire", codecForString) + .property("auto_refund", makeCodecOptional(codecForDuration)) + .property("wire_method", codecForString) + .property("summary", codecForString) + .property("nonce", codecForString) + .property("amount", codecForString) + .property("auditors", makeCodecForList(codecForAuditorHandle())) + .property("pay_deadline", codecForTimestamp) + .property("refund_deadline", codecForTimestamp) + .property("wire_transfer_deadline", codecForTimestamp) + .property("timestamp", codecForTimestamp) + .property("locations", codecForAny) + .property("max_fee", codecForString) + .property("max_wire_fee", makeCodecOptional(codecForString)) + .property("merchant", codecForAny) + .property("merchant_pub", codecForString) + .property("exchanges", makeCodecForList(codecForExchangeHandle())) + .property("products", makeCodecOptional(makeCodecForList(codecForAny))) + .property("extra", codecForAny) + .build("ContractTerms"), + ); + +export const codecForMerchantRefundPermission = () => + typecheckedCodec<MerchantRefundPermission>( + makeCodecForObject<MerchantRefundPermission>() + .property("refund_amount", codecForString) + .property("refund_fee", codecForString) + .property("coin_pub", codecForString) + .property("rtransaction_id", codecForNumber) + .property("merchant_sig", codecForString) + .build("MerchantRefundPermission"), + ); + +export const codecForMerchantRefundResponse = () => + typecheckedCodec<MerchantRefundResponse>( + makeCodecForObject<MerchantRefundResponse>() + .property("merchant_pub", codecForString) + .property("h_contract_terms", codecForString) + .property( + "refund_permissions", + makeCodecForList(codecForMerchantRefundPermission()), + ) + .build("MerchantRefundResponse"), + ); + +export const codecForReserveSigSingleton = () => + typecheckedCodec<ReserveSigSingleton>( + makeCodecForObject<ReserveSigSingleton>() + .property("reserve_sig", codecForString) + .build("ReserveSigSingleton"), + ); + +export const codecForTipResponse = () => + typecheckedCodec<TipResponse>( + makeCodecForObject<TipResponse>() + .property("reserve_pub", codecForString) + .property("reserve_sigs", makeCodecForList(codecForReserveSigSingleton())) + .build("TipResponse"), + ); + +export const codecForPayback = () => + typecheckedCodec<Payback>( + makeCodecForObject<Payback>() + .property("h_denom_pub", codecForString) + .build("Payback"), + ); + +export const codecForExchangeKeysJson = () => + typecheckedCodec<ExchangeKeysJson>( + makeCodecForObject<ExchangeKeysJson>() + .property("denoms", makeCodecForList(codecForDenomination())) + .property("master_public_key", codecForString) + .property("auditors", makeCodecForList(codecForAuditor())) + .property("list_issue_date", codecForTimestamp) + .property("payback", makeCodecOptional(makeCodecForList(codecForPayback()))) + .property("signkeys", codecForAny) + .property("version", codecForString) + .build("KeysJson"), + ); + + +export const codecForWireFeesJson = () => + typecheckedCodec<WireFeesJson>( + makeCodecForObject<WireFeesJson>() + .property("wire_fee", codecForString) + .property("closing_fee", codecForString) + .property("sig", codecForString) + .property("start_date", codecForTimestamp) + .property("end_date", codecForTimestamp) + .build("WireFeesJson"), + ); + +export const codecForAccountInfo = () => + typecheckedCodec<AccountInfo>( + makeCodecForObject<AccountInfo>() + .property("url", codecForString) + .property("master_sig", codecForString) + .build("AccountInfo"), + ); + +export const codecForExchangeWireJson = () => + typecheckedCodec<ExchangeWireJson>( + makeCodecForObject<ExchangeWireJson>() + .property("accounts", makeCodecForList(codecForAccountInfo())) + .property("fees", makeCodecForMap(makeCodecForList(codecForWireFeesJson()))) + .build("ExchangeWireJson"), + ); + +export const codecForProposal = () => + typecheckedCodec<Proposal>( + makeCodecForObject<Proposal>() + .property("contract_terms", codecForAny) + .property("sig", codecForString) + .build("Proposal"), + ); + +export const codecForCheckPaymentResponse = () => + typecheckedCodec<CheckPaymentResponse>( + makeCodecForObject<CheckPaymentResponse>() + .property("paid", codecForBoolean) + .property("refunded", makeCodecOptional(codecForBoolean)) + .property("refunded_amount", makeCodecOptional(codecForString)) + .property("contract_terms", makeCodecOptional(codecForAny)) + .property("taler_pay_uri", makeCodecOptional(codecForString)) + .property("contract_url", makeCodecOptional(codecForString)) + .build("CheckPaymentResponse"), + ); + + +export const codecForWithdrawOperationStatusResponse = () => + typecheckedCodec<WithdrawOperationStatusResponse>( + makeCodecForObject<WithdrawOperationStatusResponse>() + .property("selection_done", codecForBoolean) + .property("transfer_done", codecForBoolean) + .property("amount",codecForString) + .property("sender_wire", makeCodecOptional(codecForString)) + .property("suggested_exchange", makeCodecOptional(codecForString)) + .property("confirm_transfer_url", makeCodecOptional(codecForString)) + .property("wire_types", makeCodecForList(codecForString)) + .build("WithdrawOperationStatusResponse"), + ); + +export const codecForTipPickupGetResponse = () => + typecheckedCodec<TipPickupGetResponse>( + makeCodecForObject<TipPickupGetResponse>() + .property("extra", codecForAny) + .property("amount", codecForString) + .property("amount_left", codecForString) + .property("exchange_url", codecForString) + .property("stamp_expire", codecForTimestamp) + .property("stamp_created", codecForTimestamp) + .build("TipPickupGetResponse"), + ); + + +export const codecForRecoupConfirmation = () => + typecheckedCodec<RecoupConfirmation>( + makeCodecForObject<RecoupConfirmation>() + .property("reserve_pub", codecForString) + .property("amount", codecForString) + .property("timestamp", codecForTimestamp) + .property("exchange_sig", codecForString) + .property("exchange_pub", codecForString) + .build("RecoupConfirmation"), + ); diff --git a/src/types/types-test.ts b/src/types/types-test.ts index a686fbe38..77ab2c4e4 100644 --- a/src/types/types-test.ts +++ b/src/types/types-test.ts @@ -16,7 +16,7 @@ import test from "ava"; import * as Amounts from "../util/amounts"; -import { ContractTerms } from "./talerTypes"; +import { ContractTerms, codecForContractTerms } from "./talerTypes"; const amt = ( value: number, @@ -130,6 +130,7 @@ test("amount stringification", t => { test("contract terms validation", t => { const c = { + nonce: "123123123", H_wire: "123", amount: "EUR:1.5", auditors: [], @@ -138,23 +139,23 @@ test("contract terms validation", t => { max_fee: "EUR:1.5", merchant_pub: "12345", order_id: "test_order", - pay_deadline: "Date(12346)", - wire_transfer_deadline: "Date(12346)", + pay_deadline: { t_ms: 42 }, + wire_transfer_deadline: { t_ms: 42 }, merchant_base_url: "https://example.com/pay", products: [], - refund_deadline: "Date(12345)", + refund_deadline: { t_ms: 42 }, summary: "hello", - timestamp: "Date(12345)", + timestamp: { t_ms: 42 }, wire_method: "test", }; - ContractTerms.checked(c); + codecForContractTerms().decode(c); const c1 = JSON.parse(JSON.stringify(c)); - c1.exchanges = []; + c1.pay_deadline = "foo"; try { - ContractTerms.checked(c1); + codecForContractTerms().decode(c1); } catch (e) { t.pass(); return; diff --git a/src/types/walletTypes.ts b/src/types/walletTypes.ts index df19d8dc2..223ca4329 100644 --- a/src/types/walletTypes.ts +++ b/src/types/walletTypes.ts @@ -25,8 +25,7 @@ /** * Imports. */ -import { AmountJson } from "../util/amounts"; -import { Checkable } from "../util/checkable"; +import { AmountJson, codecForAmountJson } from "../util/amounts"; import * as LibtoolVersion from "../util/libtoolVersion"; import { CoinRecord, @@ -35,30 +34,23 @@ import { ExchangeWireInfo, } from "./dbTypes"; import { CoinPaySig, ContractTerms } from "./talerTypes"; +import { Timestamp } from "../util/time"; +import { typecheckedCodec, makeCodecForObject, codecForString, makeCodecOptional } from "../util/codec"; /** * Response for the create reserve request to the wallet. */ -@Checkable.Class() export class CreateReserveResponse { /** * Exchange URL where the bank should create the reserve. * The URL is canonicalized in the response. */ - @Checkable.String() exchange: string; /** * Reserve public key of the newly created reserve. */ - @Checkable.String() reservePub: string; - - /** - * Verify that a value matches the schema of this class and convert it into a - * member. - */ - static checked: (obj: any) => CreateReserveResponse; } /** @@ -259,88 +251,83 @@ export interface SenderWireInfos { /** * Request to mark a reserve as confirmed. */ -@Checkable.Class() -export class CreateReserveRequest { +export interface CreateReserveRequest { /** * The initial amount for the reserve. */ - @Checkable.Value(() => AmountJson) amount: AmountJson; /** * Exchange URL where the bank should create the reserve. */ - @Checkable.String() exchange: string; /** * Payto URI that identifies the exchange's account that the funds * for this reserve go into. */ - @Checkable.String() exchangeWire: string; /** * Wire details (as a payto URI) for the bank account that sent the funds to * the exchange. */ - @Checkable.Optional(Checkable.String()) senderWire?: string; /** * URL to fetch the withdraw status from the bank. */ - @Checkable.Optional(Checkable.String()) bankWithdrawStatusUrl?: string; - - /** - * Verify that a value matches the schema of this class and convert it into a - * member. - */ - static checked: (obj: any) => CreateReserveRequest; } +export const codecForCreateReserveRequest = () => + typecheckedCodec<CreateReserveRequest>( + makeCodecForObject<CreateReserveRequest>() + .property("amount", codecForAmountJson()) + .property("exchange", codecForString) + .property("exchangeWire", codecForString) + .property("senderWire", makeCodecOptional(codecForString)) + .property("bankWithdrawStatusUrl", makeCodecOptional(codecForString)) + .build("CreateReserveRequest"), + ); + /** * Request to mark a reserve as confirmed. */ -@Checkable.Class() -export class ConfirmReserveRequest { +export interface ConfirmReserveRequest { /** * Public key of then reserve that should be marked * as confirmed. */ - @Checkable.String() reservePub: string; - - /** - * Verify that a value matches the schema of this class and convert it into a - * member. - */ - static checked: (obj: any) => ConfirmReserveRequest; } + +export const codecForConfirmReserveRequest = () => + typecheckedCodec<ConfirmReserveRequest>( + makeCodecForObject<ConfirmReserveRequest>() + .property("reservePub", codecForString) + .build("ConfirmReserveRequest"), + ); + /** * Wire coins to the user's own bank account. */ -@Checkable.Class() export class ReturnCoinsRequest { /** * The amount to wire. */ - @Checkable.Value(() => AmountJson) amount: AmountJson; /** * The exchange to take the coins from. */ - @Checkable.String() exchange: string; /** * Wire details for the bank account of the customer that will * receive the funds. */ - @Checkable.Any() senderWire?: object; /** @@ -391,8 +378,8 @@ export interface TipStatus { tipId: string; merchantTipId: string; merchantOrigin: string; - expirationTimestamp: number; - timestamp: number; + expirationTimestamp: Timestamp; + timestamp: Timestamp; totalFees: AmountJson; } @@ -418,14 +405,14 @@ export type PreparePayResult = export interface PreparePayResultPaymentPossible { status: "payment-possible"; proposalId: string; - contractTerms: ContractTerms; + contractTermsRaw: string; totalFees: AmountJson; } export interface PreparePayResultInsufficientBalance { status: "insufficient-balance"; proposalId: string; - contractTerms: ContractTerms; + contractTermsRaw: any; } export interface PreparePayResultError { @@ -435,7 +422,7 @@ export interface PreparePayResultError { export interface PreparePayResultPaid { status: "paid"; - contractTerms: ContractTerms; + contractTermsRaw: any; nextUrl: string; } @@ -459,7 +446,7 @@ export interface AcceptWithdrawalResponse { * Details about a purchase, including refund status. */ export interface PurchaseDetails { - contractTerms: ContractTerms; + contractTerms: any; hasRefund: boolean; totalRefundAmount: AmountJson; totalRefundAndRefreshFees: AmountJson; @@ -479,30 +466,6 @@ export interface OperationError { details: any; } -@Checkable.Class() -export class Timestamp { - /** - * Timestamp in milliseconds. - */ - @Checkable.Number() - readonly t_ms: number; - - static checked: (obj: any) => Timestamp; -} - -export interface Duration { - /** - * Duration in milliseconds. - */ - readonly d_ms: number; -} - -export function getTimestampNow(): Timestamp { - return { - t_ms: new Date().getTime(), - }; -} - export interface PlanchetCreationResult { coinPub: string; coinPriv: string; diff --git a/src/util/RequestThrottler.ts b/src/util/RequestThrottler.ts index 01695ec37..0566306de 100644 --- a/src/util/RequestThrottler.ts +++ b/src/util/RequestThrottler.ts @@ -21,7 +21,7 @@ /** * Imports. */ -import { getTimestampNow, Timestamp } from "../types/walletTypes"; +import { getTimestampNow, Timestamp, timestampSubtractDuraction, timestampDifference } from "../util/time"; /** * Maximum request per second, per origin. @@ -50,10 +50,14 @@ class OriginState { private refill(): void { const now = getTimestampNow(); - const d = now.t_ms - this.lastUpdate.t_ms; - this.tokensSecond = Math.min(MAX_PER_SECOND, this.tokensSecond + (d / 1000)); - this.tokensMinute = Math.min(MAX_PER_MINUTE, this.tokensMinute + (d / 1000 * 60)); - this.tokensHour = Math.min(MAX_PER_HOUR, this.tokensHour + (d / 1000 * 60 * 60)); + const d = timestampDifference(now, this.lastUpdate); + if (d.d_ms === "forever") { + throw Error("assertion failed") + } + const d_s = d.d_ms / 1000; + this.tokensSecond = Math.min(MAX_PER_SECOND, this.tokensSecond + (d_s / 1000)); + this.tokensMinute = Math.min(MAX_PER_MINUTE, this.tokensMinute + (d_s / 1000 * 60)); + this.tokensHour = Math.min(MAX_PER_HOUR, this.tokensHour + (d_s / 1000 * 60 * 60)); this.lastUpdate = now; } diff --git a/src/util/amounts.ts b/src/util/amounts.ts index c8fb76793..c85c4839a 100644 --- a/src/util/amounts.ts +++ b/src/util/amounts.ts @@ -1,17 +1,17 @@ /* - This file is part of TALER - (C) 2018 GNUnet e.V. and INRIA + This file is part of GNU Taler + (C) 2019 Taler Systems S.A. - TALER is free software; you can redistribute it and/or modify it under the + GNU Taler is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3, or (at your option) any later version. - TALER is distributed in the hope that it will be useful, but WITHOUT ANY + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with - TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ /** @@ -21,7 +21,12 @@ /** * Imports. */ -import { Checkable } from "./checkable"; +import { + typecheckedCodec, + makeCodecForObject, + codecForString, + codecForNumber, +} from "./codec"; /** * Number of fractional units that one value unit represents. @@ -44,29 +49,32 @@ export const maxAmountValue = 2 ** 52; * Non-negative financial amount. Fractional values are expressed as multiples * of 1e-8. */ -@Checkable.Class() -export class AmountJson { +export interface AmountJson { /** * Value, must be an integer. */ - @Checkable.Number() readonly value: number; /** * Fraction, must be an integer. Represent 1/1e8 of a unit. */ - @Checkable.Number() readonly fraction: number; /** * Currency of the amount. */ - @Checkable.String() readonly currency: string; - - static checked: (obj: any) => AmountJson; } +export const codecForAmountJson = () => + typecheckedCodec<AmountJson>( + makeCodecForObject<AmountJson>() + .property("currency", codecForString) + .property("value", codecForNumber) + .property("fraction", codecForNumber) + .build("AmountJson"), + ); + /** * Result of a possibly overflowing operation. */ diff --git a/src/util/checkable.ts b/src/util/checkable.ts deleted file mode 100644 index 3c9fe5bc1..000000000 --- a/src/util/checkable.ts +++ /dev/null @@ -1,417 +0,0 @@ -/* - This file is part of TALER - (C) 2016 GNUnet e.V. - - TALER is free software; you can redistribute it and/or modify it under the - terms of the GNU General Public License as published by the Free Software - Foundation; either version 3, or (at your option) any later version. - - TALER is distributed in the hope that it will be useful, but WITHOUT ANY - WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR - A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with - TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> - */ - - -/** - * Decorators for validating JSON objects and converting them to a typed - * object. - * - * The decorators are put onto classes, and the validation is done - * via a static method that is filled in by the annotation. - * - * Example: - * ``` - * @Checkable.Class - * class Person { - * @Checkable.String - * name: string; - * @Checkable.Number - * age: number; - * - * // Method will be implemented automatically - * static checked(obj: any): Person; - * } - * ``` - */ -export namespace Checkable { - - type Path = Array<number | string>; - - interface SchemaErrorConstructor { - new (err: string): SchemaError; - } - - interface SchemaError { - name: string; - message: string; - } - - interface Prop { - propertyKey: any; - checker: any; - type?: any; - typeThunk?: () => any; - elementChecker?: any; - elementProp?: any; - keyProp?: any; - stringChecker?: (s: string) => boolean; - valueProp?: any; - optional?: boolean; - } - - interface CheckableInfo { - extraAllowed: boolean; - props: Prop[]; - } - - // tslint:disable-next-line:no-shadowed-variable - export const SchemaError = (function SchemaError(this: any, message: string) { - const that: any = this as any; - that.name = "SchemaError"; - that.message = message; - that.stack = (new Error() as any).stack; - }) as any as SchemaErrorConstructor; - - - SchemaError.prototype = new Error(); - - /** - * Classes that are checkable are annotated with this - * checkable info symbol, which contains the information necessary - * to check if they're valid. - */ - const checkableInfoSym = Symbol("checkableInfo"); - - /** - * Get the current property list for a checkable type. - */ - function getCheckableInfo(target: any): CheckableInfo { - let chk = target[checkableInfoSym] as CheckableInfo|undefined; - if (!chk) { - chk = { props: [], extraAllowed: false }; - target[checkableInfoSym] = chk; - } - return chk; - } - - - function checkNumber(target: any, prop: Prop, path: Path): any { - if ((typeof target) !== "number") { - throw new SchemaError(`expected number for ${path}`); - } - return target; - } - - - function checkString(target: any, prop: Prop, path: Path): any { - 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; - } - - function checkBoolean(target: any, prop: Prop, path: Path): any { - if (typeof target !== "boolean") { - throw new SchemaError(`expected boolean for ${path}, got ${typeof target} instead`); - } - return target; - } - - - function checkAnyObject(target: any, prop: Prop, path: Path): any { - if (typeof target !== "object") { - throw new SchemaError(`expected (any) object for ${path}, got ${typeof target} instead`); - } - return target; - } - - - function checkAny(target: any, prop: Prop, path: Path): any { - return target; - } - - - function checkList(target: any, prop: Prop, path: Path): any { - if (!Array.isArray(target)) { - throw new SchemaError(`array expected for ${path}, got ${typeof target} instead`); - } - for (let i = 0; i < target.length; i++) { - const v = target[i]; - prop.elementChecker(v, prop.elementProp, path.concat([i])); - } - return target; - } - - function checkMap(target: any, prop: Prop, path: Path): any { - if (typeof target !== "object") { - throw new SchemaError(`expected object for ${path}, got ${typeof target} instead`); - } - for (const key in target) { - prop.keyProp.checker(key, prop.keyProp, path.concat([key])); - const value = target[key]; - prop.valueProp.checker(value, prop.valueProp, path.concat([key])); - } - return target; - } - - - function checkOptional(target: any, prop: Prop, path: Path): any { - console.assert(prop.propertyKey); - prop.elementChecker(target, - prop.elementProp, - path.concat([prop.propertyKey])); - return target; - } - - - function checkValue(target: any, prop: Prop, path: Path): any { - let type; - if (prop.type) { - type = prop.type; - } else if (prop.typeThunk) { - type = prop.typeThunk(); - if (!type) { - throw Error(`assertion failed: typeThunk returned null (prop is ${JSON.stringify(prop)})`); - } - } else { - throw Error(`assertion failed: type/typeThunk missing (prop is ${JSON.stringify(prop)})`); - } - const typeName = type.name || "??"; - const v = target; - if (!v || typeof v !== "object") { - throw new SchemaError( - `expected object for ${path.join(".")}, got ${typeof v} instead`); - } - const chk = type.prototype[checkableInfoSym]; - const props = chk.props; - const remainingPropNames = new Set(Object.getOwnPropertyNames(v)); - const obj = new type(); - for (const innerProp of props) { - if (!remainingPropNames.has(innerProp.propertyKey)) { - if (innerProp.optional) { - continue; - } - throw new SchemaError(`Property '${innerProp.propertyKey}' missing on '${path}' of '${typeName}'`); - } - if (!remainingPropNames.delete(innerProp.propertyKey)) { - throw new SchemaError("assertion failed"); - } - const propVal = v[innerProp.propertyKey]; - obj[innerProp.propertyKey] = innerProp.checker(propVal, - innerProp, - path.concat([innerProp.propertyKey])); - } - - if (!chk.extraAllowed && remainingPropNames.size !== 0) { - const err = `superfluous properties ${JSON.stringify(Array.from(remainingPropNames.values()))} of ${typeName}`; - throw new SchemaError(err); - } - return obj; - } - - - /** - * Class with checkable annotations on fields. - * This annotation adds the implementation of the `checked` - * static method. - */ - export function Class(opts: {extra?: boolean, validate?: boolean} = {}) { - return (target: any) => { - const chk = getCheckableInfo(target.prototype); - chk.extraAllowed = !!opts.extra; - target.checked = (v: any) => { - const cv = checkValue(v, { - checker: checkValue, - propertyKey: "(root)", - type: target, - }, ["(root)"]); - if (opts.validate) { - if (typeof target.validate !== "function") { - throw Error("invalid Checkable annotion: validate method required"); - } - // May throw exception - target.validate(cv); - } - return cv; - }; - return target; - }; - } - - - /** - * Target property must be a Checkable object of the given type. - */ - export function Value(typeThunk: () => any) { - function deco(target: object, propertyKey: string | symbol): void { - const chk = getCheckableInfo(target); - chk.props.push({ - checker: checkValue, - propertyKey, - typeThunk, - }); - } - - return deco; - } - - - /** - * List of values that match the given annotation. For example, `@Checkable.List(Checkable.String)` is - * an annotation for a list of strings. - */ - export function List(type: any) { - const stub = {}; - type(stub, "(list-element)"); - const elementProp = getCheckableInfo(stub).props[0]; - const elementChecker = elementProp.checker; - if (!elementChecker) { - throw Error("assertion failed"); - } - function deco(target: object, propertyKey: string | symbol): void { - const chk = getCheckableInfo(target); - chk.props.push({ - checker: checkList, - elementChecker, - elementProp, - propertyKey, - }); - } - - return deco; - } - - - /** - * Map from the key type to value type. Takes two annotations, - * one for the key type and one for the value type. - */ - export function Map(keyType: any, valueType: any) { - const keyStub = {}; - keyType(keyStub, "(map-key)"); - const keyProp = getCheckableInfo(keyStub).props[0]; - if (!keyProp) { - throw Error("assertion failed"); - } - const valueStub = {}; - valueType(valueStub, "(map-value)"); - const valueProp = getCheckableInfo(valueStub).props[0]; - if (!valueProp) { - throw Error("assertion failed"); - } - function deco(target: object, propertyKey: string | symbol): void { - const chk = getCheckableInfo(target); - chk.props.push({ - checker: checkMap, - keyProp, - propertyKey, - valueProp, - }); - } - - return deco; - } - - - /** - * Makes another annotation optional, for example `@Checkable.Optional(Checkable.Number)`. - */ - export function Optional(type: (target: object, propertyKey: string | symbol) => void | any) { - const stub = {}; - type(stub, "(optional-element)"); - const elementProp = getCheckableInfo(stub).props[0]; - const elementChecker = elementProp.checker; - if (!elementChecker) { - throw Error("assertion failed"); - } - function deco(target: object, propertyKey: string | symbol): void { - const chk = getCheckableInfo(target); - chk.props.push({ - checker: checkOptional, - elementChecker, - elementProp, - optional: true, - propertyKey, - }); - } - - return deco; - } - - - /** - * Target property must be a number. - */ - 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 deco = (target: object, propertyKey: string | symbol) => { - const chk = getCheckableInfo(target); - chk.props.push({ - checker: checkAnyObject, - propertyKey, - }); - }; - return deco; - } - - - /** - * Target property can be anything. - * - * 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 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( - 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 deco = (target: object, propertyKey: string | symbol) => { - const chk = getCheckableInfo(target); - chk.props.push({ checker: checkBoolean, propertyKey }); - }; - return deco; - } -} diff --git a/src/util/codec.ts b/src/util/codec.ts index a13816c59..e18a5e74c 100644 --- a/src/util/codec.ts +++ b/src/util/codec.ts @@ -32,11 +32,11 @@ export class DecodingError extends Error { /** * Context information to show nicer error messages when decoding fails. */ -interface Context { +export interface Context { readonly path?: string[]; } -function renderContext(c?: Context): string { +export function renderContext(c?: Context): string { const p = c?.path; if (p) { return p.join("."); @@ -84,6 +84,9 @@ class ObjectCodecBuilder<OutputType, PartialOutputType> { x: K, codec: Codec<V>, ): ObjectCodecBuilder<OutputType, PartialOutputType & SingletonRecord<K, V>> { + if (!codec) { + throw Error("inner codec must be defined"); + } this.propList.push({ name: x, codec: codec }); return this as any; } @@ -143,6 +146,9 @@ class UnionCodecBuilder< CommonBaseType, PartialTargetType | V > { + if (!codec) { + throw Error("inner codec must be defined"); + } this.alternatives.set(tagValue, { codec, tagValue }); return this as any; } @@ -215,6 +221,9 @@ export function makeCodecForUnion<T>(): UnionCodecPreBuilder<T> { export function makeCodecForMap<T>( innerCodec: Codec<T>, ): Codec<{ [x: string]: T }> { + if (!innerCodec) { + throw Error("inner codec must be defined"); + } return { decode(x: any, c?: Context): { [x: string]: T } { const map: { [x: string]: T } = {}; @@ -233,6 +242,9 @@ export function makeCodecForMap<T>( * Return a codec for a list, containing values described by the inner codec. */ export function makeCodecForList<T>(innerCodec: Codec<T>): Codec<T[]> { + if (!innerCodec) { + throw Error("inner codec must be defined"); + } return { decode(x: any, c?: Context): T[] { const arr: T[] = []; @@ -255,7 +267,19 @@ export const codecForNumber: Codec<number> = { if (typeof x === "number") { return x; } - throw new DecodingError(`expected number at ${renderContext(c)}`); + throw new DecodingError(`expected number at ${renderContext(c)} but got ${typeof x}`); + }, +}; + +/** + * Return a codec for a value that must be a number. + */ +export const codecForBoolean: Codec<boolean> = { + decode(x: any, c?: Context): boolean { + if (typeof x === "boolean") { + return x; + } + throw new DecodingError(`expected boolean at ${renderContext(c)} but got ${typeof x}`); }, }; @@ -267,7 +291,16 @@ export const codecForString: Codec<string> = { if (typeof x === "string") { return x; } - throw new DecodingError(`expected string at ${renderContext(c)}`); + throw new DecodingError(`expected string at ${renderContext(c)} but got ${typeof x}`); + }, +}; + +/** + * Codec that allows any value. + */ +export const codecForAny: Codec<any> = { + decode(x: any, c?: Context): any { + return x; }, }; @@ -281,12 +314,23 @@ export function makeCodecForConstString<V extends string>(s: V): Codec<V> { return x; } throw new DecodingError( - `expected string constant "${s}" at ${renderContext(c)}`, + `expected string constant "${s}" at ${renderContext(c)} but got ${typeof x}`, ); }, }; } +export function makeCodecOptional<V>(innerCodec: Codec<V>): Codec<V | undefined> { + return { + decode(x: any, c?: Context): V | undefined { + if (x === undefined || x === null) { + return undefined; + } + return innerCodec.decode(x, c); + } + } +} + export function typecheckedCodec<T = undefined>(c: Codec<T>): Codec<T> { return c; } diff --git a/src/util/helpers.ts b/src/util/helpers.ts index 8136f44fa..722688d35 100644 --- a/src/util/helpers.ts +++ b/src/util/helpers.ts @@ -24,8 +24,6 @@ import { AmountJson } from "./amounts"; import * as Amounts from "./amounts"; -import { Timestamp, Duration } from "../types/walletTypes"; - /** * Show an amount in a form suitable for the user. * FIXME: In the future, this should consider currency-specific @@ -114,75 +112,6 @@ export function flatMap<T, U>(xs: T[], f: (x: T) => U[]): U[] { return xs.reduce((acc: U[], next: T) => [...f(next), ...acc], []); } - -/** - * Extract a numeric timstamp (in seconds) from the Taler date format - * ("/Date([n])/"). Returns null if input is not in the right format. - */ -export function getTalerStampSec(stamp: string): number | null { - const m = stamp.match(/\/?Date\(([0-9]*)\)\/?/); - if (!m || !m[1]) { - return null; - } - return parseInt(m[1], 10); -} - -/** - * Extract a timestamp from a Taler timestamp string. - */ -export function extractTalerStamp(stamp: string): Timestamp | undefined { - const m = stamp.match(/\/?Date\(([0-9]*)\)\/?/); - if (!m || !m[1]) { - return undefined; - } - return { - t_ms: parseInt(m[1], 10) * 1000, - }; -} - -/** - * Extract a timestamp from a Taler timestamp string. - */ -export function extractTalerStampOrThrow(stamp: string): Timestamp { - const r = extractTalerStamp(stamp); - if (!r) { - throw Error("invalid time stamp"); - } - return r; -} - -/** - * Extract a duration from a Taler duration string. - */ -export function extractTalerDuration(duration: string): Duration | undefined { - const m = duration.match(/\/?Delay\(([0-9]*)\)\/?/); - if (!m || !m[1]) { - return undefined; - } - return { - d_ms: parseInt(m[1], 10) * 1000, - }; -} - -/** - * Extract a duration from a Taler duration string. - */ -export function extractTalerDurationOrThrow(duration: string): Duration { - const r = extractTalerDuration(duration); - if (!r) { - throw Error("invalid duration"); - } - return r; -} - -/** - * Check if a timestamp is in the right format. - */ -export function timestampCheck(stamp: string): boolean { - return getTalerStampSec(stamp) !== null; -} - - /** * Compute the hash function of a JSON object. */ diff --git a/src/util/time.ts b/src/util/time.ts new file mode 100644 index 000000000..54d22bf81 --- /dev/null +++ b/src/util/time.ts @@ -0,0 +1,165 @@ +import { Codec, renderContext, Context } from "./codec"; + +/* + This file is part of GNU Taler + (C) 2017-2019 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/** + * Helpers for relative and absolute time. + */ + +export class Timestamp { + /** + * Timestamp in milliseconds. + */ + readonly t_ms: number | "never"; +} + +export interface Duration { + /** + * Duration in milliseconds. + */ + readonly d_ms: number | "forever"; +} + +export function getTimestampNow(): Timestamp { + return { + t_ms: new Date().getTime(), + }; +} + +export function getDurationRemaining( + deadline: Timestamp, + now = getTimestampNow(), +): Duration { + if (deadline.t_ms === "never") { + return { d_ms: "forever" }; + } + if (now.t_ms === "never") { + throw Error("invalid argument for 'now'"); + } + if (deadline.t_ms < now.t_ms) { + return { d_ms: 0 }; + } + return { d_ms: deadline.t_ms - now.t_ms }; +} + +export function timestampMin(t1: Timestamp, t2: Timestamp): Timestamp { + if (t1.t_ms === "never") { + return { t_ms: t2.t_ms }; + } + if (t2.t_ms === "never") { + return { t_ms: t2.t_ms }; + } + return { t_ms: Math.min(t1.t_ms, t2.t_ms) }; +} + +export function durationMin(d1: Duration, d2: Duration): Duration { + if (d1.d_ms === "forever") { + return { d_ms: d2.d_ms }; + } + if (d2.d_ms === "forever") { + return { d_ms: d2.d_ms }; + } + return { d_ms: Math.min(d1.d_ms, d2.d_ms) }; +} + +export function timestampCmp(t1: Timestamp, t2: Timestamp): number { + if (t1.t_ms === "never") { + if (t2.t_ms === "never") { + return 0; + } + return 1; + } + if (t2.t_ms === "never") { + return -1; + } + if (t1.t_ms == t2.t_ms) { + return 0; + } + if (t1.t_ms > t2.t_ms) { + return 1; + } + return -1; +} + +export function timestampAddDuration(t1: Timestamp, d: Duration): Timestamp { + if (t1.t_ms === "never" || d.d_ms === "forever") { + return { t_ms: "never" }; + } + return { t_ms: t1.t_ms + d.d_ms }; +} + +export function timestampSubtractDuraction( + t1: Timestamp, + d: Duration, +): Timestamp { + if (t1.t_ms === "never") { + return { t_ms: "never" }; + } + if (d.d_ms === "forever") { + return { t_ms: 0 }; + } + return { t_ms: Math.max(0, t1.t_ms - d.d_ms) }; +} + +export function stringifyTimestamp(t: Timestamp) { + if (t.t_ms === "never") { + return "never"; + } + return new Date(t.t_ms).toISOString(); +} + +export function timestampDifference(t1: Timestamp, t2: Timestamp): Duration { + if (t1.t_ms === "never") { + return { d_ms: "forever" }; + } + if (t2.t_ms === "never") { + return { d_ms: "forever" }; + } + return { d_ms: Math.abs(t1.t_ms - t2.t_ms) }; +} + +export const codecForTimestamp: Codec<Timestamp> = { + decode(x: any, c?: Context): Timestamp { + const t_ms = x.t_ms; + if (typeof t_ms === "string") { + if (t_ms === "never") { + return { t_ms: "never" }; + } + throw Error(`expected timestamp at ${renderContext(c)}`); + } + if (typeof t_ms === "number") { + return { t_ms }; + } + throw Error(`expected timestamp at ${renderContext(c)}`); + }, +}; + +export const codecForDuration: Codec<Duration> = { + decode(x: any, c?: Context): Duration { + const d_ms = x.d_ms; + if (typeof d_ms === "string") { + if (d_ms === "forever") { + return { d_ms: "forever" }; + } + throw Error(`expected duration at ${renderContext(c)}`); + } + if (typeof d_ms === "number") { + return { d_ms }; + } + throw Error(`expected duration at ${renderContext(c)}`); + }, +}; diff --git a/src/util/timer.ts b/src/util/timer.ts index 865c17faf..000f36608 100644 --- a/src/util/timer.ts +++ b/src/util/timer.ts @@ -1,17 +1,19 @@ +import { Duration } from "./time"; + /* - This file is part of TALER - (C) 2017 GNUnet e.V. + This file is part of GNU Taler + (C) 2017-2019 Taler Systems S.A. - TALER is free software; you can redistribute it and/or modify it under the + GNU Taler is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3, or (at your option) any later version. - TALER is distributed in the hope that it will be useful, but WITHOUT ANY + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with - TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ /** @@ -105,11 +107,13 @@ export class TimerGroup { } } - resolveAfter(delayMs: number): Promise<void> { + resolveAfter(delayMs: Duration): Promise<void> { return new Promise<void>((resolve, reject) => { - this.after(delayMs, () => { - resolve(); - }); + if (delayMs.d_ms !== "forever") { + this.after(delayMs.d_ms, () => { + resolve(); + }); + } }); } diff --git a/src/wallet.ts b/src/wallet.ts index 407318aa2..b08122b66 100644 --- a/src/wallet.ts +++ b/src/wallet.ts @@ -91,7 +91,6 @@ import { getHistory } from "./operations/history"; import { getPendingOperations } from "./operations/pending"; import { getBalances } from "./operations/balance"; import { acceptTip, getTipStatus, processTip } from "./operations/tip"; -import { returnCoins } from "./operations/return"; import { payback } from "./operations/payback"; import { TimerGroup } from "./util/timer"; import { AsyncCondition } from "./util/promiseUtils"; @@ -109,6 +108,7 @@ import { getFullRefundFees, applyRefund, } from "./operations/refund"; +import { durationMin, Duration } from "./util/time"; const builtinCurrencies: CurrencyRecord[] = [ @@ -289,15 +289,15 @@ export class Wallet { numGivingLiveness++; } } - let dt; + let dt: Duration; if ( allPending.pendingOperations.length === 0 || allPending.nextRetryDelay.d_ms === Number.MAX_SAFE_INTEGER ) { // Wait for 5 seconds - dt = 5000; + dt = { d_ms: 5000 }; } else { - dt = Math.min(5000, allPending.nextRetryDelay.d_ms); + dt = durationMin({ d_ms: 5000}, allPending.nextRetryDelay); } const timeout = this.timerGroup.resolveAfter(dt); this.ws.notify({ @@ -599,7 +599,7 @@ export class Wallet { * Trigger paying coins back into the user's account. */ async returnCoins(req: ReturnCoinsRequest): Promise<void> { - return returnCoins(this.ws, req); + throw Error("not implemented"); } /** @@ -708,7 +708,7 @@ export class Wallet { ]).amount; const totalFees = totalRefundFees; return { - contractTerms: purchase.contractTerms, + contractTerms: purchase.contractTermsRaw, hasRefund: purchase.timestampLastRefundStatus !== undefined, totalRefundAmount: totalRefundAmount, totalRefundAndRefreshFees: totalFees, diff --git a/src/webex/pages/pay.tsx b/src/webex/pages/pay.tsx index eca115e78..b7f09b6f6 100644 --- a/src/webex/pages/pay.tsx +++ b/src/webex/pages/pay.tsx @@ -74,7 +74,7 @@ function TalerPayDialog({ talerPayUri }: { talerPayUri: string }) { ); } - const contractTerms = payStatus.contractTerms; + const contractTerms = payStatus.contractTermsRaw; if (!contractTerms) { return ( diff --git a/src/webex/renderHtml.tsx b/src/webex/renderHtml.tsx index 767058ebf..3204c410d 100644 --- a/src/webex/renderHtml.tsx +++ b/src/webex/renderHtml.tsx @@ -31,6 +31,7 @@ import * as moment from "moment"; import * as i18n from "./i18n"; import React from "react"; import ReactDOM from "react-dom"; +import { stringifyTimestamp } from "../util/time"; /** * Render amount as HTML, which non-breaking space between @@ -215,7 +216,7 @@ function FeeDetailsView(props: { <tbody> {rci!.wireFees.feesForType[s].map(f => ( <tr> - <td>{moment.unix(Math.floor(f.endStamp.t_ms / 1000)).format("llll")}</td> + <td>{stringifyTimestamp(f.endStamp)}</td> <td>{renderAmount(f.wireFee)}</td> <td>{renderAmount(f.closingFee)}</td> </tr> @@ -239,9 +240,8 @@ function FeeDetailsView(props: { <p> {i18n.str`Rounding loss:`} {overhead} </p> - <p>{i18n.str`Earliest expiration (for deposit): ${moment - .unix(rci.earliestDepositExpiration.t_ms / 1000) - .fromNow()}`}</p> + <p>{i18n.str`Earliest expiration (for deposit): ${ + stringifyTimestamp(rci.earliestDepositExpiration)}`}</p> <h3>Coin Fees</h3> <div style={{ overflow: "auto" }}> <table className="pure-table"> diff --git a/src/webex/wxBackend.ts b/src/webex/wxBackend.ts index 97774a5c2..ae12f9f91 100644 --- a/src/webex/wxBackend.ts +++ b/src/webex/wxBackend.ts @@ -25,8 +25,8 @@ */ import { BrowserCryptoWorkerFactory } from "../crypto/workers/cryptoApi"; import { deleteTalerDatabase, openTalerDatabase, WALLET_DB_VERSION } from "../db"; -import { ConfirmReserveRequest, CreateReserveRequest, ReturnCoinsRequest, WalletDiagnostics } from "../types/walletTypes"; -import { AmountJson } from "../util/amounts"; +import { ConfirmReserveRequest, CreateReserveRequest, ReturnCoinsRequest, WalletDiagnostics, codecForCreateReserveRequest, codecForConfirmReserveRequest } from "../types/walletTypes"; +import { AmountJson, codecForAmountJson } from "../util/amounts"; import { BrowserHttpLib } from "../util/http"; import { OpenedPromise, openPromise } from "../util/promiseUtils"; import { classifyTalerUri, TalerUriType } from "../util/taleruri"; @@ -91,14 +91,14 @@ async function handleMessage( exchange: detail.exchange, senderWire: detail.senderWire, }; - const req = CreateReserveRequest.checked(d); + const req = codecForCreateReserveRequest().decode(d); return needsWallet().createReserve(req); } case "confirm-reserve": { const d = { reservePub: detail.reservePub, }; - const req = ConfirmReserveRequest.checked(d); + const req = codecForConfirmReserveRequest().decode(d); return needsWallet().confirmReserve(req); } case "confirm-pay": { @@ -117,7 +117,7 @@ async function handleMessage( if (!detail.baseUrl || typeof detail.baseUrl !== "string") { return Promise.resolve({ error: "bad url" }); } - const amount = AmountJson.checked(detail.amount); + const amount = codecForAmountJson().decode(detail.amount); return needsWallet().getWithdrawDetailsForAmount(detail.baseUrl, amount); } case "get-history": { diff --git a/tsconfig.json b/tsconfig.json index 4156247a2..ec15f8ddd 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -56,7 +56,6 @@ "src/operations/refresh.ts", "src/operations/refund.ts", "src/operations/reserves.ts", - "src/operations/return.ts", "src/operations/state.ts", "src/operations/tip.ts", "src/operations/versions.ts", @@ -75,7 +74,6 @@ "src/util/amounts.ts", "src/util/assertUnreachable.ts", "src/util/asyncMemo.ts", - "src/util/checkable.ts", "src/util/codec-test.ts", "src/util/codec.ts", "src/util/helpers-test.ts", @@ -90,6 +88,7 @@ "src/util/query.ts", "src/util/taleruri-test.ts", "src/util/taleruri.ts", + "src/util/time.ts", "src/util/timer.ts", "src/util/wire.ts", "src/wallet-test.ts", |