new date format, replace checkable annotations with codecs

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

View File

@ -30,6 +30,7 @@ import {
RefreshSessionRecord,
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,
);

View File

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

View File

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

View File

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

View File

@ -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,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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,

View File

@ -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],

View File

@ -1,271 +0,0 @@
/*
This file is part of GNU Taler
(C) 2019 GNUnet e.V.
GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
Foundation; either version 3, or (at your option) any later version.
GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
/**
* Imports.
*/
import {
ReturnCoinsRequest,
CoinWithDenom,
} from "../types/walletTypes";
import { Database } from "../util/query";
import { InternalWalletState } from "./state";
import { Stores, TipRecord, CoinStatus, CoinsReturnRecord, CoinRecord } from "../types/dbTypes";
import * as Amounts from "../util/amounts";
import { AmountJson } from "../util/amounts";
import { Logger } from "../util/logging";
import { canonicalJson } from "../util/helpers";
import { ContractTerms } from "../types/talerTypes";
import { selectPayCoins } from "./pay";
const logger = new Logger("return.ts");
async function getCoinsForReturn(
ws: InternalWalletState,
exchangeBaseUrl: string,
amount: AmountJson,
): Promise<CoinWithDenom[] | undefined> {
const exchange = await ws.db.get(
Stores.exchanges,
exchangeBaseUrl,
);
if (!exchange) {
throw Error(`Exchange ${exchangeBaseUrl} not known to the wallet`);
}
const coins: CoinRecord[] = await ws.db.iterIndex(
Stores.coins.exchangeBaseUrlIndex,
exchange.baseUrl,
).toArray();
if (!coins || !coins.length) {
return [];
}
const denoms = await ws.db.iterIndex(
Stores.denominations.exchangeBaseUrlIndex,
exchange.baseUrl,
).toArray();
// Denomination of the first coin, we assume that all other
// coins have the same currency
const firstDenom = await ws.db.get(Stores.denominations, [
exchange.baseUrl,
coins[0].denomPub,
]);
if (!firstDenom) {
throw Error("db inconsistent");
}
const currency = firstDenom.value.currency;
const cds: CoinWithDenom[] = [];
for (const coin of coins) {
const denom = await ws.db.get(Stores.denominations, [
exchange.baseUrl,
coin.denomPub,
]);
if (!denom) {
throw Error("db inconsistent");
}
if (denom.value.currency !== currency) {
console.warn(
`same pubkey for different currencies at exchange ${exchange.baseUrl}`,
);
continue;
}
if (coin.suspended) {
continue;
}
if (coin.status !== CoinStatus.Fresh) {
continue;
}
cds.push({ coin, denom });
}
const res = selectPayCoins(denoms, cds, amount, amount);
if (res) {
return res.cds;
}
return undefined;
}
/**
* Trigger paying coins back into the user's account.
*/
export async function returnCoins(
ws: InternalWalletState,
req: ReturnCoinsRequest,
): Promise<void> {
logger.trace("got returnCoins request", req);
const wireType = (req.senderWire as any).type;
logger.trace("wireType", wireType);
if (!wireType || typeof wireType !== "string") {
console.error(`wire type must be a non-empty string, not ${wireType}`);
return;
}
const stampSecNow = Math.floor(new Date().getTime() / 1000);
const exchange = await ws.db.get(Stores.exchanges, req.exchange);
if (!exchange) {
console.error(`Exchange ${req.exchange} not known to the wallet`);
return;
}
const exchangeDetails = exchange.details;
if (!exchangeDetails) {
throw Error("exchange information needs to be updated first.");
}
logger.trace("selecting coins for return:", req);
const cds = await getCoinsForReturn(ws, req.exchange, req.amount);
logger.trace(cds);
if (!cds) {
throw Error("coin return impossible, can't select coins");
}
const { priv, pub } = await ws.cryptoApi.createEddsaKeypair();
const wireHash = await ws.cryptoApi.hashString(
canonicalJson(req.senderWire),
);
const contractTerms: ContractTerms = {
H_wire: wireHash,
amount: Amounts.toString(req.amount),
auditors: [],
exchanges: [
{ master_pub: exchangeDetails.masterPublicKey, url: exchange.baseUrl },
],
extra: {},
fulfillment_url: "",
locations: [],
max_fee: Amounts.toString(req.amount),
merchant: {},
merchant_pub: pub,
order_id: "none",
pay_deadline: `/Date(${stampSecNow + 30 * 5})/`,
wire_transfer_deadline: `/Date(${stampSecNow + 60 * 5})/`,
merchant_base_url: "taler://return-to-account",
products: [],
refund_deadline: `/Date(${stampSecNow + 60 * 5})/`,
timestamp: `/Date(${stampSecNow})/`,
wire_method: wireType,
};
const contractTermsHash = await ws.cryptoApi.hashString(
canonicalJson(contractTerms),
);
const payCoinInfo = await ws.cryptoApi.signDeposit(
contractTerms,
cds,
Amounts.parseOrThrow(contractTerms.amount),
);
logger.trace("pci", payCoinInfo);
const coins = payCoinInfo.coinInfo.map(s => ({ coinPaySig: s.sig }));
const coinsReturnRecord: CoinsReturnRecord = {
coins,
contractTerms,
contractTermsHash,
exchange: exchange.baseUrl,
merchantPriv: priv,
wire: req.senderWire,
};
await ws.db.runWithWriteTransaction(
[Stores.coinsReturns, Stores.coins],
async tx => {
await tx.put(Stores.coinsReturns, coinsReturnRecord);
for (let coinInfo of payCoinInfo.coinInfo) {
const coin = await tx.get(Stores.coins, coinInfo.coinPub);
if (!coin) {
throw Error("coin allocated for deposit not in database anymore");
}
const remaining = Amounts.sub(coin.currentAmount, coinInfo.subtractedAmount);
if (remaining.saturated) {
throw Error("coin allocated for deposit does not have enough balance");
}
coin.currentAmount = remaining.amount;
await tx.put(Stores.coins, coin);
}
},
);
depositReturnedCoins(ws, coinsReturnRecord);
}
async function depositReturnedCoins(
ws: InternalWalletState,
coinsReturnRecord: CoinsReturnRecord,
): Promise<void> {
for (const c of coinsReturnRecord.coins) {
if (c.depositedSig) {
continue;
}
const req = {
H_wire: coinsReturnRecord.contractTerms.H_wire,
coin_pub: c.coinPaySig.coin_pub,
coin_sig: c.coinPaySig.coin_sig,
contribution: c.coinPaySig.contribution,
denom_pub: c.coinPaySig.denom_pub,
h_contract_terms: coinsReturnRecord.contractTermsHash,
merchant_pub: coinsReturnRecord.contractTerms.merchant_pub,
pay_deadline: coinsReturnRecord.contractTerms.pay_deadline,
refund_deadline: coinsReturnRecord.contractTerms.refund_deadline,
timestamp: coinsReturnRecord.contractTerms.timestamp,
ub_sig: c.coinPaySig.ub_sig,
wire: coinsReturnRecord.wire,
wire_transfer_deadline: coinsReturnRecord.contractTerms.pay_deadline,
};
logger.trace("req", req);
const reqUrl = new URL("deposit", coinsReturnRecord.exchange);
const resp = await ws.http.postJson(reqUrl.href, req);
if (resp.status !== 200) {
console.error("deposit failed due to status code", resp);
continue;
}
const respJson = await resp.json();
if (respJson.status !== "DEPOSIT_OK") {
console.error("deposit failed", resp);
continue;
}
if (!respJson.sig) {
console.error("invalid 'sig' field", resp);
continue;
}
// FIXME: verify signature
// For every successful deposit, we replace the old record with an updated one
const currentCrr = await ws.db.get(
Stores.coinsReturns,
coinsReturnRecord.contractTermsHash,
);
if (!currentCrr) {
console.error("database inconsistent");
continue;
}
for (const nc of currentCrr.coins) {
if (nc.coinPaySig.coin_pub === c.coinPaySig.coin_pub) {
nc.depositedSig = respJson.sig;
}
}
await ws.db.put(Stores.coinsReturns, currentCrr);
}
}

View File

@ -18,13 +18,14 @@ import { InternalWalletState } from "./state";
import { parseTipUri } from "../util/taleruri";
import {
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");

View File

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

View File

@ -29,10 +29,11 @@ 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.
@ -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")
)
));
.property("history", makeCodecForList(codecForReserveTransaction()))
.build("ReserveStatus"),
);

View File

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

View File

@ -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.
@ -1131,15 +1118,12 @@ 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
@ -1147,11 +1131,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",
]);
}

View File

@ -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";
/**

View File

@ -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",

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

@ -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.
*/

View File

@ -1,417 +0,0 @@
/*
This file is part of TALER
(C) 2016 GNUnet e.V.
TALER is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
Foundation; either version 3, or (at your option) any later version.
TALER is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with
TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
/**
* Decorators for validating JSON objects and converting them to a typed
* object.
*
* The decorators are put onto classes, and the validation is done
* via a static method that is filled in by the annotation.
*
* Example:
* ```
* @Checkable.Class
* class Person {
* @Checkable.String
* name: string;
* @Checkable.Number
* age: number;
*
* // Method will be implemented automatically
* static checked(obj: any): Person;
* }
* ```
*/
export namespace Checkable {
type Path = Array<number | string>;
interface SchemaErrorConstructor {
new (err: string): SchemaError;
}
interface SchemaError {
name: string;
message: string;
}
interface Prop {
propertyKey: any;
checker: any;
type?: any;
typeThunk?: () => any;
elementChecker?: any;
elementProp?: any;
keyProp?: any;
stringChecker?: (s: string) => boolean;
valueProp?: any;
optional?: boolean;
}
interface CheckableInfo {
extraAllowed: boolean;
props: Prop[];
}
// tslint:disable-next-line:no-shadowed-variable
export const SchemaError = (function SchemaError(this: any, message: string) {
const that: any = this as any;
that.name = "SchemaError";
that.message = message;
that.stack = (new Error() as any).stack;
}) as any as SchemaErrorConstructor;
SchemaError.prototype = new Error();
/**
* Classes that are checkable are annotated with this
* checkable info symbol, which contains the information necessary
* to check if they're valid.
*/
const checkableInfoSym = Symbol("checkableInfo");
/**
* Get the current property list for a checkable type.
*/
function getCheckableInfo(target: any): CheckableInfo {
let chk = target[checkableInfoSym] as CheckableInfo|undefined;
if (!chk) {
chk = { props: [], extraAllowed: false };
target[checkableInfoSym] = chk;
}
return chk;
}
function checkNumber(target: any, prop: Prop, path: Path): any {
if ((typeof target) !== "number") {
throw new SchemaError(`expected number for ${path}`);
}
return target;
}
function checkString(target: any, prop: Prop, path: Path): any {
if (typeof target !== "string") {
throw new SchemaError(`expected string for ${path}, got ${typeof target} instead`);
}
if (prop.stringChecker && !prop.stringChecker(target)) {
throw new SchemaError(`string property ${path} malformed`);
}
return target;
}
function checkBoolean(target: any, prop: Prop, path: Path): any {
if (typeof target !== "boolean") {
throw new SchemaError(`expected boolean for ${path}, got ${typeof target} instead`);
}
return target;
}
function checkAnyObject(target: any, prop: Prop, path: Path): any {
if (typeof target !== "object") {
throw new SchemaError(`expected (any) object for ${path}, got ${typeof target} instead`);
}
return target;
}
function checkAny(target: any, prop: Prop, path: Path): any {
return target;
}
function checkList(target: any, prop: Prop, path: Path): any {
if (!Array.isArray(target)) {
throw new SchemaError(`array expected for ${path}, got ${typeof target} instead`);
}
for (let i = 0; i < target.length; i++) {
const v = target[i];
prop.elementChecker(v, prop.elementProp, path.concat([i]));
}
return target;
}
function checkMap(target: any, prop: Prop, path: Path): any {
if (typeof target !== "object") {
throw new SchemaError(`expected object for ${path}, got ${typeof target} instead`);
}
for (const key in target) {
prop.keyProp.checker(key, prop.keyProp, path.concat([key]));
const value = target[key];
prop.valueProp.checker(value, prop.valueProp, path.concat([key]));
}
return target;
}
function checkOptional(target: any, prop: Prop, path: Path): any {
console.assert(prop.propertyKey);
prop.elementChecker(target,
prop.elementProp,
path.concat([prop.propertyKey]));
return target;
}
function checkValue(target: any, prop: Prop, path: Path): any {
let type;
if (prop.type) {
type = prop.type;
} else if (prop.typeThunk) {
type = prop.typeThunk();
if (!type) {
throw Error(`assertion failed: typeThunk returned null (prop is ${JSON.stringify(prop)})`);
}
} else {
throw Error(`assertion failed: type/typeThunk missing (prop is ${JSON.stringify(prop)})`);
}
const typeName = type.name || "??";
const v = target;
if (!v || typeof v !== "object") {
throw new SchemaError(
`expected object for ${path.join(".")}, got ${typeof v} instead`);
}
const chk = type.prototype[checkableInfoSym];
const props = chk.props;
const remainingPropNames = new Set(Object.getOwnPropertyNames(v));
const obj = new type();
for (const innerProp of props) {
if (!remainingPropNames.has(innerProp.propertyKey)) {
if (innerProp.optional) {
continue;
}
throw new SchemaError(`Property '${innerProp.propertyKey}' missing on '${path}' of '${typeName}'`);
}
if (!remainingPropNames.delete(innerProp.propertyKey)) {
throw new SchemaError("assertion failed");
}
const propVal = v[innerProp.propertyKey];
obj[innerProp.propertyKey] = innerProp.checker(propVal,
innerProp,
path.concat([innerProp.propertyKey]));
}
if (!chk.extraAllowed && remainingPropNames.size !== 0) {
const err = `superfluous properties ${JSON.stringify(Array.from(remainingPropNames.values()))} of ${typeName}`;
throw new SchemaError(err);
}
return obj;
}
/**
* Class with checkable annotations on fields.
* This annotation adds the implementation of the `checked`
* static method.
*/
export function Class(opts: {extra?: boolean, validate?: boolean} = {}) {
return (target: any) => {
const chk = getCheckableInfo(target.prototype);
chk.extraAllowed = !!opts.extra;
target.checked = (v: any) => {
const cv = checkValue(v, {
checker: checkValue,
propertyKey: "(root)",
type: target,
}, ["(root)"]);
if (opts.validate) {
if (typeof target.validate !== "function") {
throw Error("invalid Checkable annotion: validate method required");
}
// May throw exception
target.validate(cv);
}
return cv;
};
return target;
};
}
/**
* Target property must be a Checkable object of the given type.
*/
export function Value(typeThunk: () => any) {
function deco(target: object, propertyKey: string | symbol): void {
const chk = getCheckableInfo(target);
chk.props.push({
checker: checkValue,
propertyKey,
typeThunk,
});
}
return deco;
}
/**
* List of values that match the given annotation. For example, `@Checkable.List(Checkable.String)` is
* an annotation for a list of strings.
*/
export function List(type: any) {
const stub = {};
type(stub, "(list-element)");
const elementProp = getCheckableInfo(stub).props[0];
const elementChecker = elementProp.checker;
if (!elementChecker) {
throw Error("assertion failed");
}
function deco(target: object, propertyKey: string | symbol): void {
const chk = getCheckableInfo(target);
chk.props.push({
checker: checkList,
elementChecker,
elementProp,
propertyKey,
});
}
return deco;
}
/**
* Map from the key type to value type. Takes two annotations,
* one for the key type and one for the value type.
*/
export function Map(keyType: any, valueType: any) {
const keyStub = {};
keyType(keyStub, "(map-key)");
const keyProp = getCheckableInfo(keyStub).props[0];
if (!keyProp) {
throw Error("assertion failed");
}
const valueStub = {};
valueType(valueStub, "(map-value)");
const valueProp = getCheckableInfo(valueStub).props[0];
if (!valueProp) {
throw Error("assertion failed");
}
function deco(target: object, propertyKey: string | symbol): void {
const chk = getCheckableInfo(target);
chk.props.push({
checker: checkMap,
keyProp,
propertyKey,
valueProp,
});
}
return deco;
}
/**
* Makes another annotation optional, for example `@Checkable.Optional(Checkable.Number)`.
*/
export function Optional(type: (target: object, propertyKey: string | symbol) => void | any) {
const stub = {};
type(stub, "(optional-element)");
const elementProp = getCheckableInfo(stub).props[0];
const elementChecker = elementProp.checker;
if (!elementChecker) {
throw Error("assertion failed");
}
function deco(target: object, propertyKey: string | symbol): void {
const chk = getCheckableInfo(target);
chk.props.push({
checker: checkOptional,
elementChecker,
elementProp,
optional: true,
propertyKey,
});
}
return deco;
}
/**
* Target property must be a number.
*/
export function Number(): (target: object, propertyKey: string | symbol) => void {
const deco = (target: object, propertyKey: string | symbol) => {
const chk = getCheckableInfo(target);
chk.props.push({checker: checkNumber, propertyKey});
};
return deco;
}
/**
* Target property must be an arbitary object.
*/
export function AnyObject(): (target: object, propertyKey: string | symbol) => void {
const deco = (target: object, propertyKey: string | symbol) => {
const chk = getCheckableInfo(target);
chk.props.push({
checker: checkAnyObject,
propertyKey,
});
};
return deco;
}
/**
* Target property can be anything.
*
* Not useful by itself, but in combination with higher-order annotations
* such as List or Map.
*/
export function Any(): (target: object, propertyKey: string | symbol) => void {
const deco = (target: object, propertyKey: string | symbol) => {
const chk = getCheckableInfo(target);
chk.props.push({
checker: checkAny,
optional: true,
propertyKey,
});
};
return deco;
}
/**
* Target property must be a string.
*/
export function String(
stringChecker?: (s: string) => boolean): (target: object, propertyKey: string | symbol,
) => void {
const deco = (target: object, propertyKey: string | symbol) => {
const chk = getCheckableInfo(target);
chk.props.push({ checker: checkString, propertyKey, stringChecker });
};
return deco;
}
/**
* Target property must be a boolean value.
*/
export function Boolean(): (target: object, propertyKey: string | symbol) => void {
const deco = (target: object, propertyKey: string | symbol) => {
const chk = getCheckableInfo(target);
chk.props.push({ checker: checkBoolean, propertyKey });
};
return deco;
}
}

View File

@ -32,11 +32,11 @@ export class DecodingError extends Error {
/**
* Context information to show nicer error messages when decoding fails.
*/
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;
}

View File

@ -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.
*/

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

@ -0,0 +1,165 @@
import { Codec, renderContext, Context } from "./codec";
/*
This file is part of GNU Taler
(C) 2017-2019 Taler Systems S.A.
GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
Foundation; either version 3, or (at your option) any later version.
GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
/**
* Helpers for relative and absolute time.
*/
export class Timestamp {
/**
* Timestamp in milliseconds.
*/
readonly t_ms: number | "never";
}
export interface Duration {
/**
* Duration in milliseconds.
*/
readonly d_ms: number | "forever";
}
export function getTimestampNow(): Timestamp {
return {
t_ms: new Date().getTime(),
};
}
export function getDurationRemaining(
deadline: Timestamp,
now = getTimestampNow(),
): Duration {
if (deadline.t_ms === "never") {
return { d_ms: "forever" };
}
if (now.t_ms === "never") {
throw Error("invalid argument for 'now'");
}
if (deadline.t_ms < now.t_ms) {
return { d_ms: 0 };
}
return { d_ms: deadline.t_ms - now.t_ms };
}
export function timestampMin(t1: Timestamp, t2: Timestamp): Timestamp {
if (t1.t_ms === "never") {
return { t_ms: t2.t_ms };
}
if (t2.t_ms === "never") {
return { t_ms: t2.t_ms };
}
return { t_ms: Math.min(t1.t_ms, t2.t_ms) };
}
export function durationMin(d1: Duration, d2: Duration): Duration {
if (d1.d_ms === "forever") {
return { d_ms: d2.d_ms };
}
if (d2.d_ms === "forever") {
return { d_ms: d2.d_ms };
}
return { d_ms: Math.min(d1.d_ms, d2.d_ms) };
}
export function timestampCmp(t1: Timestamp, t2: Timestamp): number {
if (t1.t_ms === "never") {
if (t2.t_ms === "never") {
return 0;
}
return 1;
}
if (t2.t_ms === "never") {
return -1;
}
if (t1.t_ms == t2.t_ms) {
return 0;
}
if (t1.t_ms > t2.t_ms) {
return 1;
}
return -1;
}
export function timestampAddDuration(t1: Timestamp, d: Duration): Timestamp {
if (t1.t_ms === "never" || d.d_ms === "forever") {
return { t_ms: "never" };
}
return { t_ms: t1.t_ms + d.d_ms };
}
export function timestampSubtractDuraction(
t1: Timestamp,
d: Duration,
): Timestamp {
if (t1.t_ms === "never") {
return { t_ms: "never" };
}
if (d.d_ms === "forever") {
return { t_ms: 0 };
}
return { t_ms: Math.max(0, t1.t_ms - d.d_ms) };
}
export function stringifyTimestamp(t: Timestamp) {
if (t.t_ms === "never") {
return "never";
}
return new Date(t.t_ms).toISOString();
}
export function timestampDifference(t1: Timestamp, t2: Timestamp): Duration {
if (t1.t_ms === "never") {
return { d_ms: "forever" };
}
if (t2.t_ms === "never") {
return { d_ms: "forever" };
}
return { d_ms: Math.abs(t1.t_ms - t2.t_ms) };
}
export const codecForTimestamp: Codec<Timestamp> = {
decode(x: any, c?: Context): Timestamp {
const t_ms = x.t_ms;
if (typeof t_ms === "string") {
if (t_ms === "never") {
return { t_ms: "never" };
}
throw Error(`expected timestamp at ${renderContext(c)}`);
}
if (typeof t_ms === "number") {
return { t_ms };
}
throw Error(`expected timestamp at ${renderContext(c)}`);
},
};
export const codecForDuration: Codec<Duration> = {
decode(x: any, c?: Context): Duration {
const d_ms = x.d_ms;
if (typeof d_ms === "string") {
if (d_ms === "forever") {
return { d_ms: "forever" };
}
throw Error(`expected duration at ${renderContext(c)}`);
}
if (typeof d_ms === "number") {
return { d_ms };
}
throw Error(`expected duration at ${renderContext(c)}`);
},
};

View File

@ -1,17 +1,19 @@
/*
This file is part of TALER
(C) 2017 GNUnet e.V.
import { Duration } from "./time";
TALER is free software; you can redistribute it and/or modify it under the
/*
This file is part of GNU Taler
(C) 2017-2019 Taler Systems S.A.
GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
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();
});
}
});
}

View File

@ -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,

View File

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

View File

@ -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">

View File

@ -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": {

View File

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