new date format, replace checkable annotations with codecs

This commit is contained in:
Florian Dold 2019-12-19 20:42:49 +01:00
parent 49e3b3e5b9
commit 0c9358c1b2
No known key found for this signature in database
GPG Key ID: D2E4F00F29D02A4B
35 changed files with 908 additions and 1484 deletions

View File

@ -30,6 +30,7 @@ import {
RefreshSessionRecord, RefreshSessionRecord,
TipPlanchet, TipPlanchet,
WireFee, WireFee,
WalletContractData,
} from "../../types/dbTypes"; } from "../../types/dbTypes";
import { CryptoWorker } from "./cryptoWorker"; import { CryptoWorker } from "./cryptoWorker";
@ -384,14 +385,16 @@ export class CryptoApi {
} }
signDeposit( signDeposit(
contractTerms: ContractTerms, contractTermsRaw: string,
contractData: WalletContractData,
cds: CoinWithDenom[], cds: CoinWithDenom[],
totalAmount: AmountJson, totalAmount: AmountJson,
): Promise<PaySigInfo> { ): Promise<PaySigInfo> {
return this.doRpc<PaySigInfo>( return this.doRpc<PaySigInfo>(
"signDeposit", "signDeposit",
3, 3,
contractTerms, contractTermsRaw,
contractData,
cds, cds,
totalAmount, totalAmount,
); );

View File

@ -33,6 +33,7 @@ import {
TipPlanchet, TipPlanchet,
WireFee, WireFee,
initRetryInfo, initRetryInfo,
WalletContractData,
} from "../../types/dbTypes"; } from "../../types/dbTypes";
import { CoinPaySig, ContractTerms, PaybackRequest } from "../../types/talerTypes"; import { CoinPaySig, ContractTerms, PaybackRequest } from "../../types/talerTypes";
@ -40,13 +41,11 @@ import {
BenchmarkResult, BenchmarkResult,
CoinWithDenom, CoinWithDenom,
PaySigInfo, PaySigInfo,
Timestamp,
PlanchetCreationResult, PlanchetCreationResult,
PlanchetCreationRequest, PlanchetCreationRequest,
getTimestampNow,
CoinPayInfo, CoinPayInfo,
} from "../../types/walletTypes"; } from "../../types/walletTypes";
import { canonicalJson, getTalerStampSec } from "../../util/helpers"; import { canonicalJson } from "../../util/helpers";
import { AmountJson } from "../../util/amounts"; import { AmountJson } from "../../util/amounts";
import * as Amounts from "../../util/amounts"; import * as Amounts from "../../util/amounts";
import * as timer from "../../util/timer"; import * as timer from "../../util/timer";
@ -70,6 +69,7 @@ import {
} from "../talerCrypto"; } from "../talerCrypto";
import { randomBytes } from "../primitives/nacl-fast"; import { randomBytes } from "../primitives/nacl-fast";
import { kdf } from "../primitives/kdf"; import { kdf } from "../primitives/kdf";
import { Timestamp, getTimestampNow } from "../../util/time";
enum SignaturePurpose { enum SignaturePurpose {
RESERVE_WITHDRAW = 1200, RESERVE_WITHDRAW = 1200,
@ -104,20 +104,6 @@ function timestampToBuffer(ts: Timestamp): Uint8Array {
v.setBigUint64(0, s); v.setBigUint64(0, s);
return new Uint8Array(b); 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 { class SignaturePurposeBuilder {
private chunks: Uint8Array[] = []; private chunks: Uint8Array[] = [];
@ -346,7 +332,8 @@ export class CryptoImplementation {
* and deposit permissions for each given coin. * and deposit permissions for each given coin.
*/ */
signDeposit( signDeposit(
contractTerms: ContractTerms, contractTermsRaw: string,
contractData: WalletContractData,
cds: CoinWithDenom[], cds: CoinWithDenom[],
totalAmount: AmountJson, totalAmount: AmountJson,
): PaySigInfo { ): PaySigInfo {
@ -354,14 +341,13 @@ export class CryptoImplementation {
coinInfo: [], coinInfo: [],
}; };
const contractTermsHash = this.hashString(canonicalJson(contractTerms)); const contractTermsHash = this.hashString(canonicalJson(JSON.parse(contractTermsRaw)));
const feeList: AmountJson[] = cds.map(x => x.denom.feeDeposit); const feeList: AmountJson[] = cds.map(x => x.denom.feeDeposit);
let fees = Amounts.add(Amounts.getZero(feeList[0].currency), ...feeList) let fees = Amounts.add(Amounts.getZero(feeList[0].currency), ...feeList)
.amount; .amount;
// okay if saturates // okay if saturates
fees = Amounts.sub(fees, Amounts.parseOrThrow(contractTerms.max_fee)) fees = Amounts.sub(fees, contractData.maxDepositFee).amount;
.amount;
const total = Amounts.add(fees, totalAmount).amount; const total = Amounts.add(fees, totalAmount).amount;
let amountSpent = Amounts.getZero(cds[0].coin.currentAmount.currency); let amountSpent = Amounts.getZero(cds[0].coin.currentAmount.currency);
@ -395,12 +381,12 @@ export class CryptoImplementation {
const d = buildSigPS(SignaturePurpose.WALLET_COIN_DEPOSIT) const d = buildSigPS(SignaturePurpose.WALLET_COIN_DEPOSIT)
.put(decodeCrock(contractTermsHash)) .put(decodeCrock(contractTermsHash))
.put(decodeCrock(contractTerms.H_wire)) .put(decodeCrock(contractData.wireInfoHash))
.put(talerTimestampStringToBuffer(contractTerms.timestamp)) .put(timestampToBuffer(contractData.timestamp))
.put(talerTimestampStringToBuffer(contractTerms.refund_deadline)) .put(timestampToBuffer(contractData.refundDeadline))
.put(amountToBuffer(coinSpend)) .put(amountToBuffer(coinSpend))
.put(amountToBuffer(cd.denom.feeDeposit)) .put(amountToBuffer(cd.denom.feeDeposit))
.put(decodeCrock(contractTerms.merchant_pub)) .put(decodeCrock(contractData.merchantPub))
.put(decodeCrock(cd.coin.coinPub)) .put(decodeCrock(cd.coin.coinPub))
.build(); .build();
const coinSig = eddsaSign(d, decodeCrock(cd.coin.coinPriv)); const coinSig = eddsaSign(d, decodeCrock(cd.coin.coinPriv));

View File

@ -23,7 +23,7 @@
* Imports. * Imports.
*/ */
import axios from "axios"; import axios from "axios";
import { CheckPaymentResponse } from "../types/talerTypes"; import { CheckPaymentResponse, codecForCheckPaymentResponse } from "../types/talerTypes";
/** /**
* Connection to the *internal* merchant backend. * Connection to the *internal* merchant backend.
@ -96,8 +96,8 @@ export class MerchantBackendConnection {
amount, amount,
summary, summary,
fulfillment_url: fulfillmentUrl, fulfillment_url: fulfillmentUrl,
refund_deadline: `/Date(${t})/`, refund_deadline: { t_ms: t * 1000 },
wire_transfer_deadline: `/Date(${t})/`, wire_transfer_deadline: { t_ms: t * 1000 },
}, },
}; };
const resp = await axios({ const resp = await axios({
@ -133,6 +133,7 @@ export class MerchantBackendConnection {
if (resp.status != 200) { if (resp.status != 200) {
throw Error("failed to check payment"); throw Error("failed to check payment");
} }
return CheckPaymentResponse.checked(resp.data);
return codecForCheckPaymentResponse().decode(resp.data);
} }
} }

View File

@ -50,7 +50,7 @@ async function doPay(
return; return;
} }
if (result.status === "insufficient-balance") { if (result.status === "insufficient-balance") {
console.log("contract", result.contractTerms!); console.log("contract", result.contractTermsRaw);
console.error("insufficient balance"); console.error("insufficient balance");
process.exit(1); process.exit(1);
return; return;
@ -65,7 +65,7 @@ async function doPay(
} else { } else {
throw Error("not reached"); throw Error("not reached");
} }
console.log("contract", result.contractTerms!); console.log("contract", result.contractTermsRaw);
let pay; let pay;
if (options.alwaysYes) { if (options.alwaysYes) {
pay = true; pay = true;

View File

@ -15,8 +15,8 @@
*/ */
import { InternalWalletState } from "./state"; import { InternalWalletState } from "./state";
import { KeysJson, Denomination, ExchangeWireJson } from "../types/talerTypes"; import { ExchangeKeysJson, Denomination, ExchangeWireJson, codecForExchangeKeysJson, codecForExchangeWireJson } from "../types/talerTypes";
import { getTimestampNow, OperationError } from "../types/walletTypes"; import { OperationError } from "../types/walletTypes";
import { import {
ExchangeRecord, ExchangeRecord,
ExchangeUpdateStatus, ExchangeUpdateStatus,
@ -29,8 +29,6 @@ import {
} from "../types/dbTypes"; } from "../types/dbTypes";
import { import {
canonicalizeBaseUrl, canonicalizeBaseUrl,
extractTalerStamp,
extractTalerStampOrThrow,
} from "../util/helpers"; } from "../util/helpers";
import { Database } from "../util/query"; import { Database } from "../util/query";
import * as Amounts from "../util/amounts"; import * as Amounts from "../util/amounts";
@ -40,6 +38,7 @@ import {
guardOperationException, guardOperationException,
} from "./errors"; } from "./errors";
import { WALLET_CACHE_BREAKER_CLIENT_VERSION } from "./versions"; import { WALLET_CACHE_BREAKER_CLIENT_VERSION } from "./versions";
import { getTimestampNow } from "../util/time";
async function denominationRecordFromKeys( async function denominationRecordFromKeys(
ws: InternalWalletState, ws: InternalWalletState,
@ -57,12 +56,10 @@ async function denominationRecordFromKeys(
feeWithdraw: Amounts.parseOrThrow(denomIn.fee_withdraw), feeWithdraw: Amounts.parseOrThrow(denomIn.fee_withdraw),
isOffered: true, isOffered: true,
masterSig: denomIn.master_sig, masterSig: denomIn.master_sig,
stampExpireDeposit: extractTalerStampOrThrow(denomIn.stamp_expire_deposit), stampExpireDeposit: denomIn.stamp_expire_deposit,
stampExpireLegal: extractTalerStampOrThrow(denomIn.stamp_expire_legal), stampExpireLegal: denomIn.stamp_expire_legal,
stampExpireWithdraw: extractTalerStampOrThrow( stampExpireWithdraw: denomIn.stamp_expire_withdraw,
denomIn.stamp_expire_withdraw, stampStart: denomIn.stamp_start,
),
stampStart: extractTalerStampOrThrow(denomIn.stamp_start),
status: DenominationStatus.Unverified, status: DenominationStatus.Unverified,
value: Amounts.parseOrThrow(denomIn.value), value: Amounts.parseOrThrow(denomIn.value),
}; };
@ -117,9 +114,9 @@ async function updateExchangeWithKeys(
}); });
throw new OperationFailedAndReportedError(m); throw new OperationFailedAndReportedError(m);
} }
let exchangeKeysJson: KeysJson; let exchangeKeysJson: ExchangeKeysJson;
try { try {
exchangeKeysJson = KeysJson.checked(keysResp); exchangeKeysJson = codecForExchangeKeysJson().decode(keysResp);
} catch (e) { } catch (e) {
const m = `Parsing /keys response failed: ${e.message}`; const m = `Parsing /keys response failed: ${e.message}`;
await setExchangeError(ws, baseUrl, { await setExchangeError(ws, baseUrl, {
@ -130,9 +127,7 @@ async function updateExchangeWithKeys(
throw new OperationFailedAndReportedError(m); throw new OperationFailedAndReportedError(m);
} }
const lastUpdateTimestamp = extractTalerStamp( const lastUpdateTimestamp = exchangeKeysJson.list_issue_date
exchangeKeysJson.list_issue_date,
);
if (!lastUpdateTimestamp) { if (!lastUpdateTimestamp) {
const m = `Parsing /keys response failed: invalid list_issue_date.`; const m = `Parsing /keys response failed: invalid list_issue_date.`;
await setExchangeError(ws, baseUrl, { await setExchangeError(ws, baseUrl, {
@ -329,7 +324,7 @@ async function updateExchangeWithWireInfo(
if (!wiJson) { if (!wiJson) {
throw Error("/wire response malformed"); throw Error("/wire response malformed");
} }
const wireInfo = ExchangeWireJson.checked(wiJson); const wireInfo = codecForExchangeWireJson().decode(wiJson);
for (const a of wireInfo.accounts) { for (const a of wireInfo.accounts) {
console.log("validating exchange acct"); console.log("validating exchange acct");
const isValid = await ws.cryptoApi.isValidWireAccount( const isValid = await ws.cryptoApi.isValidWireAccount(
@ -345,14 +340,8 @@ async function updateExchangeWithWireInfo(
for (const wireMethod of Object.keys(wireInfo.fees)) { for (const wireMethod of Object.keys(wireInfo.fees)) {
const feeList: WireFee[] = []; const feeList: WireFee[] = [];
for (const x of wireInfo.fees[wireMethod]) { for (const x of wireInfo.fees[wireMethod]) {
const startStamp = extractTalerStamp(x.start_date); const startStamp = x.start_date;
if (!startStamp) { const endStamp = x.end_date;
throw Error("wrong date format");
}
const endStamp = extractTalerStamp(x.end_date);
if (!endStamp) {
throw Error("wrong date format");
}
const fee: WireFee = { const fee: WireFee = {
closingFee: Amounts.parseOrThrow(x.closing_fee), closingFee: Amounts.parseOrThrow(x.closing_fee),
endStamp, endStamp,

View File

@ -37,6 +37,7 @@ import {
import { assertUnreachable } from "../util/assertUnreachable"; import { assertUnreachable } from "../util/assertUnreachable";
import { TransactionHandle, Store } from "../util/query"; import { TransactionHandle, Store } from "../util/query";
import { ReserveTransactionType } from "../types/ReserveTransaction"; import { ReserveTransactionType } from "../types/ReserveTransaction";
import { timestampCmp } from "../util/time";
/** /**
* Create an event ID from the type and the primary key for the event. * Create an event ID from the type and the primary key for the event.
@ -53,11 +54,11 @@ function getOrderShortInfo(
return undefined; return undefined;
} }
return { return {
amount: download.contractTerms.amount, amount: Amounts.toString(download.contractData.amount),
orderId: download.contractTerms.order_id, orderId: download.contractData.orderId,
merchantBaseUrl: download.contractTerms.merchant_base_url, merchantBaseUrl: download.contractData.merchantBaseUrl,
proposalId: proposal.proposalId, proposalId: proposal.proposalId,
summary: download.contractTerms.summary || "", summary: download.contractData.summary,
}; };
} }
@ -356,9 +357,7 @@ export async function getHistory(
if (!orderShortInfo) { if (!orderShortInfo) {
return; return;
} }
const purchaseAmount = Amounts.parseOrThrow( const purchaseAmount = purchase.contractData.amount;
purchase.contractTerms.amount,
);
let amountRefundedRaw = Amounts.getZero(purchaseAmount.currency); let amountRefundedRaw = Amounts.getZero(purchaseAmount.currency);
let amountRefundedInvalid = Amounts.getZero(purchaseAmount.currency); let amountRefundedInvalid = Amounts.getZero(purchaseAmount.currency);
let amountRefundedEffective = 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 }; return { history };
} }

View File

@ -37,6 +37,7 @@ import {
Stores, Stores,
updateRetryInfoTimeout, updateRetryInfoTimeout,
PayEventRecord, PayEventRecord,
WalletContractData,
} from "../types/dbTypes"; } from "../types/dbTypes";
import { NotificationType } from "../types/notifications"; import { NotificationType } from "../types/notifications";
import { import {
@ -46,33 +47,29 @@ import {
MerchantRefundResponse, MerchantRefundResponse,
PayReq, PayReq,
Proposal, Proposal,
codecForMerchantRefundResponse,
codecForProposal,
codecForContractTerms,
} from "../types/talerTypes"; } from "../types/talerTypes";
import { import {
CoinSelectionResult, CoinSelectionResult,
CoinWithDenom, CoinWithDenom,
ConfirmPayResult, ConfirmPayResult,
getTimestampNow,
OperationError, OperationError,
PaySigInfo, PaySigInfo,
PreparePayResult, PreparePayResult,
RefreshReason, RefreshReason,
Timestamp,
} from "../types/walletTypes"; } from "../types/walletTypes";
import * as Amounts from "../util/amounts"; import * as Amounts from "../util/amounts";
import { AmountJson } from "../util/amounts"; import { AmountJson } from "../util/amounts";
import { import { amountToPretty, canonicalJson, strcmp } from "../util/helpers";
amountToPretty,
canonicalJson,
extractTalerDuration,
extractTalerStampOrThrow,
strcmp,
} from "../util/helpers";
import { Logger } from "../util/logging"; import { Logger } from "../util/logging";
import { getOrderDownloadUrl, parsePayUri } from "../util/taleruri"; import { getOrderDownloadUrl, parsePayUri } from "../util/taleruri";
import { guardOperationException } from "./errors"; import { guardOperationException } from "./errors";
import { createRefreshGroup, getTotalRefreshCost } from "./refresh"; import { createRefreshGroup, getTotalRefreshCost } from "./refresh";
import { acceptRefundResponse } from "./refund"; import { acceptRefundResponse } from "./refund";
import { InternalWalletState } from "./state"; import { InternalWalletState } from "./state";
import { Timestamp, getTimestampNow, timestampAddDuration } from "../util/time";
interface CoinsForPaymentArgs { interface CoinsForPaymentArgs {
allowedAuditors: Auditor[]; allowedAuditors: Auditor[];
@ -177,20 +174,20 @@ export function selectPayCoins(
*/ */
async function getCoinsForPayment( async function getCoinsForPayment(
ws: InternalWalletState, ws: InternalWalletState,
args: CoinsForPaymentArgs, args: WalletContractData,
): Promise<CoinSelectionResult | undefined> { ): Promise<CoinSelectionResult | undefined> {
const { const {
allowedAuditors, allowedAuditors,
allowedExchanges, allowedExchanges,
depositFeeLimit, maxDepositFee,
paymentAmount, amount,
wireFeeAmortization, wireFeeAmortization,
wireFeeLimit, maxWireFee,
wireFeeTime, timestamp,
wireMethod, wireMethod,
} = args; } = args;
let remainingAmount = paymentAmount; let remainingAmount = amount;
const exchanges = await ws.db.iter(Stores.exchanges).toArray(); const exchanges = await ws.db.iter(Stores.exchanges).toArray();
@ -207,7 +204,7 @@ async function getCoinsForPayment(
// is the exchange explicitly allowed? // is the exchange explicitly allowed?
for (const allowedExchange of allowedExchanges) { for (const allowedExchange of allowedExchanges) {
if (allowedExchange.master_pub === exchangeDetails.masterPublicKey) { if (allowedExchange.exchangePub === exchangeDetails.masterPublicKey) {
isOkay = true; isOkay = true;
break; break;
} }
@ -217,7 +214,7 @@ async function getCoinsForPayment(
if (!isOkay) { if (!isOkay) {
for (const allowedAuditor of allowedAuditors) { for (const allowedAuditor of allowedAuditors) {
for (const auditor of exchangeDetails.auditors) { for (const auditor of exchangeDetails.auditors) {
if (auditor.auditor_pub === allowedAuditor.auditor_pub) { if (auditor.auditor_pub === allowedAuditor.auditorPub) {
isOkay = true; isOkay = true;
break; break;
} }
@ -281,7 +278,7 @@ async function getCoinsForPayment(
let totalFees = Amounts.getZero(currency); let totalFees = Amounts.getZero(currency);
let wireFee: AmountJson | undefined; let wireFee: AmountJson | undefined;
for (const fee of exchangeFees.feesForType[wireMethod] || []) { for (const fee of exchangeFees.feesForType[wireMethod] || []) {
if (fee.startStamp <= wireFeeTime && fee.endStamp >= wireFeeTime) { if (fee.startStamp <= timestamp && fee.endStamp >= timestamp) {
wireFee = fee.wireFee; wireFee = fee.wireFee;
break; break;
} }
@ -289,13 +286,13 @@ async function getCoinsForPayment(
if (wireFee) { if (wireFee) {
const amortizedWireFee = Amounts.divide(wireFee, wireFeeAmortization); const amortizedWireFee = Amounts.divide(wireFee, wireFeeAmortization);
if (Amounts.cmp(wireFeeLimit, amortizedWireFee) < 0) { if (Amounts.cmp(maxWireFee, amortizedWireFee) < 0) {
totalFees = Amounts.add(amortizedWireFee, totalFees).amount; totalFees = Amounts.add(amortizedWireFee, totalFees).amount;
remainingAmount = Amounts.add(amortizedWireFee, remainingAmount).amount; remainingAmount = Amounts.add(amortizedWireFee, remainingAmount).amount;
} }
} }
const res = selectPayCoins(denoms, cds, remainingAmount, depositFeeLimit); const res = selectPayCoins(denoms, cds, remainingAmount, maxDepositFee);
if (res) { if (res) {
totalFees = Amounts.add(totalFees, res.totalFees).amount; totalFees = Amounts.add(totalFees, res.totalFees).amount;
@ -332,18 +329,17 @@ async function recordConfirmPay(
} }
logger.trace(`recording payment with session ID ${sessionId}`); logger.trace(`recording payment with session ID ${sessionId}`);
const payReq: PayReq = { const payReq: PayReq = {
coins: payCoinInfo.coinInfo.map((x) => x.sig), coins: payCoinInfo.coinInfo.map(x => x.sig),
merchant_pub: d.contractTerms.merchant_pub, merchant_pub: d.contractData.merchantPub,
mode: "pay", mode: "pay",
order_id: d.contractTerms.order_id, order_id: d.contractData.orderId,
}; };
const t: PurchaseRecord = { const t: PurchaseRecord = {
abortDone: false, abortDone: false,
abortRequested: false, abortRequested: false,
contractTerms: d.contractTerms, contractTermsRaw: d.contractTermsRaw,
contractTermsHash: d.contractTermsHash, contractData: d.contractData,
lastSessionId: sessionId, lastSessionId: sessionId,
merchantSig: d.merchantSig,
payReq, payReq,
timestampAccept: getTimestampNow(), timestampAccept: getTimestampNow(),
timestampLastRefundStatus: undefined, timestampLastRefundStatus: undefined,
@ -383,14 +379,19 @@ async function recordConfirmPay(
throw Error("coin allocated for payment doesn't exist anymore"); throw Error("coin allocated for payment doesn't exist anymore");
} }
coin.status = CoinStatus.Dormant; coin.status = CoinStatus.Dormant;
const remaining = Amounts.sub(coin.currentAmount, coinInfo.subtractedAmount); const remaining = Amounts.sub(
coin.currentAmount,
coinInfo.subtractedAmount,
);
if (remaining.saturated) { if (remaining.saturated) {
throw Error("not enough remaining balance on coin for payment"); throw Error("not enough remaining balance on coin for payment");
} }
coin.currentAmount = remaining.amount; coin.currentAmount = remaining.amount;
await tx.put(Stores.coins, coin); 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); await createRefreshGroup(tx, refreshCoinPubs, RefreshReason.Pay);
}, },
); );
@ -402,11 +403,11 @@ async function recordConfirmPay(
return t; return t;
} }
function getNextUrl(contractTerms: ContractTerms): string { function getNextUrl(contractData: WalletContractData): string {
const f = contractTerms.fulfillment_url; const f = contractData.fulfillmentUrl;
if (f.startsWith("http://") || f.startsWith("https://")) { if (f.startsWith("http://") || f.startsWith("https://")) {
const fu = new URL(contractTerms.fulfillment_url); const fu = new URL(contractData.fulfillmentUrl);
fu.searchParams.set("order_id", contractTerms.order_id); fu.searchParams.set("order_id", contractData.orderId);
return fu.href; return fu.href;
} else { } else {
return f; return f;
@ -440,7 +441,7 @@ export async function abortFailedPayment(
const abortReq = { ...purchase.payReq, mode: "abort-refund" }; 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 { try {
resp = await ws.http.postJson(payUrl, abortReq); resp = await ws.http.postJson(payUrl, abortReq);
@ -454,7 +455,9 @@ export async function abortFailedPayment(
throw Error(`unexpected status for /pay (${resp.status})`); throw Error(`unexpected status for /pay (${resp.status})`);
} }
const refundResponse = MerchantRefundResponse.checked(await resp.json()); const refundResponse = codecForMerchantRefundResponse().decode(
await resp.json(),
);
await acceptRefundResponse( await acceptRefundResponse(
ws, ws,
purchase.proposalId, purchase.proposalId,
@ -574,13 +577,16 @@ async function processDownloadProposalImpl(
throw Error(`contract download failed with status ${resp.status}`); 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( const contractTermsHash = await ws.cryptoApi.hashString(
canonicalJson(proposalResp.contract_terms), 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( await ws.db.runWithWriteTransaction(
[Stores.proposals, Stores.purchases], [Stores.proposals, Stores.purchases],
@ -592,10 +598,42 @@ async function processDownloadProposalImpl(
if (p.proposalStatus !== ProposalStatus.DOWNLOADING) { if (p.proposalStatus !== ProposalStatus.DOWNLOADING) {
return; 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 = { p.download = {
contractTerms: proposalResp.contract_terms, contractData: {
amount,
contractTermsHash: contractTermsHash,
fulfillmentUrl: parsedContractTerms.fulfillment_url,
merchantBaseUrl: parsedContractTerms.merchant_base_url,
merchantPub: parsedContractTerms.merchant_pub,
merchantSig: proposalResp.sig, merchantSig: proposalResp.sig,
contractTermsHash, 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 ( if (
fulfillmentUrl.startsWith("http://") || fulfillmentUrl.startsWith("http://") ||
@ -697,7 +735,7 @@ export async function submitPay(
console.log("paying with session ID", sessionId); 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 { try {
resp = await ws.http.postJson(payUrl, payReq); resp = await ws.http.postJson(payUrl, payReq);
@ -714,10 +752,10 @@ export async function submitPay(
const now = getTimestampNow(); const now = getTimestampNow();
const merchantPub = purchase.contractTerms.merchant_pub; const merchantPub = purchase.contractData.merchantPub;
const valid: boolean = await ws.cryptoApi.isValidPaymentSignature( const valid: boolean = await ws.cryptoApi.isValidPaymentSignature(
merchantResp.sig, merchantResp.sig,
purchase.contractTermsHash, purchase.contractData.contractTermsHash,
merchantPub, merchantPub,
); );
if (!valid) { if (!valid) {
@ -731,19 +769,13 @@ export async function submitPay(
purchase.lastPayError = undefined; purchase.lastPayError = undefined;
purchase.payRetryInfo = initRetryInfo(false); purchase.payRetryInfo = initRetryInfo(false);
if (isFirst) { if (isFirst) {
const ar = purchase.contractTerms.auto_refund; const ar = purchase.contractData.autoRefund;
if (ar) { if (ar) {
console.log("auto_refund present"); console.log("auto_refund present");
const autoRefundDelay = extractTalerDuration(ar);
console.log("auto_refund valid", autoRefundDelay);
if (autoRefundDelay) {
purchase.refundStatusRequested = true; purchase.refundStatusRequested = true;
purchase.refundStatusRetryInfo = initRetryInfo(); purchase.refundStatusRetryInfo = initRetryInfo();
purchase.lastRefundStatusError = undefined; purchase.lastRefundStatusError = undefined;
purchase.autoRefundDeadline = { purchase.autoRefundDeadline = timestampAddDuration(now, ar);
t_ms: now.t_ms + autoRefundDelay.d_ms,
};
}
} }
} }
@ -761,8 +793,8 @@ export async function submitPay(
}, },
); );
const nextUrl = getNextUrl(purchase.contractTerms); const nextUrl = getNextUrl(purchase.contractData);
ws.cachedNextUrl[purchase.contractTerms.fulfillment_url] = { ws.cachedNextUrl[purchase.contractData.fulfillmentUrl] = {
nextUrl, nextUrl,
lastSessionId: sessionId, lastSessionId: sessionId,
}; };
@ -816,9 +848,9 @@ export async function preparePay(
console.error("bad proposal", proposal); console.error("bad proposal", proposal);
throw Error("proposal is in invalid state"); throw Error("proposal is in invalid state");
} }
const contractTerms = d.contractTerms; const contractData = d.contractData;
const merchantSig = d.merchantSig; const merchantSig = d.contractData.merchantSig;
if (!contractTerms || !merchantSig) { if (!merchantSig) {
throw Error("BUG: proposal is in invalid state"); 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); const purchase = await ws.db.get(Stores.purchases, proposalId);
if (!purchase) { if (!purchase) {
const paymentAmount = Amounts.parseOrThrow(contractTerms.amount); // If not already paid, check if we could pay for it.
let wireFeeLimit; const res = await getCoinsForPayment(ws, contractData);
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 (!res) { if (!res) {
console.log("not confirming payment, insufficient coins"); console.log("not confirming payment, insufficient coins");
return { return {
status: "insufficient-balance", status: "insufficient-balance",
contractTerms: contractTerms, contractTermsRaw: d.contractTermsRaw,
proposalId: proposal.proposalId, proposalId: proposal.proposalId,
}; };
} }
return { return {
status: "payment-possible", status: "payment-possible",
contractTerms: contractTerms, contractTermsRaw: d.contractTermsRaw,
proposalId: proposal.proposalId, proposalId: proposal.proposalId,
totalFees: res.totalFees, totalFees: res.totalFees,
}; };
} }
if (uriResult.sessionId && purchase.lastSessionId !== uriResult.sessionId) { if (uriResult.sessionId && purchase.lastSessionId !== uriResult.sessionId) {
console.log("automatically re-submitting payment with different session ID") console.log(
await ws.db.runWithWriteTransaction([Stores.purchases], async (tx) => { "automatically re-submitting payment with different session ID",
);
await ws.db.runWithWriteTransaction([Stores.purchases], async tx => {
const p = await tx.get(Stores.purchases, proposalId); const p = await tx.get(Stores.purchases, proposalId);
if (!p) { if (!p) {
return; return;
@ -879,8 +897,8 @@ export async function preparePay(
return { return {
status: "paid", status: "paid",
contractTerms: purchase.contractTerms, contractTermsRaw: purchase.contractTermsRaw,
nextUrl: getNextUrl(purchase.contractTerms), nextUrl: getNextUrl(purchase.contractData),
}; };
} }
@ -906,7 +924,10 @@ export async function confirmPay(
throw Error("proposal is in invalid state"); 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 (purchase) {
if ( if (
@ -926,25 +947,7 @@ export async function confirmPay(
logger.trace("confirmPay: purchase record does not exist yet"); logger.trace("confirmPay: purchase record does not exist yet");
const contractAmount = Amounts.parseOrThrow(d.contractTerms.amount); const res = await getCoinsForPayment(ws, d.contractData);
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,
});
logger.trace("coin selection result", res); logger.trace("coin selection result", res);
@ -956,7 +959,8 @@ export async function confirmPay(
const { cds, totalAmount } = res; const { cds, totalAmount } = res;
const payCoinInfo = await ws.cryptoApi.signDeposit( const payCoinInfo = await ws.cryptoApi.signDeposit(
d.contractTerms, d.contractTermsRaw,
d.contractData,
cds, cds,
totalAmount, totalAmount,
); );
@ -964,7 +968,7 @@ export async function confirmPay(
ws, ws,
proposal, proposal,
payCoinInfo, payCoinInfo,
sessionIdOverride sessionIdOverride,
); );
logger.trace("confirmPay: submitting payment after creating purchase record"); logger.trace("confirmPay: submitting payment after creating purchase record");

View File

@ -24,7 +24,7 @@ import { InternalWalletState } from "./state";
import { Stores, TipRecord, CoinStatus } from "../types/dbTypes"; import { Stores, TipRecord, CoinStatus } from "../types/dbTypes";
import { Logger } from "../util/logging"; import { Logger } from "../util/logging";
import { PaybackConfirmation } from "../types/talerTypes"; import { RecoupConfirmation, codecForRecoupConfirmation } from "../types/talerTypes";
import { updateExchangeFromUrl } from "./exchanges"; import { updateExchangeFromUrl } from "./exchanges";
import { NotificationType } from "../types/notifications"; import { NotificationType } from "../types/notifications";
@ -72,7 +72,7 @@ export async function payback(
if (resp.status !== 200) { if (resp.status !== 200) {
throw Error(); throw Error();
} }
const paybackConfirmation = PaybackConfirmation.checked(await resp.json()); const paybackConfirmation = codecForRecoupConfirmation().decode(await resp.json());
if (paybackConfirmation.reserve_pub !== coin.reservePub) { if (paybackConfirmation.reserve_pub !== coin.reservePub) {
throw Error(`Coin's reserve doesn't match reserve on payback`); throw Error(`Coin's reserve doesn't match reserve on payback`);
} }

View File

@ -27,7 +27,7 @@ import {
PendingOperationsResponse, PendingOperationsResponse,
PendingOperationType, PendingOperationType,
} from "../types/pending"; } 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 { TransactionHandle } from "../util/query";
import { InternalWalletState } from "./state"; import { InternalWalletState } from "./state";
@ -36,10 +36,8 @@ function updateRetryDelay(
now: Timestamp, now: Timestamp,
retryTimestamp: Timestamp, retryTimestamp: Timestamp,
): Duration { ): Duration {
if (retryTimestamp.t_ms <= now.t_ms) { const remaining = getDurationRemaining(retryTimestamp, now);
return { d_ms: 0 }; return durationMin(oldDelay, remaining);
}
return { d_ms: Math.min(oldDelay.d_ms, retryTimestamp.t_ms - now.t_ms) };
} }
async function gatherExchangePending( async function gatherExchangePending(
@ -278,7 +276,7 @@ async function gatherProposalPending(
resp.pendingOperations.push({ resp.pendingOperations.push({
type: PendingOperationType.ProposalChoice, type: PendingOperationType.ProposalChoice,
givesLifeness: false, givesLifeness: false,
merchantBaseUrl: proposal.download!!.contractTerms.merchant_base_url, merchantBaseUrl: proposal.download!!.contractData.merchantBaseUrl,
proposalId: proposal.proposalId, proposalId: proposal.proposalId,
proposalTimestamp: proposal.timestamp, proposalTimestamp: proposal.timestamp,
}); });

View File

@ -34,7 +34,6 @@ import { Logger } from "../util/logging";
import { getWithdrawDenomList } from "./withdraw"; import { getWithdrawDenomList } from "./withdraw";
import { updateExchangeFromUrl } from "./exchanges"; import { updateExchangeFromUrl } from "./exchanges";
import { import {
getTimestampNow,
OperationError, OperationError,
CoinPublicKey, CoinPublicKey,
RefreshReason, RefreshReason,
@ -43,6 +42,7 @@ import {
import { guardOperationException } from "./errors"; import { guardOperationException } from "./errors";
import { NotificationType } from "../types/notifications"; import { NotificationType } from "../types/notifications";
import { getRandomBytes, encodeCrock } from "../crypto/talerCrypto"; import { getRandomBytes, encodeCrock } from "../crypto/talerCrypto";
import { getTimestampNow } from "../util/time";
const logger = new Logger("refresh.ts"); const logger = new Logger("refresh.ts");

View File

@ -26,7 +26,6 @@
import { InternalWalletState } from "./state"; import { InternalWalletState } from "./state";
import { import {
OperationError, OperationError,
getTimestampNow,
RefreshReason, RefreshReason,
CoinPublicKey, CoinPublicKey,
} from "../types/walletTypes"; } from "../types/walletTypes";
@ -47,12 +46,14 @@ import {
MerchantRefundPermission, MerchantRefundPermission,
MerchantRefundResponse, MerchantRefundResponse,
RefundRequest, RefundRequest,
codecForMerchantRefundResponse,
} from "../types/talerTypes"; } from "../types/talerTypes";
import { AmountJson } from "../util/amounts"; import { AmountJson } from "../util/amounts";
import { guardOperationException, OperationFailedError } from "./errors"; import { guardOperationException, OperationFailedError } from "./errors";
import { randomBytes } from "../crypto/primitives/nacl-fast"; import { randomBytes } from "../crypto/primitives/nacl-fast";
import { encodeCrock } from "../crypto/talerCrypto"; import { encodeCrock } from "../crypto/talerCrypto";
import { HttpResponseStatus } from "../util/http"; import { HttpResponseStatus } from "../util/http";
import { getTimestampNow } from "../util/time";
async function incrementPurchaseQueryRefundRetry( async function incrementPurchaseQueryRefundRetry(
ws: InternalWalletState, ws: InternalWalletState,
@ -288,7 +289,7 @@ export async function applyRefund(
console.log("processing purchase for refund"); console.log("processing purchase for refund");
await startRefundQuery(ws, purchase.proposalId); await startRefundQuery(ws, purchase.proposalId);
return purchase.contractTermsHash; return purchase.contractData.contractTermsHash;
} }
export async function processPurchaseQueryRefund( export async function processPurchaseQueryRefund(
@ -334,9 +335,9 @@ async function processPurchaseQueryRefundImpl(
const refundUrlObj = new URL( const refundUrlObj = new URL(
"refund", "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; const refundUrl = refundUrlObj.href;
let resp; let resp;
try { try {
@ -349,7 +350,7 @@ async function processPurchaseQueryRefundImpl(
throw Error(`unexpected status code (${resp.status}) for /refund`); 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( await acceptRefundResponse(
ws, ws,
proposalId, proposalId,
@ -409,8 +410,8 @@ async function processPurchaseApplyRefundImpl(
const perm = info.perm; const perm = info.perm;
const req: RefundRequest = { const req: RefundRequest = {
coin_pub: perm.coin_pub, coin_pub: perm.coin_pub,
h_contract_terms: purchase.contractTermsHash, h_contract_terms: purchase.contractData.contractTermsHash,
merchant_pub: purchase.contractTerms.merchant_pub, merchant_pub: purchase.contractData.merchantPub,
merchant_sig: perm.merchant_sig, merchant_sig: perm.merchant_sig,
refund_amount: perm.refund_amount, refund_amount: perm.refund_amount,
refund_fee: perm.refund_fee, refund_fee: perm.refund_fee,

View File

@ -17,7 +17,6 @@
import { import {
CreateReserveRequest, CreateReserveRequest,
CreateReserveResponse, CreateReserveResponse,
getTimestampNow,
ConfirmReserveRequest, ConfirmReserveRequest,
OperationError, OperationError,
AcceptWithdrawalResponse, AcceptWithdrawalResponse,
@ -42,7 +41,7 @@ import {
getExchangeTrust, getExchangeTrust,
getExchangePaytoUri, getExchangePaytoUri,
} from "./exchanges"; } from "./exchanges";
import { WithdrawOperationStatusResponse } from "../types/talerTypes"; import { WithdrawOperationStatusResponse, codecForWithdrawOperationStatusResponse } from "../types/talerTypes";
import { assertUnreachable } from "../util/assertUnreachable"; import { assertUnreachable } from "../util/assertUnreachable";
import { encodeCrock, getRandomBytes } from "../crypto/talerCrypto"; import { encodeCrock, getRandomBytes } from "../crypto/talerCrypto";
import { randomBytes } from "../crypto/primitives/nacl-fast"; import { randomBytes } from "../crypto/primitives/nacl-fast";
@ -57,6 +56,7 @@ import {
} from "./errors"; } from "./errors";
import { NotificationType } from "../types/notifications"; import { NotificationType } from "../types/notifications";
import { codecForReserveStatus } from "../types/ReserveStatus"; import { codecForReserveStatus } from "../types/ReserveStatus";
import { getTimestampNow } from "../util/time";
const logger = new Logger("reserves.ts"); const logger = new Logger("reserves.ts");
@ -289,7 +289,7 @@ async function processReserveBankStatusImpl(
`unexpected status ${statusResp.status} for bank status query`, `unexpected status ${statusResp.status} for bank status query`,
); );
} }
status = WithdrawOperationStatusResponse.checked(await statusResp.json()); status = codecForWithdrawOperationStatusResponse().decode(await statusResp.json());
} catch (e) { } catch (e) {
throw e; throw e;
} }
@ -390,6 +390,7 @@ async function updateReserve(
let resp; let resp;
try { try {
resp = await ws.http.get(reqUrl.href); resp = await ws.http.get(reqUrl.href);
console.log("got reserve/status response", await resp.json());
if (resp.status === 404) { if (resp.status === 404) {
const m = "The exchange does not know about this reserve (yet)."; const m = "The exchange does not know about this reserve (yet).";
await incrementReserveRetry(ws, reservePub, undefined); await incrementReserveRetry(ws, reservePub, undefined);
@ -408,7 +409,7 @@ async function updateReserve(
throw new OperationFailedAndReportedError(m); throw new OperationFailedAndReportedError(m);
} }
const respJson = await resp.json(); const respJson = await resp.json();
const reserveInfo = codecForReserveStatus.decode(respJson); const reserveInfo = codecForReserveStatus().decode(respJson);
const balance = Amounts.parseOrThrow(reserveInfo.balance); const balance = Amounts.parseOrThrow(reserveInfo.balance);
await ws.db.runWithWriteTransaction( await ws.db.runWithWriteTransaction(
[Stores.reserves, Stores.reserveUpdatedEvents], [Stores.reserves, Stores.reserveUpdatedEvents],

View File

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

View File

@ -18,13 +18,14 @@ import { InternalWalletState } from "./state";
import { parseTipUri } from "../util/taleruri"; import { parseTipUri } from "../util/taleruri";
import { import {
TipStatus, TipStatus,
getTimestampNow,
OperationError, OperationError,
} from "../types/walletTypes"; } from "../types/walletTypes";
import { import {
TipPickupGetResponse, TipPickupGetResponse,
TipPlanchetDetail, TipPlanchetDetail,
TipResponse, TipResponse,
codecForTipPickupGetResponse,
codecForTipResponse,
} from "../types/talerTypes"; } from "../types/talerTypes";
import * as Amounts from "../util/amounts"; import * as Amounts from "../util/amounts";
import { import {
@ -39,11 +40,11 @@ import {
getVerifiedWithdrawDenomList, getVerifiedWithdrawDenomList,
processWithdrawSession, processWithdrawSession,
} from "./withdraw"; } from "./withdraw";
import { getTalerStampSec, extractTalerStampOrThrow } from "../util/helpers";
import { updateExchangeFromUrl } from "./exchanges"; import { updateExchangeFromUrl } from "./exchanges";
import { getRandomBytes, encodeCrock } from "../crypto/talerCrypto"; import { getRandomBytes, encodeCrock } from "../crypto/talerCrypto";
import { guardOperationException } from "./errors"; import { guardOperationException } from "./errors";
import { NotificationType } from "../types/notifications"; import { NotificationType } from "../types/notifications";
import { getTimestampNow } from "../util/time";
export async function getTipStatus( export async function getTipStatus(
ws: InternalWalletState, ws: InternalWalletState,
@ -63,7 +64,7 @@ export async function getTipStatus(
} }
const respJson = await merchantResp.json(); const respJson = await merchantResp.json();
console.log("resp:", respJson); console.log("resp:", respJson);
const tipPickupStatus = TipPickupGetResponse.checked(respJson); const tipPickupStatus = codecForTipPickupGetResponse().decode(respJson);
console.log("status", tipPickupStatus); console.log("status", tipPickupStatus);
@ -88,7 +89,7 @@ export async function getTipStatus(
acceptedTimestamp: undefined, acceptedTimestamp: undefined,
rejectedTimestamp: undefined, rejectedTimestamp: undefined,
amount, amount,
deadline: extractTalerStampOrThrow(tipPickupStatus.stamp_expire), deadline: tipPickupStatus.stamp_expire,
exchangeUrl: tipPickupStatus.exchange_url, exchangeUrl: tipPickupStatus.exchange_url,
merchantBaseUrl: res.merchantBaseUrl, merchantBaseUrl: res.merchantBaseUrl,
nextUrl: undefined, nextUrl: undefined,
@ -115,8 +116,8 @@ export async function getTipStatus(
nextUrl: tipPickupStatus.extra.next_url, nextUrl: tipPickupStatus.extra.next_url,
merchantOrigin: res.merchantOrigin, merchantOrigin: res.merchantOrigin,
merchantTipId: res.merchantTipId, merchantTipId: res.merchantTipId,
expirationTimestamp: getTalerStampSec(tipPickupStatus.stamp_expire)!, expirationTimestamp: tipPickupStatus.stamp_expire,
timestamp: getTalerStampSec(tipPickupStatus.stamp_created)!, timestamp: tipPickupStatus.stamp_created,
totalFees: tipRecord.totalFees, totalFees: tipRecord.totalFees,
tipId: tipRecord.tipId, tipId: tipRecord.tipId,
}; };
@ -240,7 +241,7 @@ async function processTipImpl(
throw e; throw e;
} }
const response = TipResponse.checked(await merchantResp.json()); const response = codecForTipResponse().decode(await merchantResp.json());
if (response.reserve_sigs.length !== tipRecord.planchets.length) { if (response.reserve_sigs.length !== tipRecord.planchets.length) {
throw Error("number of tip responses does not match requested planchets"); throw Error("number of tip responses does not match requested planchets");

View File

@ -27,33 +27,39 @@ import {
} from "../types/dbTypes"; } from "../types/dbTypes";
import * as Amounts from "../util/amounts"; import * as Amounts from "../util/amounts";
import { import {
getTimestampNow,
AcceptWithdrawalResponse,
BankWithdrawDetails, BankWithdrawDetails,
ExchangeWithdrawDetails, ExchangeWithdrawDetails,
WithdrawDetails, WithdrawDetails,
OperationError, OperationError,
} from "../types/walletTypes"; } from "../types/walletTypes";
import { WithdrawOperationStatusResponse } from "../types/talerTypes"; import { WithdrawOperationStatusResponse, codecForWithdrawOperationStatusResponse } from "../types/talerTypes";
import { InternalWalletState } from "./state"; import { InternalWalletState } from "./state";
import { parseWithdrawUri } from "../util/taleruri"; import { parseWithdrawUri } from "../util/taleruri";
import { Logger } from "../util/logging"; import { Logger } from "../util/logging";
import { import { updateExchangeFromUrl, getExchangeTrust } from "./exchanges";
updateExchangeFromUrl,
getExchangeTrust,
} from "./exchanges";
import { WALLET_EXCHANGE_PROTOCOL_VERSION } from "./versions"; import { WALLET_EXCHANGE_PROTOCOL_VERSION } from "./versions";
import * as LibtoolVersion from "../util/libtoolVersion"; import * as LibtoolVersion from "../util/libtoolVersion";
import { guardOperationException } from "./errors"; import { guardOperationException } from "./errors";
import { NotificationType } from "../types/notifications"; import { NotificationType } from "../types/notifications";
import {
getTimestampNow,
getDurationRemaining,
timestampCmp,
timestampSubtractDuraction,
} from "../util/time";
const logger = new Logger("withdraw.ts"); const logger = new Logger("withdraw.ts");
function isWithdrawableDenom(d: DenominationRecord) { function isWithdrawableDenom(d: DenominationRecord) {
const now = getTimestampNow(); const now = getTimestampNow();
const started = now.t_ms >= d.stampStart.t_ms; const started = timestampCmp(now, d.stampStart) >= 0;
const stillOkay = d.stampExpireWithdraw.t_ms + 60 * 1000 > now.t_ms; const lastPossibleWithdraw = timestampSubtractDuraction(
d.stampExpireWithdraw,
{ d_ms: 50 * 1000 },
);
const remaining = getDurationRemaining(lastPossibleWithdraw, now);
const stillOkay = remaining.d_ms !== 0;
return started && stillOkay; return started && stillOkay;
} }
@ -108,11 +114,14 @@ export async function getBankWithdrawalInfo(
} }
const resp = await ws.http.get(uriResult.statusUrl); const resp = await ws.http.get(uriResult.statusUrl);
if (resp.status !== 200) { 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(); const respJson = await resp.json();
console.log("resp:", respJson); console.log("resp:", respJson);
const status = WithdrawOperationStatusResponse.checked(respJson);
const status = codecForWithdrawOperationStatusResponse().decode(respJson);
return { return {
amount: Amounts.parseOrThrow(status.amount), amount: Amounts.parseOrThrow(status.amount),
confirmTransferUrl: status.confirm_transfer_url, confirmTransferUrl: status.confirm_transfer_url,
@ -125,15 +134,13 @@ export async function getBankWithdrawalInfo(
}; };
} }
async function getPossibleDenoms( async function getPossibleDenoms(
ws: InternalWalletState, ws: InternalWalletState,
exchangeBaseUrl: string, exchangeBaseUrl: string,
): Promise<DenominationRecord[]> { ): Promise<DenominationRecord[]> {
return await ws.db.iterIndex( return await ws.db
Stores.denominations.exchangeBaseUrlIndex, .iterIndex(Stores.denominations.exchangeBaseUrlIndex, exchangeBaseUrl)
exchangeBaseUrl, .filter(d => {
).filter(d => {
return ( return (
d.status === DenominationStatus.Unverified || d.status === DenominationStatus.Unverified ||
d.status === DenominationStatus.VerifiedGood d.status === DenominationStatus.VerifiedGood
@ -204,8 +211,11 @@ async function processPlanchet(
planchet.denomPub, planchet.denomPub,
); );
const isValid = await ws.cryptoApi.rsaVerify(
const isValid = await ws.cryptoApi.rsaVerify(planchet.coinPub, denomSig, planchet.denomPub); planchet.coinPub,
denomSig,
planchet.denomPub,
);
if (!isValid) { if (!isValid) {
throw Error("invalid RSA signature by the exchange"); throw Error("invalid RSA signature by the exchange");
} }
@ -261,7 +271,10 @@ async function processPlanchet(
r.amountWithdrawCompleted, r.amountWithdrawCompleted,
Amounts.add(denom.value, denom.feeWithdraw).amount, Amounts.add(denom.value, denom.feeWithdraw).amount,
).amount; ).amount;
if (Amounts.cmp(r.amountWithdrawCompleted, r.amountWithdrawAllocated) == 0) { if (
Amounts.cmp(r.amountWithdrawCompleted, r.amountWithdrawAllocated) ==
0
) {
reserveDepleted = true; reserveDepleted = true;
} }
await tx.put(Stores.reserves, r); await tx.put(Stores.reserves, r);
@ -273,9 +286,9 @@ async function processPlanchet(
); );
if (success) { if (success) {
ws.notify( { ws.notify({
type: NotificationType.CoinWithdrawn, type: NotificationType.CoinWithdrawn,
} ); });
} }
if (withdrawSessionFinished) { if (withdrawSessionFinished) {
@ -436,10 +449,10 @@ async function processWithdrawCoin(
return; return;
} }
const coin = await ws.db.getIndexed( const coin = await ws.db.getIndexed(Stores.coins.byWithdrawalWithIdx, [
Stores.coins.byWithdrawalWithIdx, withdrawalSessionId,
[withdrawalSessionId, coinIndex], coinIndex,
); ]);
if (coin) { if (coin) {
console.log("coin already exists"); console.log("coin already exists");
@ -494,7 +507,7 @@ async function resetWithdrawSessionRetry(
ws: InternalWalletState, ws: InternalWalletState,
withdrawalSessionId: string, withdrawalSessionId: string,
) { ) {
await ws.db.mutate(Stores.withdrawalSession, withdrawalSessionId, (x) => { await ws.db.mutate(Stores.withdrawalSession, withdrawalSessionId, x => {
if (x.retryInfo.active) { if (x.retryInfo.active) {
x.retryInfo = initRetryInfo(); x.retryInfo = initRetryInfo();
} }
@ -570,16 +583,12 @@ export async function getExchangeWithdrawalInfo(
} }
} }
const possibleDenoms = await ws.db.iterIndex( const possibleDenoms = await ws.db
Stores.denominations.exchangeBaseUrlIndex, .iterIndex(Stores.denominations.exchangeBaseUrlIndex, baseUrl)
baseUrl, .filter(d => d.isOffered);
).filter(d => d.isOffered);
const trustedAuditorPubs = []; const trustedAuditorPubs = [];
const currencyRecord = await ws.db.get( const currencyRecord = await ws.db.get(Stores.currencies, amount.currency);
Stores.currencies,
amount.currency,
);
if (currencyRecord) { if (currencyRecord) {
trustedAuditorPubs.push(...currencyRecord.auditors.map(a => a.auditorPub)); trustedAuditorPubs.push(...currencyRecord.auditors.map(a => a.auditorPub));
} }
@ -606,7 +615,10 @@ export async function getExchangeWithdrawalInfo(
let tosAccepted = false; let tosAccepted = false;
if (exchangeInfo.termsOfServiceAcceptedTimestamp) { if (exchangeInfo.termsOfServiceAcceptedTimestamp) {
if (exchangeInfo.termsOfServiceAcceptedEtag == exchangeInfo.termsOfServiceLastEtag) { if (
exchangeInfo.termsOfServiceAcceptedEtag ==
exchangeInfo.termsOfServiceLastEtag
) {
tosAccepted = true; tosAccepted = true;
} }
} }

View File

@ -29,10 +29,11 @@ import {
makeCodecForUnion, makeCodecForUnion,
makeCodecForList, makeCodecForList,
} from "../util/codec"; } from "../util/codec";
import { runBlock } from "../util/helpers";
import { AmountString } from "./talerTypes"; import { AmountString } from "./talerTypes";
import { ReserveTransaction, codecForReserveTransaction } from "./ReserveTransaction"; import {
ReserveTransaction,
codecForReserveTransaction,
} from "./ReserveTransaction";
/** /**
* Status of a reserve. * Status of a reserve.
@ -51,11 +52,10 @@ export interface ReserveStatus {
history: ReserveTransaction[]; history: ReserveTransaction[];
} }
export const codecForReserveStatus = runBlock(() => ( export const codecForReserveStatus = () =>
typecheckedCodec<ReserveStatus>( typecheckedCodec<ReserveStatus>(
makeCodecForObject<ReserveStatus>() makeCodecForObject<ReserveStatus>()
.property("balance", codecForString) .property("balance", codecForString)
.property("history", makeCodecForList(codecForReserveTransaction)) .property("history", makeCodecForList(codecForReserveTransaction()))
.build("ReserveStatus") .build("ReserveStatus"),
) );
));

View File

@ -28,15 +28,14 @@ import {
makeCodecForConstString, makeCodecForConstString,
makeCodecForUnion, makeCodecForUnion,
} from "../util/codec"; } from "../util/codec";
import { runBlock } from "../util/helpers";
import { import {
AmountString, AmountString,
Base32String, Base32String,
EddsaSignatureString, EddsaSignatureString,
TimestampString,
EddsaPublicKeyString, EddsaPublicKeyString,
CoinPublicKeyString, CoinPublicKeyString,
} from "./talerTypes"; } from "./talerTypes";
import { Timestamp, codecForTimestamp } from "../util/time";
export const enum ReserveTransactionType { export const enum ReserveTransactionType {
Withdraw = "WITHDRAW", Withdraw = "WITHDRAW",
@ -96,7 +95,7 @@ export interface ReserveDepositTransaction {
/** /**
* Timestamp of the incoming wire transfer. * Timestamp of the incoming wire transfer.
*/ */
timestamp: TimestampString; timestamp: Timestamp;
} }
export interface ReserveClosingTransaction { export interface ReserveClosingTransaction {
@ -137,7 +136,7 @@ export interface ReserveClosingTransaction {
/** /**
* Time when the reserve was closed. * Time when the reserve was closed.
*/ */
timestamp: TimestampString; timestamp: Timestamp;
} }
export interface ReservePaybackTransaction { export interface ReservePaybackTransaction {
@ -173,7 +172,7 @@ export interface ReservePaybackTransaction {
/** /**
* Time when the funds were paid back into the reserve. * Time when the funds were paid back into the reserve.
*/ */
timestamp: TimestampString; timestamp: Timestamp;
/** /**
* Public key of the coin that was paid back. * Public key of the coin that was paid back.
@ -190,7 +189,7 @@ export type ReserveTransaction =
| ReserveClosingTransaction | ReserveClosingTransaction
| ReservePaybackTransaction; | ReservePaybackTransaction;
export const codecForReserveWithdrawTransaction = runBlock(() => export const codecForReserveWithdrawTransaction = () =>
typecheckedCodec<ReserveWithdrawTransaction>( typecheckedCodec<ReserveWithdrawTransaction>(
makeCodecForObject<ReserveWithdrawTransaction>() makeCodecForObject<ReserveWithdrawTransaction>()
.property("amount", codecForString) .property("amount", codecForString)
@ -203,22 +202,20 @@ export const codecForReserveWithdrawTransaction = runBlock(() =>
) )
.property("withdraw_fee", codecForString) .property("withdraw_fee", codecForString)
.build("ReserveWithdrawTransaction"), .build("ReserveWithdrawTransaction"),
), );
);
export const codecForReserveDepositTransaction = runBlock(() => export const codecForReserveDepositTransaction = () =>
typecheckedCodec<ReserveDepositTransaction>( typecheckedCodec<ReserveDepositTransaction>(
makeCodecForObject<ReserveDepositTransaction>() makeCodecForObject<ReserveDepositTransaction>()
.property("amount", codecForString) .property("amount", codecForString)
.property("sender_account_url", codecForString) .property("sender_account_url", codecForString)
.property("timestamp", codecForString) .property("timestamp", codecForTimestamp)
.property("wire_reference", codecForString) .property("wire_reference", codecForString)
.property("type", makeCodecForConstString(ReserveTransactionType.Deposit)) .property("type", makeCodecForConstString(ReserveTransactionType.Deposit))
.build("ReserveDepositTransaction"), .build("ReserveDepositTransaction"),
), );
);
export const codecForReserveClosingTransaction = runBlock(() => export const codecForReserveClosingTransaction = () =>
typecheckedCodec<ReserveClosingTransaction>( typecheckedCodec<ReserveClosingTransaction>(
makeCodecForObject<ReserveClosingTransaction>() makeCodecForObject<ReserveClosingTransaction>()
.property("amount", codecForString) .property("amount", codecForString)
@ -226,14 +223,13 @@ export const codecForReserveClosingTransaction = runBlock(() =>
.property("exchange_pub", codecForString) .property("exchange_pub", codecForString)
.property("exchange_sig", codecForString) .property("exchange_sig", codecForString)
.property("h_wire", codecForString) .property("h_wire", codecForString)
.property("timestamp", codecForString) .property("timestamp", codecForTimestamp)
.property("type", makeCodecForConstString(ReserveTransactionType.Closing)) .property("type", makeCodecForConstString(ReserveTransactionType.Closing))
.property("wtid", codecForString) .property("wtid", codecForString)
.build("ReserveClosingTransaction"), .build("ReserveClosingTransaction"),
), );
);
export const codecForReservePaybackTransaction = runBlock(() => export const codecForReservePaybackTransaction = () =>
typecheckedCodec<ReservePaybackTransaction>( typecheckedCodec<ReservePaybackTransaction>(
makeCodecForObject<ReservePaybackTransaction>() makeCodecForObject<ReservePaybackTransaction>()
.property("amount", codecForString) .property("amount", codecForString)
@ -241,33 +237,31 @@ export const codecForReservePaybackTransaction = runBlock(() =>
.property("exchange_pub", codecForString) .property("exchange_pub", codecForString)
.property("exchange_sig", codecForString) .property("exchange_sig", codecForString)
.property("receiver_account_details", codecForString) .property("receiver_account_details", codecForString)
.property("timestamp", codecForString) .property("timestamp", codecForTimestamp)
.property("type", makeCodecForConstString(ReserveTransactionType.Payback)) .property("type", makeCodecForConstString(ReserveTransactionType.Payback))
.property("wire_transfer", codecForString) .property("wire_transfer", codecForString)
.build("ReservePaybackTransaction"), .build("ReservePaybackTransaction"),
), );
);
export const codecForReserveTransaction = runBlock(() => export const codecForReserveTransaction = () =>
typecheckedCodec<ReserveTransaction>( typecheckedCodec<ReserveTransaction>(
makeCodecForUnion<ReserveTransaction>() makeCodecForUnion<ReserveTransaction>()
.discriminateOn("type") .discriminateOn("type")
.alternative( .alternative(
ReserveTransactionType.Withdraw, ReserveTransactionType.Withdraw,
codecForReserveWithdrawTransaction, codecForReserveWithdrawTransaction(),
) )
.alternative( .alternative(
ReserveTransactionType.Closing, ReserveTransactionType.Closing,
codecForReserveClosingTransaction, codecForReserveClosingTransaction(),
) )
.alternative( .alternative(
ReserveTransactionType.Payback, ReserveTransactionType.Payback,
codecForReservePaybackTransaction, codecForReservePaybackTransaction(),
) )
.alternative( .alternative(
ReserveTransactionType.Deposit, ReserveTransactionType.Deposit,
codecForReserveDepositTransaction, codecForReserveDepositTransaction(),
) )
.build<ReserveTransaction>("ReserveTransaction"), .build<ReserveTransaction>("ReserveTransaction"),
), );
);

View File

@ -24,7 +24,6 @@
* Imports. * Imports.
*/ */
import { AmountJson } from "../util/amounts"; import { AmountJson } from "../util/amounts";
import { Checkable } from "../util/checkable";
import { import {
Auditor, Auditor,
CoinPaySig, CoinPaySig,
@ -33,17 +32,16 @@ import {
MerchantRefundPermission, MerchantRefundPermission,
PayReq, PayReq,
TipResponse, TipResponse,
ExchangeHandle,
} from "./talerTypes"; } from "./talerTypes";
import { Index, Store } from "../util/query"; import { Index, Store } from "../util/query";
import { import {
Timestamp,
OperationError, OperationError,
Duration,
getTimestampNow,
RefreshReason, RefreshReason,
} from "./walletTypes"; } from "./walletTypes";
import { ReserveTransaction } from "./ReserveTransaction"; import { ReserveTransaction } from "./ReserveTransaction";
import { Timestamp, Duration, getTimestampNow } from "../util/time";
export enum ReserveRecordStatus { export enum ReserveRecordStatus {
/** /**
@ -104,6 +102,13 @@ export function updateRetryInfoTimeout(
p: RetryPolicy = defaultRetryPolicy, p: RetryPolicy = defaultRetryPolicy,
): void { ): void {
const now = getTimestampNow(); 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 = const t =
now.t_ms + p.backoffDelta.d_ms * Math.pow(p.backoffBase, r.retryCounter); now.t_ms + p.backoffDelta.d_ms * Math.pow(p.backoffBase, r.retryCounter);
r.nextRetry = { t_ms: t }; r.nextRetry = { t_ms: t };
@ -319,86 +324,72 @@ export enum DenominationStatus {
/** /**
* Denomination record as stored in the wallet's database. * Denomination record as stored in the wallet's database.
*/ */
@Checkable.Class() export interface DenominationRecord {
export class DenominationRecord {
/** /**
* Value of one coin of the denomination. * Value of one coin of the denomination.
*/ */
@Checkable.Value(() => AmountJson)
value: AmountJson; value: AmountJson;
/** /**
* The denomination public key. * The denomination public key.
*/ */
@Checkable.String()
denomPub: string; denomPub: string;
/** /**
* Hash of the denomination public key. * Hash of the denomination public key.
* Stored in the database for faster lookups. * Stored in the database for faster lookups.
*/ */
@Checkable.String()
denomPubHash: string; denomPubHash: string;
/** /**
* Fee for withdrawing. * Fee for withdrawing.
*/ */
@Checkable.Value(() => AmountJson)
feeWithdraw: AmountJson; feeWithdraw: AmountJson;
/** /**
* Fee for depositing. * Fee for depositing.
*/ */
@Checkable.Value(() => AmountJson)
feeDeposit: AmountJson; feeDeposit: AmountJson;
/** /**
* Fee for refreshing. * Fee for refreshing.
*/ */
@Checkable.Value(() => AmountJson)
feeRefresh: AmountJson; feeRefresh: AmountJson;
/** /**
* Fee for refunding. * Fee for refunding.
*/ */
@Checkable.Value(() => AmountJson)
feeRefund: AmountJson; feeRefund: AmountJson;
/** /**
* Validity start date of the denomination. * Validity start date of the denomination.
*/ */
@Checkable.Value(() => Timestamp)
stampStart: Timestamp; stampStart: Timestamp;
/** /**
* Date after which the currency can't be withdrawn anymore. * Date after which the currency can't be withdrawn anymore.
*/ */
@Checkable.Value(() => Timestamp)
stampExpireWithdraw: Timestamp; stampExpireWithdraw: Timestamp;
/** /**
* Date after the denomination officially doesn't exist anymore. * Date after the denomination officially doesn't exist anymore.
*/ */
@Checkable.Value(() => Timestamp)
stampExpireLegal: Timestamp; stampExpireLegal: Timestamp;
/** /**
* Data after which coins of this denomination can't be deposited anymore. * Data after which coins of this denomination can't be deposited anymore.
*/ */
@Checkable.Value(() => Timestamp)
stampExpireDeposit: Timestamp; stampExpireDeposit: Timestamp;
/** /**
* Signature by the exchange's master key over the denomination * Signature by the exchange's master key over the denomination
* information. * information.
*/ */
@Checkable.String()
masterSig: string; masterSig: string;
/** /**
* Did we verify the signature on the denomination? * Did we verify the signature on the denomination?
*/ */
@Checkable.Number()
status: DenominationStatus; status: DenominationStatus;
/** /**
@ -406,20 +397,12 @@ export class DenominationRecord {
* we checked? * we checked?
* Only false when the exchange redacts a previously published denomination. * Only false when the exchange redacts a previously published denomination.
*/ */
@Checkable.Boolean()
isOffered: boolean; isOffered: boolean;
/** /**
* Base URL of the exchange. * Base URL of the exchange.
*/ */
@Checkable.String()
exchangeBaseUrl: 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", REPURCHASE = "repurchase",
} }
@Checkable.Class() export interface ProposalDownload {
export class ProposalDownload {
/** /**
* The contract that was offered by the merchant. * The contract that was offered by the merchant.
*/ */
@Checkable.Value(() => ContractTerms) contractTermsRaw: string;
contractTerms: ContractTerms;
/** contractData: WalletContractData;
* Signature by the merchant over the contract details.
*/
@Checkable.String()
merchantSig: string;
/**
* Signature by the merchant over the contract details.
*/
@Checkable.String()
contractTermsHash: string;
} }
/** /**
* Record for a downloaded order, stored in the wallet's database. * Record for a downloaded order, stored in the wallet's database.
*/ */
@Checkable.Class() export interface ProposalRecord {
export class ProposalRecord {
@Checkable.String()
orderId: string; orderId: string;
@Checkable.String()
merchantBaseUrl: string; merchantBaseUrl: string;
/** /**
@ -753,38 +721,31 @@ export class ProposalRecord {
/** /**
* Unique ID when the order is stored in the wallet DB. * Unique ID when the order is stored in the wallet DB.
*/ */
@Checkable.String()
proposalId: string; proposalId: string;
/** /**
* Timestamp (in ms) of when the record * Timestamp (in ms) of when the record
* was created. * was created.
*/ */
@Checkable.Number()
timestamp: Timestamp; timestamp: Timestamp;
/** /**
* Private key for the nonce. * Private key for the nonce.
*/ */
@Checkable.String()
noncePriv: string; noncePriv: string;
/** /**
* Public key for the nonce. * Public key for the nonce.
*/ */
@Checkable.String()
noncePub: string; noncePub: string;
@Checkable.String()
proposalStatus: ProposalStatus; proposalStatus: ProposalStatus;
@Checkable.String()
repurchaseProposalId: string | undefined; repurchaseProposalId: string | undefined;
/** /**
* Session ID we got when downloading the contract. * Session ID we got when downloading the contract.
*/ */
@Checkable.Optional(Checkable.String())
downloadSessionId?: string; downloadSessionId?: string;
/** /**
@ -793,12 +754,6 @@ export class ProposalRecord {
*/ */
retryInfo: RetryInfo; 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; lastError: OperationError | undefined;
} }
@ -1120,6 +1075,38 @@ export interface ReserveUpdatedEventRecord {
newHistoryTransactions: ReserveTransaction[]; 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 * Record that stores status information about one purchase, starting from when
* the customer accepts a proposal. Includes refund status if applicable. * the customer accepts a proposal. Includes refund status if applicable.
@ -1131,15 +1118,12 @@ export interface PurchaseRecord {
*/ */
proposalId: string; proposalId: string;
/**
* Hash of the contract terms.
*/
contractTermsHash: string;
/** /**
* Contract terms we got from the merchant. * Contract terms we got from the merchant.
*/ */
contractTerms: ContractTerms; contractTermsRaw: string;
contractData: WalletContractData;
/** /**
* The payment request, ready to be send to the merchant's * The payment request, ready to be send to the merchant's
@ -1147,11 +1131,6 @@ export interface PurchaseRecord {
*/ */
payReq: PayReq; payReq: PayReq;
/**
* Signature from the merchant over the contract terms.
*/
merchantSig: string;
/** /**
* Timestamp of the first time that sending a payment to the merchant * Timestamp of the first time that sending a payment to the merchant
* for this purchase was successful. * 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. * the wallet itself, where the wallet acts as a "merchant" for the customer.
*/ */
export interface CoinsReturnRecord { export interface CoinsReturnRecord {
/** contractTermsRaw: string;
* Hash of the contract for sending coins to our own bank account.
*/
contractTermsHash: string;
contractTerms: ContractTerms; contractData: WalletContractData;
/** /**
* Private key where corresponding * Private key where corresponding
@ -1446,11 +1422,11 @@ export namespace Stores {
fulfillmentUrlIndex = new Index<string, PurchaseRecord>( fulfillmentUrlIndex = new Index<string, PurchaseRecord>(
this, this,
"fulfillmentUrlIndex", "fulfillmentUrlIndex",
"contractTerms.fulfillment_url", "contractData.fulfillmentUrl",
); );
orderIdIndex = new Index<string, PurchaseRecord>(this, "orderIdIndex", [ orderIdIndex = new Index<string, PurchaseRecord>(this, "orderIdIndex", [
"contractTerms.merchant_base_url", "contractData.merchantBaseUrl",
"contractTerms.order_id", "contractData.orderId",
]); ]);
} }

View File

@ -18,9 +18,10 @@
* Type and schema definitions for the wallet's history. * Type and schema definitions for the wallet's history.
*/ */
import { Timestamp, RefreshReason } from "./walletTypes"; import { RefreshReason } from "./walletTypes";
import { ReserveTransaction } from "./ReserveTransaction"; import { ReserveTransaction } from "./ReserveTransaction";
import { WithdrawalSource } from "./dbTypes"; import { WithdrawalSource } from "./dbTypes";
import { Timestamp } from "../util/time";
/** /**

View File

@ -21,8 +21,9 @@
/** /**
* Imports. * Imports.
*/ */
import { OperationError, Timestamp, Duration } from "./walletTypes"; import { OperationError } from "./walletTypes";
import { WithdrawalSource, RetryInfo } from "./dbTypes"; import { WithdrawalSource, RetryInfo } from "./dbTypes";
import { Timestamp, Duration } from "../util/time";
export const enum PendingOperationType { export const enum PendingOperationType {
Bug = "bug", Bug = "bug",

File diff suppressed because it is too large Load Diff

View File

@ -16,7 +16,7 @@
import test from "ava"; import test from "ava";
import * as Amounts from "../util/amounts"; import * as Amounts from "../util/amounts";
import { ContractTerms } from "./talerTypes"; import { ContractTerms, codecForContractTerms } from "./talerTypes";
const amt = ( const amt = (
value: number, value: number,
@ -130,6 +130,7 @@ test("amount stringification", t => {
test("contract terms validation", t => { test("contract terms validation", t => {
const c = { const c = {
nonce: "123123123",
H_wire: "123", H_wire: "123",
amount: "EUR:1.5", amount: "EUR:1.5",
auditors: [], auditors: [],
@ -138,23 +139,23 @@ test("contract terms validation", t => {
max_fee: "EUR:1.5", max_fee: "EUR:1.5",
merchant_pub: "12345", merchant_pub: "12345",
order_id: "test_order", order_id: "test_order",
pay_deadline: "Date(12346)", pay_deadline: { t_ms: 42 },
wire_transfer_deadline: "Date(12346)", wire_transfer_deadline: { t_ms: 42 },
merchant_base_url: "https://example.com/pay", merchant_base_url: "https://example.com/pay",
products: [], products: [],
refund_deadline: "Date(12345)", refund_deadline: { t_ms: 42 },
summary: "hello", summary: "hello",
timestamp: "Date(12345)", timestamp: { t_ms: 42 },
wire_method: "test", wire_method: "test",
}; };
ContractTerms.checked(c); codecForContractTerms().decode(c);
const c1 = JSON.parse(JSON.stringify(c)); const c1 = JSON.parse(JSON.stringify(c));
c1.exchanges = []; c1.pay_deadline = "foo";
try { try {
ContractTerms.checked(c1); codecForContractTerms().decode(c1);
} catch (e) { } catch (e) {
t.pass(); t.pass();
return; return;

View File

@ -25,8 +25,7 @@
/** /**
* Imports. * Imports.
*/ */
import { AmountJson } from "../util/amounts"; import { AmountJson, codecForAmountJson } from "../util/amounts";
import { Checkable } from "../util/checkable";
import * as LibtoolVersion from "../util/libtoolVersion"; import * as LibtoolVersion from "../util/libtoolVersion";
import { import {
CoinRecord, CoinRecord,
@ -35,30 +34,23 @@ import {
ExchangeWireInfo, ExchangeWireInfo,
} from "./dbTypes"; } from "./dbTypes";
import { CoinPaySig, ContractTerms } from "./talerTypes"; 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. * Response for the create reserve request to the wallet.
*/ */
@Checkable.Class()
export class CreateReserveResponse { export class CreateReserveResponse {
/** /**
* Exchange URL where the bank should create the reserve. * Exchange URL where the bank should create the reserve.
* The URL is canonicalized in the response. * The URL is canonicalized in the response.
*/ */
@Checkable.String()
exchange: string; exchange: string;
/** /**
* Reserve public key of the newly created reserve. * Reserve public key of the newly created reserve.
*/ */
@Checkable.String()
reservePub: 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. * Request to mark a reserve as confirmed.
*/ */
@Checkable.Class() export interface CreateReserveRequest {
export class CreateReserveRequest {
/** /**
* The initial amount for the reserve. * The initial amount for the reserve.
*/ */
@Checkable.Value(() => AmountJson)
amount: AmountJson; amount: AmountJson;
/** /**
* Exchange URL where the bank should create the reserve. * Exchange URL where the bank should create the reserve.
*/ */
@Checkable.String()
exchange: string; exchange: string;
/** /**
* Payto URI that identifies the exchange's account that the funds * Payto URI that identifies the exchange's account that the funds
* for this reserve go into. * for this reserve go into.
*/ */
@Checkable.String()
exchangeWire: string; exchangeWire: string;
/** /**
* Wire details (as a payto URI) for the bank account that sent the funds to * Wire details (as a payto URI) for the bank account that sent the funds to
* the exchange. * the exchange.
*/ */
@Checkable.Optional(Checkable.String())
senderWire?: string; senderWire?: string;
/** /**
* URL to fetch the withdraw status from the bank. * URL to fetch the withdraw status from the bank.
*/ */
@Checkable.Optional(Checkable.String())
bankWithdrawStatusUrl?: 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. * Request to mark a reserve as confirmed.
*/ */
@Checkable.Class() export interface ConfirmReserveRequest {
export class ConfirmReserveRequest {
/** /**
* Public key of then reserve that should be marked * Public key of then reserve that should be marked
* as confirmed. * as confirmed.
*/ */
@Checkable.String()
reservePub: 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. * Wire coins to the user's own bank account.
*/ */
@Checkable.Class()
export class ReturnCoinsRequest { export class ReturnCoinsRequest {
/** /**
* The amount to wire. * The amount to wire.
*/ */
@Checkable.Value(() => AmountJson)
amount: AmountJson; amount: AmountJson;
/** /**
* The exchange to take the coins from. * The exchange to take the coins from.
*/ */
@Checkable.String()
exchange: string; exchange: string;
/** /**
* Wire details for the bank account of the customer that will * Wire details for the bank account of the customer that will
* receive the funds. * receive the funds.
*/ */
@Checkable.Any()
senderWire?: object; senderWire?: object;
/** /**
@ -391,8 +378,8 @@ export interface TipStatus {
tipId: string; tipId: string;
merchantTipId: string; merchantTipId: string;
merchantOrigin: string; merchantOrigin: string;
expirationTimestamp: number; expirationTimestamp: Timestamp;
timestamp: number; timestamp: Timestamp;
totalFees: AmountJson; totalFees: AmountJson;
} }
@ -418,14 +405,14 @@ export type PreparePayResult =
export interface PreparePayResultPaymentPossible { export interface PreparePayResultPaymentPossible {
status: "payment-possible"; status: "payment-possible";
proposalId: string; proposalId: string;
contractTerms: ContractTerms; contractTermsRaw: string;
totalFees: AmountJson; totalFees: AmountJson;
} }
export interface PreparePayResultInsufficientBalance { export interface PreparePayResultInsufficientBalance {
status: "insufficient-balance"; status: "insufficient-balance";
proposalId: string; proposalId: string;
contractTerms: ContractTerms; contractTermsRaw: any;
} }
export interface PreparePayResultError { export interface PreparePayResultError {
@ -435,7 +422,7 @@ export interface PreparePayResultError {
export interface PreparePayResultPaid { export interface PreparePayResultPaid {
status: "paid"; status: "paid";
contractTerms: ContractTerms; contractTermsRaw: any;
nextUrl: string; nextUrl: string;
} }
@ -459,7 +446,7 @@ export interface AcceptWithdrawalResponse {
* Details about a purchase, including refund status. * Details about a purchase, including refund status.
*/ */
export interface PurchaseDetails { export interface PurchaseDetails {
contractTerms: ContractTerms; contractTerms: any;
hasRefund: boolean; hasRefund: boolean;
totalRefundAmount: AmountJson; totalRefundAmount: AmountJson;
totalRefundAndRefreshFees: AmountJson; totalRefundAndRefreshFees: AmountJson;
@ -479,30 +466,6 @@ export interface OperationError {
details: any; 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 { export interface PlanchetCreationResult {
coinPub: string; coinPub: string;
coinPriv: string; coinPriv: string;

View File

@ -21,7 +21,7 @@
/** /**
* Imports. * Imports.
*/ */
import { getTimestampNow, Timestamp } from "../types/walletTypes"; import { getTimestampNow, Timestamp, timestampSubtractDuraction, timestampDifference } from "../util/time";
/** /**
* Maximum request per second, per origin. * Maximum request per second, per origin.
@ -50,10 +50,14 @@ class OriginState {
private refill(): void { private refill(): void {
const now = getTimestampNow(); const now = getTimestampNow();
const d = now.t_ms - this.lastUpdate.t_ms; const d = timestampDifference(now, this.lastUpdate);
this.tokensSecond = Math.min(MAX_PER_SECOND, this.tokensSecond + (d / 1000)); if (d.d_ms === "forever") {
this.tokensMinute = Math.min(MAX_PER_MINUTE, this.tokensMinute + (d / 1000 * 60)); throw Error("assertion failed")
this.tokensHour = Math.min(MAX_PER_HOUR, this.tokensHour + (d / 1000 * 60 * 60)); }
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; this.lastUpdate = now;
} }

View File

@ -1,17 +1,17 @@
/* /*
This file is part of TALER This file is part of GNU Taler
(C) 2018 GNUnet e.V. and INRIA (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 terms of the GNU General Public License as published by the Free Software
Foundation; either version 3, or (at your option) any later version. 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 WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
A PARTICULAR PURPOSE. See the GNU General Public License for more details. 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 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. * Imports.
*/ */
import { Checkable } from "./checkable"; import {
typecheckedCodec,
makeCodecForObject,
codecForString,
codecForNumber,
} from "./codec";
/** /**
* Number of fractional units that one value unit represents. * 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 * Non-negative financial amount. Fractional values are expressed as multiples
* of 1e-8. * of 1e-8.
*/ */
@Checkable.Class() export interface AmountJson {
export class AmountJson {
/** /**
* Value, must be an integer. * Value, must be an integer.
*/ */
@Checkable.Number()
readonly value: number; readonly value: number;
/** /**
* Fraction, must be an integer. Represent 1/1e8 of a unit. * Fraction, must be an integer. Represent 1/1e8 of a unit.
*/ */
@Checkable.Number()
readonly fraction: number; readonly fraction: number;
/** /**
* Currency of the amount. * Currency of the amount.
*/ */
@Checkable.String()
readonly currency: 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. * Result of a possibly overflowing operation.
*/ */

View File

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

View File

@ -32,11 +32,11 @@ export class DecodingError extends Error {
/** /**
* Context information to show nicer error messages when decoding fails. * Context information to show nicer error messages when decoding fails.
*/ */
interface Context { export interface Context {
readonly path?: string[]; readonly path?: string[];
} }
function renderContext(c?: Context): string { export function renderContext(c?: Context): string {
const p = c?.path; const p = c?.path;
if (p) { if (p) {
return p.join("."); return p.join(".");
@ -84,6 +84,9 @@ class ObjectCodecBuilder<OutputType, PartialOutputType> {
x: K, x: K,
codec: Codec<V>, codec: Codec<V>,
): ObjectCodecBuilder<OutputType, PartialOutputType & SingletonRecord<K, V>> { ): ObjectCodecBuilder<OutputType, PartialOutputType & SingletonRecord<K, V>> {
if (!codec) {
throw Error("inner codec must be defined");
}
this.propList.push({ name: x, codec: codec }); this.propList.push({ name: x, codec: codec });
return this as any; return this as any;
} }
@ -143,6 +146,9 @@ class UnionCodecBuilder<
CommonBaseType, CommonBaseType,
PartialTargetType | V PartialTargetType | V
> { > {
if (!codec) {
throw Error("inner codec must be defined");
}
this.alternatives.set(tagValue, { codec, tagValue }); this.alternatives.set(tagValue, { codec, tagValue });
return this as any; return this as any;
} }
@ -215,6 +221,9 @@ export function makeCodecForUnion<T>(): UnionCodecPreBuilder<T> {
export function makeCodecForMap<T>( export function makeCodecForMap<T>(
innerCodec: Codec<T>, innerCodec: Codec<T>,
): Codec<{ [x: string]: T }> { ): Codec<{ [x: string]: T }> {
if (!innerCodec) {
throw Error("inner codec must be defined");
}
return { return {
decode(x: any, c?: Context): { [x: string]: T } { decode(x: any, c?: Context): { [x: string]: T } {
const map: { [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. * Return a codec for a list, containing values described by the inner codec.
*/ */
export function makeCodecForList<T>(innerCodec: Codec<T>): Codec<T[]> { export function makeCodecForList<T>(innerCodec: Codec<T>): Codec<T[]> {
if (!innerCodec) {
throw Error("inner codec must be defined");
}
return { return {
decode(x: any, c?: Context): T[] { decode(x: any, c?: Context): T[] {
const arr: T[] = []; const arr: T[] = [];
@ -255,7 +267,19 @@ export const codecForNumber: Codec<number> = {
if (typeof x === "number") { if (typeof x === "number") {
return x; 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") { if (typeof x === "string") {
return x; 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; return x;
} }
throw new DecodingError( 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> { export function typecheckedCodec<T = undefined>(c: Codec<T>): Codec<T> {
return c; return c;
} }

View File

@ -24,8 +24,6 @@
import { AmountJson } from "./amounts"; import { AmountJson } from "./amounts";
import * as Amounts from "./amounts"; import * as Amounts from "./amounts";
import { Timestamp, Duration } from "../types/walletTypes";
/** /**
* Show an amount in a form suitable for the user. * Show an amount in a form suitable for the user.
* FIXME: In the future, this should consider currency-specific * 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], []); 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. * Compute the hash function of a JSON object.
*/ */

165
src/util/time.ts Normal file
View File

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

View File

@ -1,17 +1,19 @@
/* import { Duration } from "./time";
This file is part of TALER
(C) 2017 GNUnet e.V.
TALER is free software; you can redistribute it and/or modify it under the /*
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 terms of the GNU General Public License as published by the Free Software
Foundation; either version 3, or (at your option) any later version. 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 WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
A PARTICULAR PURPOSE. See the GNU General Public License for more details. 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 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) => { return new Promise<void>((resolve, reject) => {
this.after(delayMs, () => { if (delayMs.d_ms !== "forever") {
this.after(delayMs.d_ms, () => {
resolve(); resolve();
}); });
}
}); });
} }

View File

@ -91,7 +91,6 @@ import { getHistory } from "./operations/history";
import { getPendingOperations } from "./operations/pending"; import { getPendingOperations } from "./operations/pending";
import { getBalances } from "./operations/balance"; import { getBalances } from "./operations/balance";
import { acceptTip, getTipStatus, processTip } from "./operations/tip"; import { acceptTip, getTipStatus, processTip } from "./operations/tip";
import { returnCoins } from "./operations/return";
import { payback } from "./operations/payback"; import { payback } from "./operations/payback";
import { TimerGroup } from "./util/timer"; import { TimerGroup } from "./util/timer";
import { AsyncCondition } from "./util/promiseUtils"; import { AsyncCondition } from "./util/promiseUtils";
@ -109,6 +108,7 @@ import {
getFullRefundFees, getFullRefundFees,
applyRefund, applyRefund,
} from "./operations/refund"; } from "./operations/refund";
import { durationMin, Duration } from "./util/time";
const builtinCurrencies: CurrencyRecord[] = [ const builtinCurrencies: CurrencyRecord[] = [
@ -289,15 +289,15 @@ export class Wallet {
numGivingLiveness++; numGivingLiveness++;
} }
} }
let dt; let dt: Duration;
if ( if (
allPending.pendingOperations.length === 0 || allPending.pendingOperations.length === 0 ||
allPending.nextRetryDelay.d_ms === Number.MAX_SAFE_INTEGER allPending.nextRetryDelay.d_ms === Number.MAX_SAFE_INTEGER
) { ) {
// Wait for 5 seconds // Wait for 5 seconds
dt = 5000; dt = { d_ms: 5000 };
} else { } else {
dt = Math.min(5000, allPending.nextRetryDelay.d_ms); dt = durationMin({ d_ms: 5000}, allPending.nextRetryDelay);
} }
const timeout = this.timerGroup.resolveAfter(dt); const timeout = this.timerGroup.resolveAfter(dt);
this.ws.notify({ this.ws.notify({
@ -599,7 +599,7 @@ export class Wallet {
* Trigger paying coins back into the user's account. * Trigger paying coins back into the user's account.
*/ */
async returnCoins(req: ReturnCoinsRequest): Promise<void> { async returnCoins(req: ReturnCoinsRequest): Promise<void> {
return returnCoins(this.ws, req); throw Error("not implemented");
} }
/** /**
@ -708,7 +708,7 @@ export class Wallet {
]).amount; ]).amount;
const totalFees = totalRefundFees; const totalFees = totalRefundFees;
return { return {
contractTerms: purchase.contractTerms, contractTerms: purchase.contractTermsRaw,
hasRefund: purchase.timestampLastRefundStatus !== undefined, hasRefund: purchase.timestampLastRefundStatus !== undefined,
totalRefundAmount: totalRefundAmount, totalRefundAmount: totalRefundAmount,
totalRefundAndRefreshFees: totalFees, totalRefundAndRefreshFees: totalFees,

View File

@ -74,7 +74,7 @@ function TalerPayDialog({ talerPayUri }: { talerPayUri: string }) {
); );
} }
const contractTerms = payStatus.contractTerms; const contractTerms = payStatus.contractTermsRaw;
if (!contractTerms) { if (!contractTerms) {
return ( return (

View File

@ -31,6 +31,7 @@ import * as moment from "moment";
import * as i18n from "./i18n"; import * as i18n from "./i18n";
import React from "react"; import React from "react";
import ReactDOM from "react-dom"; import ReactDOM from "react-dom";
import { stringifyTimestamp } from "../util/time";
/** /**
* Render amount as HTML, which non-breaking space between * Render amount as HTML, which non-breaking space between
@ -215,7 +216,7 @@ function FeeDetailsView(props: {
<tbody> <tbody>
{rci!.wireFees.feesForType[s].map(f => ( {rci!.wireFees.feesForType[s].map(f => (
<tr> <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.wireFee)}</td>
<td>{renderAmount(f.closingFee)}</td> <td>{renderAmount(f.closingFee)}</td>
</tr> </tr>
@ -239,9 +240,8 @@ function FeeDetailsView(props: {
<p> <p>
{i18n.str`Rounding loss:`} {overhead} {i18n.str`Rounding loss:`} {overhead}
</p> </p>
<p>{i18n.str`Earliest expiration (for deposit): ${moment <p>{i18n.str`Earliest expiration (for deposit): ${
.unix(rci.earliestDepositExpiration.t_ms / 1000) stringifyTimestamp(rci.earliestDepositExpiration)}`}</p>
.fromNow()}`}</p>
<h3>Coin Fees</h3> <h3>Coin Fees</h3>
<div style={{ overflow: "auto" }}> <div style={{ overflow: "auto" }}>
<table className="pure-table"> <table className="pure-table">

View File

@ -25,8 +25,8 @@
*/ */
import { BrowserCryptoWorkerFactory } from "../crypto/workers/cryptoApi"; import { BrowserCryptoWorkerFactory } from "../crypto/workers/cryptoApi";
import { deleteTalerDatabase, openTalerDatabase, WALLET_DB_VERSION } from "../db"; import { deleteTalerDatabase, openTalerDatabase, WALLET_DB_VERSION } from "../db";
import { ConfirmReserveRequest, CreateReserveRequest, ReturnCoinsRequest, WalletDiagnostics } from "../types/walletTypes"; import { ConfirmReserveRequest, CreateReserveRequest, ReturnCoinsRequest, WalletDiagnostics, codecForCreateReserveRequest, codecForConfirmReserveRequest } from "../types/walletTypes";
import { AmountJson } from "../util/amounts"; import { AmountJson, codecForAmountJson } from "../util/amounts";
import { BrowserHttpLib } from "../util/http"; import { BrowserHttpLib } from "../util/http";
import { OpenedPromise, openPromise } from "../util/promiseUtils"; import { OpenedPromise, openPromise } from "../util/promiseUtils";
import { classifyTalerUri, TalerUriType } from "../util/taleruri"; import { classifyTalerUri, TalerUriType } from "../util/taleruri";
@ -91,14 +91,14 @@ async function handleMessage(
exchange: detail.exchange, exchange: detail.exchange,
senderWire: detail.senderWire, senderWire: detail.senderWire,
}; };
const req = CreateReserveRequest.checked(d); const req = codecForCreateReserveRequest().decode(d);
return needsWallet().createReserve(req); return needsWallet().createReserve(req);
} }
case "confirm-reserve": { case "confirm-reserve": {
const d = { const d = {
reservePub: detail.reservePub, reservePub: detail.reservePub,
}; };
const req = ConfirmReserveRequest.checked(d); const req = codecForConfirmReserveRequest().decode(d);
return needsWallet().confirmReserve(req); return needsWallet().confirmReserve(req);
} }
case "confirm-pay": { case "confirm-pay": {
@ -117,7 +117,7 @@ async function handleMessage(
if (!detail.baseUrl || typeof detail.baseUrl !== "string") { if (!detail.baseUrl || typeof detail.baseUrl !== "string") {
return Promise.resolve({ error: "bad url" }); return Promise.resolve({ error: "bad url" });
} }
const amount = AmountJson.checked(detail.amount); const amount = codecForAmountJson().decode(detail.amount);
return needsWallet().getWithdrawDetailsForAmount(detail.baseUrl, amount); return needsWallet().getWithdrawDetailsForAmount(detail.baseUrl, amount);
} }
case "get-history": { case "get-history": {

View File

@ -56,7 +56,6 @@
"src/operations/refresh.ts", "src/operations/refresh.ts",
"src/operations/refund.ts", "src/operations/refund.ts",
"src/operations/reserves.ts", "src/operations/reserves.ts",
"src/operations/return.ts",
"src/operations/state.ts", "src/operations/state.ts",
"src/operations/tip.ts", "src/operations/tip.ts",
"src/operations/versions.ts", "src/operations/versions.ts",
@ -75,7 +74,6 @@
"src/util/amounts.ts", "src/util/amounts.ts",
"src/util/assertUnreachable.ts", "src/util/assertUnreachable.ts",
"src/util/asyncMemo.ts", "src/util/asyncMemo.ts",
"src/util/checkable.ts",
"src/util/codec-test.ts", "src/util/codec-test.ts",
"src/util/codec.ts", "src/util/codec.ts",
"src/util/helpers-test.ts", "src/util/helpers-test.ts",
@ -90,6 +88,7 @@
"src/util/query.ts", "src/util/query.ts",
"src/util/taleruri-test.ts", "src/util/taleruri-test.ts",
"src/util/taleruri.ts", "src/util/taleruri.ts",
"src/util/time.ts",
"src/util/timer.ts", "src/util/timer.ts",
"src/util/wire.ts", "src/util/wire.ts",
"src/wallet-test.ts", "src/wallet-test.ts",