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