new date format, replace checkable annotations with codecs
This commit is contained in:
parent
49e3b3e5b9
commit
0c9358c1b2
@ -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,
|
||||
);
|
||||
|
@ -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));
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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,
|
||||
|
@ -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 };
|
||||
}
|
||||
|
@ -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,
|
||||
contractData: {
|
||||
amount,
|
||||
contractTermsHash: contractTermsHash,
|
||||
fulfillmentUrl: parsedContractTerms.fulfillment_url,
|
||||
merchantBaseUrl: parsedContractTerms.merchant_base_url,
|
||||
merchantPub: parsedContractTerms.merchant_pub,
|
||||
merchantSig: proposalResp.sig,
|
||||
contractTermsHash,
|
||||
orderId: parsedContractTerms.order_id,
|
||||
summary: parsedContractTerms.summary,
|
||||
autoRefund: parsedContractTerms.auto_refund,
|
||||
maxWireFee,
|
||||
payDeadline: parsedContractTerms.pay_deadline,
|
||||
refundDeadline: parsedContractTerms.refund_deadline,
|
||||
wireFeeAmortization: parsedContractTerms.wire_fee_amortization || 1,
|
||||
allowedAuditors: parsedContractTerms.auditors.map(x => ({
|
||||
auditorBaseUrl: x.url,
|
||||
auditorPub: x.master_pub,
|
||||
})),
|
||||
allowedExchanges: parsedContractTerms.exchanges.map(x => ({
|
||||
exchangeBaseUrl: x.url,
|
||||
exchangePub: x.master_pub,
|
||||
})),
|
||||
timestamp: parsedContractTerms.timestamp,
|
||||
wireMethod: parsedContractTerms.wire_method,
|
||||
wireInfoHash: parsedContractTerms.H_wire,
|
||||
maxDepositFee: Amounts.parseOrThrow(parsedContractTerms.max_fee),
|
||||
},
|
||||
contractTermsRaw: JSON.stringify(proposalResp.contract_terms),
|
||||
};
|
||||
if (
|
||||
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.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");
|
||||
|
@ -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`);
|
||||
}
|
||||
|
@ -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,
|
||||
});
|
||||
|
@ -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");
|
||||
|
||||
|
@ -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,
|
||||
|
@ -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],
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
@ -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");
|
||||
|
@ -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,15 +134,13 @@ 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 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);
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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"),
|
||||
);
|
||||
|
@ -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"),
|
||||
),
|
||||
);
|
||||
|
@ -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",
|
||||
]);
|
||||
}
|
||||
|
||||
|
@ -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";
|
||||
|
||||
|
||||
/**
|
||||
|
@ -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
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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.
|
||||
*/
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
@ -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
165
src/util/time.ts
Normal 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)}`);
|
||||
},
|
||||
};
|
@ -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, () => {
|
||||
if (delayMs.d_ms !== "forever") {
|
||||
this.after(delayMs.d_ms, () => {
|
||||
resolve();
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -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,
|
||||
|
@ -74,7 +74,7 @@ function TalerPayDialog({ talerPayUri }: { talerPayUri: string }) {
|
||||
);
|
||||
}
|
||||
|
||||
const contractTerms = payStatus.contractTerms;
|
||||
const contractTerms = payStatus.contractTermsRaw;
|
||||
|
||||
if (!contractTerms) {
|
||||
return (
|
||||
|
@ -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">
|
||||
|
@ -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": {
|
||||
|
@ -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",
|
||||
|
Loading…
Reference in New Issue
Block a user