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 /src/operations | |
parent | 49e3b3e5b9bbf1ce356ef68f301d50c689ceecb9 (diff) |
new date format, replace checkable annotations with codecs
Diffstat (limited to 'src/operations')
-rw-r--r-- | src/operations/exchanges.ts | 37 | ||||
-rw-r--r-- | src/operations/history.ts | 15 | ||||
-rw-r--r-- | src/operations/pay.ts | 212 | ||||
-rw-r--r-- | src/operations/payback.ts | 4 | ||||
-rw-r--r-- | src/operations/pending.ts | 10 | ||||
-rw-r--r-- | src/operations/refresh.ts | 2 | ||||
-rw-r--r-- | src/operations/refund.ts | 15 | ||||
-rw-r--r-- | src/operations/reserves.ts | 9 | ||||
-rw-r--r-- | src/operations/return.ts | 271 | ||||
-rw-r--r-- | src/operations/tip.ts | 15 | ||||
-rw-r--r-- | src/operations/withdraw.ts | 92 |
11 files changed, 208 insertions, 474 deletions
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; } } |