new date format, replace checkable annotations with codecs
This commit is contained in:
parent
49e3b3e5b9
commit
0c9358c1b2
@ -30,6 +30,7 @@ import {
|
|||||||
RefreshSessionRecord,
|
RefreshSessionRecord,
|
||||||
TipPlanchet,
|
TipPlanchet,
|
||||||
WireFee,
|
WireFee,
|
||||||
|
WalletContractData,
|
||||||
} from "../../types/dbTypes";
|
} from "../../types/dbTypes";
|
||||||
|
|
||||||
import { CryptoWorker } from "./cryptoWorker";
|
import { CryptoWorker } from "./cryptoWorker";
|
||||||
@ -384,14 +385,16 @@ export class CryptoApi {
|
|||||||
}
|
}
|
||||||
|
|
||||||
signDeposit(
|
signDeposit(
|
||||||
contractTerms: ContractTerms,
|
contractTermsRaw: string,
|
||||||
|
contractData: WalletContractData,
|
||||||
cds: CoinWithDenom[],
|
cds: CoinWithDenom[],
|
||||||
totalAmount: AmountJson,
|
totalAmount: AmountJson,
|
||||||
): Promise<PaySigInfo> {
|
): Promise<PaySigInfo> {
|
||||||
return this.doRpc<PaySigInfo>(
|
return this.doRpc<PaySigInfo>(
|
||||||
"signDeposit",
|
"signDeposit",
|
||||||
3,
|
3,
|
||||||
contractTerms,
|
contractTermsRaw,
|
||||||
|
contractData,
|
||||||
cds,
|
cds,
|
||||||
totalAmount,
|
totalAmount,
|
||||||
);
|
);
|
||||||
|
@ -33,6 +33,7 @@ import {
|
|||||||
TipPlanchet,
|
TipPlanchet,
|
||||||
WireFee,
|
WireFee,
|
||||||
initRetryInfo,
|
initRetryInfo,
|
||||||
|
WalletContractData,
|
||||||
} from "../../types/dbTypes";
|
} from "../../types/dbTypes";
|
||||||
|
|
||||||
import { CoinPaySig, ContractTerms, PaybackRequest } from "../../types/talerTypes";
|
import { CoinPaySig, ContractTerms, PaybackRequest } from "../../types/talerTypes";
|
||||||
@ -40,13 +41,11 @@ import {
|
|||||||
BenchmarkResult,
|
BenchmarkResult,
|
||||||
CoinWithDenom,
|
CoinWithDenom,
|
||||||
PaySigInfo,
|
PaySigInfo,
|
||||||
Timestamp,
|
|
||||||
PlanchetCreationResult,
|
PlanchetCreationResult,
|
||||||
PlanchetCreationRequest,
|
PlanchetCreationRequest,
|
||||||
getTimestampNow,
|
|
||||||
CoinPayInfo,
|
CoinPayInfo,
|
||||||
} from "../../types/walletTypes";
|
} from "../../types/walletTypes";
|
||||||
import { canonicalJson, getTalerStampSec } from "../../util/helpers";
|
import { canonicalJson } from "../../util/helpers";
|
||||||
import { AmountJson } from "../../util/amounts";
|
import { AmountJson } from "../../util/amounts";
|
||||||
import * as Amounts from "../../util/amounts";
|
import * as Amounts from "../../util/amounts";
|
||||||
import * as timer from "../../util/timer";
|
import * as timer from "../../util/timer";
|
||||||
@ -70,6 +69,7 @@ import {
|
|||||||
} from "../talerCrypto";
|
} from "../talerCrypto";
|
||||||
import { randomBytes } from "../primitives/nacl-fast";
|
import { randomBytes } from "../primitives/nacl-fast";
|
||||||
import { kdf } from "../primitives/kdf";
|
import { kdf } from "../primitives/kdf";
|
||||||
|
import { Timestamp, getTimestampNow } from "../../util/time";
|
||||||
|
|
||||||
enum SignaturePurpose {
|
enum SignaturePurpose {
|
||||||
RESERVE_WITHDRAW = 1200,
|
RESERVE_WITHDRAW = 1200,
|
||||||
@ -104,20 +104,6 @@ function timestampToBuffer(ts: Timestamp): Uint8Array {
|
|||||||
v.setBigUint64(0, s);
|
v.setBigUint64(0, s);
|
||||||
return new Uint8Array(b);
|
return new Uint8Array(b);
|
||||||
}
|
}
|
||||||
|
|
||||||
function talerTimestampStringToBuffer(ts: string): Uint8Array {
|
|
||||||
const t_sec = getTalerStampSec(ts);
|
|
||||||
if (t_sec === null || t_sec === undefined) {
|
|
||||||
// Should have been validated before!
|
|
||||||
throw Error("invalid timestamp");
|
|
||||||
}
|
|
||||||
const buffer = new ArrayBuffer(8);
|
|
||||||
const dvbuf = new DataView(buffer);
|
|
||||||
const s = BigInt(t_sec) * BigInt(1000 * 1000);
|
|
||||||
dvbuf.setBigUint64(0, s);
|
|
||||||
return new Uint8Array(buffer);
|
|
||||||
}
|
|
||||||
|
|
||||||
class SignaturePurposeBuilder {
|
class SignaturePurposeBuilder {
|
||||||
private chunks: Uint8Array[] = [];
|
private chunks: Uint8Array[] = [];
|
||||||
|
|
||||||
@ -346,7 +332,8 @@ export class CryptoImplementation {
|
|||||||
* and deposit permissions for each given coin.
|
* and deposit permissions for each given coin.
|
||||||
*/
|
*/
|
||||||
signDeposit(
|
signDeposit(
|
||||||
contractTerms: ContractTerms,
|
contractTermsRaw: string,
|
||||||
|
contractData: WalletContractData,
|
||||||
cds: CoinWithDenom[],
|
cds: CoinWithDenom[],
|
||||||
totalAmount: AmountJson,
|
totalAmount: AmountJson,
|
||||||
): PaySigInfo {
|
): PaySigInfo {
|
||||||
@ -354,14 +341,13 @@ export class CryptoImplementation {
|
|||||||
coinInfo: [],
|
coinInfo: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
const contractTermsHash = this.hashString(canonicalJson(contractTerms));
|
const contractTermsHash = this.hashString(canonicalJson(JSON.parse(contractTermsRaw)));
|
||||||
|
|
||||||
const feeList: AmountJson[] = cds.map(x => x.denom.feeDeposit);
|
const feeList: AmountJson[] = cds.map(x => x.denom.feeDeposit);
|
||||||
let fees = Amounts.add(Amounts.getZero(feeList[0].currency), ...feeList)
|
let fees = Amounts.add(Amounts.getZero(feeList[0].currency), ...feeList)
|
||||||
.amount;
|
.amount;
|
||||||
// okay if saturates
|
// okay if saturates
|
||||||
fees = Amounts.sub(fees, Amounts.parseOrThrow(contractTerms.max_fee))
|
fees = Amounts.sub(fees, contractData.maxDepositFee).amount;
|
||||||
.amount;
|
|
||||||
const total = Amounts.add(fees, totalAmount).amount;
|
const total = Amounts.add(fees, totalAmount).amount;
|
||||||
|
|
||||||
let amountSpent = Amounts.getZero(cds[0].coin.currentAmount.currency);
|
let amountSpent = Amounts.getZero(cds[0].coin.currentAmount.currency);
|
||||||
@ -395,12 +381,12 @@ export class CryptoImplementation {
|
|||||||
|
|
||||||
const d = buildSigPS(SignaturePurpose.WALLET_COIN_DEPOSIT)
|
const d = buildSigPS(SignaturePurpose.WALLET_COIN_DEPOSIT)
|
||||||
.put(decodeCrock(contractTermsHash))
|
.put(decodeCrock(contractTermsHash))
|
||||||
.put(decodeCrock(contractTerms.H_wire))
|
.put(decodeCrock(contractData.wireInfoHash))
|
||||||
.put(talerTimestampStringToBuffer(contractTerms.timestamp))
|
.put(timestampToBuffer(contractData.timestamp))
|
||||||
.put(talerTimestampStringToBuffer(contractTerms.refund_deadline))
|
.put(timestampToBuffer(contractData.refundDeadline))
|
||||||
.put(amountToBuffer(coinSpend))
|
.put(amountToBuffer(coinSpend))
|
||||||
.put(amountToBuffer(cd.denom.feeDeposit))
|
.put(amountToBuffer(cd.denom.feeDeposit))
|
||||||
.put(decodeCrock(contractTerms.merchant_pub))
|
.put(decodeCrock(contractData.merchantPub))
|
||||||
.put(decodeCrock(cd.coin.coinPub))
|
.put(decodeCrock(cd.coin.coinPub))
|
||||||
.build();
|
.build();
|
||||||
const coinSig = eddsaSign(d, decodeCrock(cd.coin.coinPriv));
|
const coinSig = eddsaSign(d, decodeCrock(cd.coin.coinPriv));
|
||||||
|
@ -23,7 +23,7 @@
|
|||||||
* Imports.
|
* Imports.
|
||||||
*/
|
*/
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import { CheckPaymentResponse } from "../types/talerTypes";
|
import { CheckPaymentResponse, codecForCheckPaymentResponse } from "../types/talerTypes";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Connection to the *internal* merchant backend.
|
* Connection to the *internal* merchant backend.
|
||||||
@ -96,8 +96,8 @@ export class MerchantBackendConnection {
|
|||||||
amount,
|
amount,
|
||||||
summary,
|
summary,
|
||||||
fulfillment_url: fulfillmentUrl,
|
fulfillment_url: fulfillmentUrl,
|
||||||
refund_deadline: `/Date(${t})/`,
|
refund_deadline: { t_ms: t * 1000 },
|
||||||
wire_transfer_deadline: `/Date(${t})/`,
|
wire_transfer_deadline: { t_ms: t * 1000 },
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
const resp = await axios({
|
const resp = await axios({
|
||||||
@ -133,6 +133,7 @@ export class MerchantBackendConnection {
|
|||||||
if (resp.status != 200) {
|
if (resp.status != 200) {
|
||||||
throw Error("failed to check payment");
|
throw Error("failed to check payment");
|
||||||
}
|
}
|
||||||
return CheckPaymentResponse.checked(resp.data);
|
|
||||||
|
return codecForCheckPaymentResponse().decode(resp.data);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -50,7 +50,7 @@ async function doPay(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (result.status === "insufficient-balance") {
|
if (result.status === "insufficient-balance") {
|
||||||
console.log("contract", result.contractTerms!);
|
console.log("contract", result.contractTermsRaw);
|
||||||
console.error("insufficient balance");
|
console.error("insufficient balance");
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
return;
|
return;
|
||||||
@ -65,7 +65,7 @@ async function doPay(
|
|||||||
} else {
|
} else {
|
||||||
throw Error("not reached");
|
throw Error("not reached");
|
||||||
}
|
}
|
||||||
console.log("contract", result.contractTerms!);
|
console.log("contract", result.contractTermsRaw);
|
||||||
let pay;
|
let pay;
|
||||||
if (options.alwaysYes) {
|
if (options.alwaysYes) {
|
||||||
pay = true;
|
pay = true;
|
||||||
|
@ -15,8 +15,8 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { InternalWalletState } from "./state";
|
import { InternalWalletState } from "./state";
|
||||||
import { KeysJson, Denomination, ExchangeWireJson } from "../types/talerTypes";
|
import { ExchangeKeysJson, Denomination, ExchangeWireJson, codecForExchangeKeysJson, codecForExchangeWireJson } from "../types/talerTypes";
|
||||||
import { getTimestampNow, OperationError } from "../types/walletTypes";
|
import { OperationError } from "../types/walletTypes";
|
||||||
import {
|
import {
|
||||||
ExchangeRecord,
|
ExchangeRecord,
|
||||||
ExchangeUpdateStatus,
|
ExchangeUpdateStatus,
|
||||||
@ -29,8 +29,6 @@ import {
|
|||||||
} from "../types/dbTypes";
|
} from "../types/dbTypes";
|
||||||
import {
|
import {
|
||||||
canonicalizeBaseUrl,
|
canonicalizeBaseUrl,
|
||||||
extractTalerStamp,
|
|
||||||
extractTalerStampOrThrow,
|
|
||||||
} from "../util/helpers";
|
} from "../util/helpers";
|
||||||
import { Database } from "../util/query";
|
import { Database } from "../util/query";
|
||||||
import * as Amounts from "../util/amounts";
|
import * as Amounts from "../util/amounts";
|
||||||
@ -40,6 +38,7 @@ import {
|
|||||||
guardOperationException,
|
guardOperationException,
|
||||||
} from "./errors";
|
} from "./errors";
|
||||||
import { WALLET_CACHE_BREAKER_CLIENT_VERSION } from "./versions";
|
import { WALLET_CACHE_BREAKER_CLIENT_VERSION } from "./versions";
|
||||||
|
import { getTimestampNow } from "../util/time";
|
||||||
|
|
||||||
async function denominationRecordFromKeys(
|
async function denominationRecordFromKeys(
|
||||||
ws: InternalWalletState,
|
ws: InternalWalletState,
|
||||||
@ -57,12 +56,10 @@ async function denominationRecordFromKeys(
|
|||||||
feeWithdraw: Amounts.parseOrThrow(denomIn.fee_withdraw),
|
feeWithdraw: Amounts.parseOrThrow(denomIn.fee_withdraw),
|
||||||
isOffered: true,
|
isOffered: true,
|
||||||
masterSig: denomIn.master_sig,
|
masterSig: denomIn.master_sig,
|
||||||
stampExpireDeposit: extractTalerStampOrThrow(denomIn.stamp_expire_deposit),
|
stampExpireDeposit: denomIn.stamp_expire_deposit,
|
||||||
stampExpireLegal: extractTalerStampOrThrow(denomIn.stamp_expire_legal),
|
stampExpireLegal: denomIn.stamp_expire_legal,
|
||||||
stampExpireWithdraw: extractTalerStampOrThrow(
|
stampExpireWithdraw: denomIn.stamp_expire_withdraw,
|
||||||
denomIn.stamp_expire_withdraw,
|
stampStart: denomIn.stamp_start,
|
||||||
),
|
|
||||||
stampStart: extractTalerStampOrThrow(denomIn.stamp_start),
|
|
||||||
status: DenominationStatus.Unverified,
|
status: DenominationStatus.Unverified,
|
||||||
value: Amounts.parseOrThrow(denomIn.value),
|
value: Amounts.parseOrThrow(denomIn.value),
|
||||||
};
|
};
|
||||||
@ -117,9 +114,9 @@ async function updateExchangeWithKeys(
|
|||||||
});
|
});
|
||||||
throw new OperationFailedAndReportedError(m);
|
throw new OperationFailedAndReportedError(m);
|
||||||
}
|
}
|
||||||
let exchangeKeysJson: KeysJson;
|
let exchangeKeysJson: ExchangeKeysJson;
|
||||||
try {
|
try {
|
||||||
exchangeKeysJson = KeysJson.checked(keysResp);
|
exchangeKeysJson = codecForExchangeKeysJson().decode(keysResp);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
const m = `Parsing /keys response failed: ${e.message}`;
|
const m = `Parsing /keys response failed: ${e.message}`;
|
||||||
await setExchangeError(ws, baseUrl, {
|
await setExchangeError(ws, baseUrl, {
|
||||||
@ -130,9 +127,7 @@ async function updateExchangeWithKeys(
|
|||||||
throw new OperationFailedAndReportedError(m);
|
throw new OperationFailedAndReportedError(m);
|
||||||
}
|
}
|
||||||
|
|
||||||
const lastUpdateTimestamp = extractTalerStamp(
|
const lastUpdateTimestamp = exchangeKeysJson.list_issue_date
|
||||||
exchangeKeysJson.list_issue_date,
|
|
||||||
);
|
|
||||||
if (!lastUpdateTimestamp) {
|
if (!lastUpdateTimestamp) {
|
||||||
const m = `Parsing /keys response failed: invalid list_issue_date.`;
|
const m = `Parsing /keys response failed: invalid list_issue_date.`;
|
||||||
await setExchangeError(ws, baseUrl, {
|
await setExchangeError(ws, baseUrl, {
|
||||||
@ -329,7 +324,7 @@ async function updateExchangeWithWireInfo(
|
|||||||
if (!wiJson) {
|
if (!wiJson) {
|
||||||
throw Error("/wire response malformed");
|
throw Error("/wire response malformed");
|
||||||
}
|
}
|
||||||
const wireInfo = ExchangeWireJson.checked(wiJson);
|
const wireInfo = codecForExchangeWireJson().decode(wiJson);
|
||||||
for (const a of wireInfo.accounts) {
|
for (const a of wireInfo.accounts) {
|
||||||
console.log("validating exchange acct");
|
console.log("validating exchange acct");
|
||||||
const isValid = await ws.cryptoApi.isValidWireAccount(
|
const isValid = await ws.cryptoApi.isValidWireAccount(
|
||||||
@ -345,14 +340,8 @@ async function updateExchangeWithWireInfo(
|
|||||||
for (const wireMethod of Object.keys(wireInfo.fees)) {
|
for (const wireMethod of Object.keys(wireInfo.fees)) {
|
||||||
const feeList: WireFee[] = [];
|
const feeList: WireFee[] = [];
|
||||||
for (const x of wireInfo.fees[wireMethod]) {
|
for (const x of wireInfo.fees[wireMethod]) {
|
||||||
const startStamp = extractTalerStamp(x.start_date);
|
const startStamp = x.start_date;
|
||||||
if (!startStamp) {
|
const endStamp = x.end_date;
|
||||||
throw Error("wrong date format");
|
|
||||||
}
|
|
||||||
const endStamp = extractTalerStamp(x.end_date);
|
|
||||||
if (!endStamp) {
|
|
||||||
throw Error("wrong date format");
|
|
||||||
}
|
|
||||||
const fee: WireFee = {
|
const fee: WireFee = {
|
||||||
closingFee: Amounts.parseOrThrow(x.closing_fee),
|
closingFee: Amounts.parseOrThrow(x.closing_fee),
|
||||||
endStamp,
|
endStamp,
|
||||||
|
@ -37,6 +37,7 @@ import {
|
|||||||
import { assertUnreachable } from "../util/assertUnreachable";
|
import { assertUnreachable } from "../util/assertUnreachable";
|
||||||
import { TransactionHandle, Store } from "../util/query";
|
import { TransactionHandle, Store } from "../util/query";
|
||||||
import { ReserveTransactionType } from "../types/ReserveTransaction";
|
import { ReserveTransactionType } from "../types/ReserveTransaction";
|
||||||
|
import { timestampCmp } from "../util/time";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create an event ID from the type and the primary key for the event.
|
* Create an event ID from the type and the primary key for the event.
|
||||||
@ -53,11 +54,11 @@ function getOrderShortInfo(
|
|||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
amount: download.contractTerms.amount,
|
amount: Amounts.toString(download.contractData.amount),
|
||||||
orderId: download.contractTerms.order_id,
|
orderId: download.contractData.orderId,
|
||||||
merchantBaseUrl: download.contractTerms.merchant_base_url,
|
merchantBaseUrl: download.contractData.merchantBaseUrl,
|
||||||
proposalId: proposal.proposalId,
|
proposalId: proposal.proposalId,
|
||||||
summary: download.contractTerms.summary || "",
|
summary: download.contractData.summary,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -356,9 +357,7 @@ export async function getHistory(
|
|||||||
if (!orderShortInfo) {
|
if (!orderShortInfo) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const purchaseAmount = Amounts.parseOrThrow(
|
const purchaseAmount = purchase.contractData.amount;
|
||||||
purchase.contractTerms.amount,
|
|
||||||
);
|
|
||||||
let amountRefundedRaw = Amounts.getZero(purchaseAmount.currency);
|
let amountRefundedRaw = Amounts.getZero(purchaseAmount.currency);
|
||||||
let amountRefundedInvalid = Amounts.getZero(purchaseAmount.currency);
|
let amountRefundedInvalid = Amounts.getZero(purchaseAmount.currency);
|
||||||
let amountRefundedEffective = Amounts.getZero(purchaseAmount.currency);
|
let amountRefundedEffective = Amounts.getZero(purchaseAmount.currency);
|
||||||
@ -408,7 +407,7 @@ export async function getHistory(
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
history.sort((h1, h2) => Math.sign(h1.timestamp.t_ms - h2.timestamp.t_ms));
|
history.sort((h1, h2) => timestampCmp(h1.timestamp, h2.timestamp));
|
||||||
|
|
||||||
return { history };
|
return { history };
|
||||||
}
|
}
|
||||||
|
@ -37,6 +37,7 @@ import {
|
|||||||
Stores,
|
Stores,
|
||||||
updateRetryInfoTimeout,
|
updateRetryInfoTimeout,
|
||||||
PayEventRecord,
|
PayEventRecord,
|
||||||
|
WalletContractData,
|
||||||
} from "../types/dbTypes";
|
} from "../types/dbTypes";
|
||||||
import { NotificationType } from "../types/notifications";
|
import { NotificationType } from "../types/notifications";
|
||||||
import {
|
import {
|
||||||
@ -46,33 +47,29 @@ import {
|
|||||||
MerchantRefundResponse,
|
MerchantRefundResponse,
|
||||||
PayReq,
|
PayReq,
|
||||||
Proposal,
|
Proposal,
|
||||||
|
codecForMerchantRefundResponse,
|
||||||
|
codecForProposal,
|
||||||
|
codecForContractTerms,
|
||||||
} from "../types/talerTypes";
|
} from "../types/talerTypes";
|
||||||
import {
|
import {
|
||||||
CoinSelectionResult,
|
CoinSelectionResult,
|
||||||
CoinWithDenom,
|
CoinWithDenom,
|
||||||
ConfirmPayResult,
|
ConfirmPayResult,
|
||||||
getTimestampNow,
|
|
||||||
OperationError,
|
OperationError,
|
||||||
PaySigInfo,
|
PaySigInfo,
|
||||||
PreparePayResult,
|
PreparePayResult,
|
||||||
RefreshReason,
|
RefreshReason,
|
||||||
Timestamp,
|
|
||||||
} from "../types/walletTypes";
|
} from "../types/walletTypes";
|
||||||
import * as Amounts from "../util/amounts";
|
import * as Amounts from "../util/amounts";
|
||||||
import { AmountJson } from "../util/amounts";
|
import { AmountJson } from "../util/amounts";
|
||||||
import {
|
import { amountToPretty, canonicalJson, strcmp } from "../util/helpers";
|
||||||
amountToPretty,
|
|
||||||
canonicalJson,
|
|
||||||
extractTalerDuration,
|
|
||||||
extractTalerStampOrThrow,
|
|
||||||
strcmp,
|
|
||||||
} from "../util/helpers";
|
|
||||||
import { Logger } from "../util/logging";
|
import { Logger } from "../util/logging";
|
||||||
import { getOrderDownloadUrl, parsePayUri } from "../util/taleruri";
|
import { getOrderDownloadUrl, parsePayUri } from "../util/taleruri";
|
||||||
import { guardOperationException } from "./errors";
|
import { guardOperationException } from "./errors";
|
||||||
import { createRefreshGroup, getTotalRefreshCost } from "./refresh";
|
import { createRefreshGroup, getTotalRefreshCost } from "./refresh";
|
||||||
import { acceptRefundResponse } from "./refund";
|
import { acceptRefundResponse } from "./refund";
|
||||||
import { InternalWalletState } from "./state";
|
import { InternalWalletState } from "./state";
|
||||||
|
import { Timestamp, getTimestampNow, timestampAddDuration } from "../util/time";
|
||||||
|
|
||||||
interface CoinsForPaymentArgs {
|
interface CoinsForPaymentArgs {
|
||||||
allowedAuditors: Auditor[];
|
allowedAuditors: Auditor[];
|
||||||
@ -177,20 +174,20 @@ export function selectPayCoins(
|
|||||||
*/
|
*/
|
||||||
async function getCoinsForPayment(
|
async function getCoinsForPayment(
|
||||||
ws: InternalWalletState,
|
ws: InternalWalletState,
|
||||||
args: CoinsForPaymentArgs,
|
args: WalletContractData,
|
||||||
): Promise<CoinSelectionResult | undefined> {
|
): Promise<CoinSelectionResult | undefined> {
|
||||||
const {
|
const {
|
||||||
allowedAuditors,
|
allowedAuditors,
|
||||||
allowedExchanges,
|
allowedExchanges,
|
||||||
depositFeeLimit,
|
maxDepositFee,
|
||||||
paymentAmount,
|
amount,
|
||||||
wireFeeAmortization,
|
wireFeeAmortization,
|
||||||
wireFeeLimit,
|
maxWireFee,
|
||||||
wireFeeTime,
|
timestamp,
|
||||||
wireMethod,
|
wireMethod,
|
||||||
} = args;
|
} = args;
|
||||||
|
|
||||||
let remainingAmount = paymentAmount;
|
let remainingAmount = amount;
|
||||||
|
|
||||||
const exchanges = await ws.db.iter(Stores.exchanges).toArray();
|
const exchanges = await ws.db.iter(Stores.exchanges).toArray();
|
||||||
|
|
||||||
@ -207,7 +204,7 @@ async function getCoinsForPayment(
|
|||||||
|
|
||||||
// is the exchange explicitly allowed?
|
// is the exchange explicitly allowed?
|
||||||
for (const allowedExchange of allowedExchanges) {
|
for (const allowedExchange of allowedExchanges) {
|
||||||
if (allowedExchange.master_pub === exchangeDetails.masterPublicKey) {
|
if (allowedExchange.exchangePub === exchangeDetails.masterPublicKey) {
|
||||||
isOkay = true;
|
isOkay = true;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@ -217,7 +214,7 @@ async function getCoinsForPayment(
|
|||||||
if (!isOkay) {
|
if (!isOkay) {
|
||||||
for (const allowedAuditor of allowedAuditors) {
|
for (const allowedAuditor of allowedAuditors) {
|
||||||
for (const auditor of exchangeDetails.auditors) {
|
for (const auditor of exchangeDetails.auditors) {
|
||||||
if (auditor.auditor_pub === allowedAuditor.auditor_pub) {
|
if (auditor.auditor_pub === allowedAuditor.auditorPub) {
|
||||||
isOkay = true;
|
isOkay = true;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@ -281,7 +278,7 @@ async function getCoinsForPayment(
|
|||||||
let totalFees = Amounts.getZero(currency);
|
let totalFees = Amounts.getZero(currency);
|
||||||
let wireFee: AmountJson | undefined;
|
let wireFee: AmountJson | undefined;
|
||||||
for (const fee of exchangeFees.feesForType[wireMethod] || []) {
|
for (const fee of exchangeFees.feesForType[wireMethod] || []) {
|
||||||
if (fee.startStamp <= wireFeeTime && fee.endStamp >= wireFeeTime) {
|
if (fee.startStamp <= timestamp && fee.endStamp >= timestamp) {
|
||||||
wireFee = fee.wireFee;
|
wireFee = fee.wireFee;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@ -289,13 +286,13 @@ async function getCoinsForPayment(
|
|||||||
|
|
||||||
if (wireFee) {
|
if (wireFee) {
|
||||||
const amortizedWireFee = Amounts.divide(wireFee, wireFeeAmortization);
|
const amortizedWireFee = Amounts.divide(wireFee, wireFeeAmortization);
|
||||||
if (Amounts.cmp(wireFeeLimit, amortizedWireFee) < 0) {
|
if (Amounts.cmp(maxWireFee, amortizedWireFee) < 0) {
|
||||||
totalFees = Amounts.add(amortizedWireFee, totalFees).amount;
|
totalFees = Amounts.add(amortizedWireFee, totalFees).amount;
|
||||||
remainingAmount = Amounts.add(amortizedWireFee, remainingAmount).amount;
|
remainingAmount = Amounts.add(amortizedWireFee, remainingAmount).amount;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const res = selectPayCoins(denoms, cds, remainingAmount, depositFeeLimit);
|
const res = selectPayCoins(denoms, cds, remainingAmount, maxDepositFee);
|
||||||
|
|
||||||
if (res) {
|
if (res) {
|
||||||
totalFees = Amounts.add(totalFees, res.totalFees).amount;
|
totalFees = Amounts.add(totalFees, res.totalFees).amount;
|
||||||
@ -332,18 +329,17 @@ async function recordConfirmPay(
|
|||||||
}
|
}
|
||||||
logger.trace(`recording payment with session ID ${sessionId}`);
|
logger.trace(`recording payment with session ID ${sessionId}`);
|
||||||
const payReq: PayReq = {
|
const payReq: PayReq = {
|
||||||
coins: payCoinInfo.coinInfo.map((x) => x.sig),
|
coins: payCoinInfo.coinInfo.map(x => x.sig),
|
||||||
merchant_pub: d.contractTerms.merchant_pub,
|
merchant_pub: d.contractData.merchantPub,
|
||||||
mode: "pay",
|
mode: "pay",
|
||||||
order_id: d.contractTerms.order_id,
|
order_id: d.contractData.orderId,
|
||||||
};
|
};
|
||||||
const t: PurchaseRecord = {
|
const t: PurchaseRecord = {
|
||||||
abortDone: false,
|
abortDone: false,
|
||||||
abortRequested: false,
|
abortRequested: false,
|
||||||
contractTerms: d.contractTerms,
|
contractTermsRaw: d.contractTermsRaw,
|
||||||
contractTermsHash: d.contractTermsHash,
|
contractData: d.contractData,
|
||||||
lastSessionId: sessionId,
|
lastSessionId: sessionId,
|
||||||
merchantSig: d.merchantSig,
|
|
||||||
payReq,
|
payReq,
|
||||||
timestampAccept: getTimestampNow(),
|
timestampAccept: getTimestampNow(),
|
||||||
timestampLastRefundStatus: undefined,
|
timestampLastRefundStatus: undefined,
|
||||||
@ -383,14 +379,19 @@ async function recordConfirmPay(
|
|||||||
throw Error("coin allocated for payment doesn't exist anymore");
|
throw Error("coin allocated for payment doesn't exist anymore");
|
||||||
}
|
}
|
||||||
coin.status = CoinStatus.Dormant;
|
coin.status = CoinStatus.Dormant;
|
||||||
const remaining = Amounts.sub(coin.currentAmount, coinInfo.subtractedAmount);
|
const remaining = Amounts.sub(
|
||||||
|
coin.currentAmount,
|
||||||
|
coinInfo.subtractedAmount,
|
||||||
|
);
|
||||||
if (remaining.saturated) {
|
if (remaining.saturated) {
|
||||||
throw Error("not enough remaining balance on coin for payment");
|
throw Error("not enough remaining balance on coin for payment");
|
||||||
}
|
}
|
||||||
coin.currentAmount = remaining.amount;
|
coin.currentAmount = remaining.amount;
|
||||||
await tx.put(Stores.coins, coin);
|
await tx.put(Stores.coins, coin);
|
||||||
}
|
}
|
||||||
const refreshCoinPubs = payCoinInfo.coinInfo.map((x) => ({coinPub: x.coinPub}));
|
const refreshCoinPubs = payCoinInfo.coinInfo.map(x => ({
|
||||||
|
coinPub: x.coinPub,
|
||||||
|
}));
|
||||||
await createRefreshGroup(tx, refreshCoinPubs, RefreshReason.Pay);
|
await createRefreshGroup(tx, refreshCoinPubs, RefreshReason.Pay);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@ -402,11 +403,11 @@ async function recordConfirmPay(
|
|||||||
return t;
|
return t;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getNextUrl(contractTerms: ContractTerms): string {
|
function getNextUrl(contractData: WalletContractData): string {
|
||||||
const f = contractTerms.fulfillment_url;
|
const f = contractData.fulfillmentUrl;
|
||||||
if (f.startsWith("http://") || f.startsWith("https://")) {
|
if (f.startsWith("http://") || f.startsWith("https://")) {
|
||||||
const fu = new URL(contractTerms.fulfillment_url);
|
const fu = new URL(contractData.fulfillmentUrl);
|
||||||
fu.searchParams.set("order_id", contractTerms.order_id);
|
fu.searchParams.set("order_id", contractData.orderId);
|
||||||
return fu.href;
|
return fu.href;
|
||||||
} else {
|
} else {
|
||||||
return f;
|
return f;
|
||||||
@ -440,7 +441,7 @@ export async function abortFailedPayment(
|
|||||||
|
|
||||||
const abortReq = { ...purchase.payReq, mode: "abort-refund" };
|
const abortReq = { ...purchase.payReq, mode: "abort-refund" };
|
||||||
|
|
||||||
const payUrl = new URL("pay", purchase.contractTerms.merchant_base_url).href;
|
const payUrl = new URL("pay", purchase.contractData.merchantBaseUrl).href;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
resp = await ws.http.postJson(payUrl, abortReq);
|
resp = await ws.http.postJson(payUrl, abortReq);
|
||||||
@ -454,7 +455,9 @@ export async function abortFailedPayment(
|
|||||||
throw Error(`unexpected status for /pay (${resp.status})`);
|
throw Error(`unexpected status for /pay (${resp.status})`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const refundResponse = MerchantRefundResponse.checked(await resp.json());
|
const refundResponse = codecForMerchantRefundResponse().decode(
|
||||||
|
await resp.json(),
|
||||||
|
);
|
||||||
await acceptRefundResponse(
|
await acceptRefundResponse(
|
||||||
ws,
|
ws,
|
||||||
purchase.proposalId,
|
purchase.proposalId,
|
||||||
@ -574,13 +577,16 @@ async function processDownloadProposalImpl(
|
|||||||
throw Error(`contract download failed with status ${resp.status}`);
|
throw Error(`contract download failed with status ${resp.status}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const proposalResp = Proposal.checked(await resp.json());
|
const proposalResp = codecForProposal().decode(await resp.json());
|
||||||
|
|
||||||
const contractTermsHash = await ws.cryptoApi.hashString(
|
const contractTermsHash = await ws.cryptoApi.hashString(
|
||||||
canonicalJson(proposalResp.contract_terms),
|
canonicalJson(proposalResp.contract_terms),
|
||||||
);
|
);
|
||||||
|
|
||||||
const fulfillmentUrl = proposalResp.contract_terms.fulfillment_url;
|
const parsedContractTerms = codecForContractTerms().decode(
|
||||||
|
proposalResp.contract_terms,
|
||||||
|
);
|
||||||
|
const fulfillmentUrl = parsedContractTerms.fulfillment_url;
|
||||||
|
|
||||||
await ws.db.runWithWriteTransaction(
|
await ws.db.runWithWriteTransaction(
|
||||||
[Stores.proposals, Stores.purchases],
|
[Stores.proposals, Stores.purchases],
|
||||||
@ -592,10 +598,42 @@ async function processDownloadProposalImpl(
|
|||||||
if (p.proposalStatus !== ProposalStatus.DOWNLOADING) {
|
if (p.proposalStatus !== ProposalStatus.DOWNLOADING) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const amount = Amounts.parseOrThrow(parsedContractTerms.amount);
|
||||||
|
let maxWireFee: AmountJson;
|
||||||
|
if (parsedContractTerms.max_wire_fee) {
|
||||||
|
maxWireFee = Amounts.parseOrThrow(parsedContractTerms.max_wire_fee);
|
||||||
|
} else {
|
||||||
|
maxWireFee = Amounts.getZero(amount.currency);
|
||||||
|
}
|
||||||
p.download = {
|
p.download = {
|
||||||
contractTerms: proposalResp.contract_terms,
|
contractData: {
|
||||||
|
amount,
|
||||||
|
contractTermsHash: contractTermsHash,
|
||||||
|
fulfillmentUrl: parsedContractTerms.fulfillment_url,
|
||||||
|
merchantBaseUrl: parsedContractTerms.merchant_base_url,
|
||||||
|
merchantPub: parsedContractTerms.merchant_pub,
|
||||||
merchantSig: proposalResp.sig,
|
merchantSig: proposalResp.sig,
|
||||||
contractTermsHash,
|
orderId: parsedContractTerms.order_id,
|
||||||
|
summary: parsedContractTerms.summary,
|
||||||
|
autoRefund: parsedContractTerms.auto_refund,
|
||||||
|
maxWireFee,
|
||||||
|
payDeadline: parsedContractTerms.pay_deadline,
|
||||||
|
refundDeadline: parsedContractTerms.refund_deadline,
|
||||||
|
wireFeeAmortization: parsedContractTerms.wire_fee_amortization || 1,
|
||||||
|
allowedAuditors: parsedContractTerms.auditors.map(x => ({
|
||||||
|
auditorBaseUrl: x.url,
|
||||||
|
auditorPub: x.master_pub,
|
||||||
|
})),
|
||||||
|
allowedExchanges: parsedContractTerms.exchanges.map(x => ({
|
||||||
|
exchangeBaseUrl: x.url,
|
||||||
|
exchangePub: x.master_pub,
|
||||||
|
})),
|
||||||
|
timestamp: parsedContractTerms.timestamp,
|
||||||
|
wireMethod: parsedContractTerms.wire_method,
|
||||||
|
wireInfoHash: parsedContractTerms.H_wire,
|
||||||
|
maxDepositFee: Amounts.parseOrThrow(parsedContractTerms.max_fee),
|
||||||
|
},
|
||||||
|
contractTermsRaw: JSON.stringify(proposalResp.contract_terms),
|
||||||
};
|
};
|
||||||
if (
|
if (
|
||||||
fulfillmentUrl.startsWith("http://") ||
|
fulfillmentUrl.startsWith("http://") ||
|
||||||
@ -697,7 +735,7 @@ export async function submitPay(
|
|||||||
|
|
||||||
console.log("paying with session ID", sessionId);
|
console.log("paying with session ID", sessionId);
|
||||||
|
|
||||||
const payUrl = new URL("pay", purchase.contractTerms.merchant_base_url).href;
|
const payUrl = new URL("pay", purchase.contractData.merchantBaseUrl).href;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
resp = await ws.http.postJson(payUrl, payReq);
|
resp = await ws.http.postJson(payUrl, payReq);
|
||||||
@ -714,10 +752,10 @@ export async function submitPay(
|
|||||||
|
|
||||||
const now = getTimestampNow();
|
const now = getTimestampNow();
|
||||||
|
|
||||||
const merchantPub = purchase.contractTerms.merchant_pub;
|
const merchantPub = purchase.contractData.merchantPub;
|
||||||
const valid: boolean = await ws.cryptoApi.isValidPaymentSignature(
|
const valid: boolean = await ws.cryptoApi.isValidPaymentSignature(
|
||||||
merchantResp.sig,
|
merchantResp.sig,
|
||||||
purchase.contractTermsHash,
|
purchase.contractData.contractTermsHash,
|
||||||
merchantPub,
|
merchantPub,
|
||||||
);
|
);
|
||||||
if (!valid) {
|
if (!valid) {
|
||||||
@ -731,19 +769,13 @@ export async function submitPay(
|
|||||||
purchase.lastPayError = undefined;
|
purchase.lastPayError = undefined;
|
||||||
purchase.payRetryInfo = initRetryInfo(false);
|
purchase.payRetryInfo = initRetryInfo(false);
|
||||||
if (isFirst) {
|
if (isFirst) {
|
||||||
const ar = purchase.contractTerms.auto_refund;
|
const ar = purchase.contractData.autoRefund;
|
||||||
if (ar) {
|
if (ar) {
|
||||||
console.log("auto_refund present");
|
console.log("auto_refund present");
|
||||||
const autoRefundDelay = extractTalerDuration(ar);
|
|
||||||
console.log("auto_refund valid", autoRefundDelay);
|
|
||||||
if (autoRefundDelay) {
|
|
||||||
purchase.refundStatusRequested = true;
|
purchase.refundStatusRequested = true;
|
||||||
purchase.refundStatusRetryInfo = initRetryInfo();
|
purchase.refundStatusRetryInfo = initRetryInfo();
|
||||||
purchase.lastRefundStatusError = undefined;
|
purchase.lastRefundStatusError = undefined;
|
||||||
purchase.autoRefundDeadline = {
|
purchase.autoRefundDeadline = timestampAddDuration(now, ar);
|
||||||
t_ms: now.t_ms + autoRefundDelay.d_ms,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -761,8 +793,8 @@ export async function submitPay(
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
const nextUrl = getNextUrl(purchase.contractTerms);
|
const nextUrl = getNextUrl(purchase.contractData);
|
||||||
ws.cachedNextUrl[purchase.contractTerms.fulfillment_url] = {
|
ws.cachedNextUrl[purchase.contractData.fulfillmentUrl] = {
|
||||||
nextUrl,
|
nextUrl,
|
||||||
lastSessionId: sessionId,
|
lastSessionId: sessionId,
|
||||||
};
|
};
|
||||||
@ -816,9 +848,9 @@ export async function preparePay(
|
|||||||
console.error("bad proposal", proposal);
|
console.error("bad proposal", proposal);
|
||||||
throw Error("proposal is in invalid state");
|
throw Error("proposal is in invalid state");
|
||||||
}
|
}
|
||||||
const contractTerms = d.contractTerms;
|
const contractData = d.contractData;
|
||||||
const merchantSig = d.merchantSig;
|
const merchantSig = d.contractData.merchantSig;
|
||||||
if (!contractTerms || !merchantSig) {
|
if (!merchantSig) {
|
||||||
throw Error("BUG: proposal is in invalid state");
|
throw Error("BUG: proposal is in invalid state");
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -828,45 +860,31 @@ export async function preparePay(
|
|||||||
const purchase = await ws.db.get(Stores.purchases, proposalId);
|
const purchase = await ws.db.get(Stores.purchases, proposalId);
|
||||||
|
|
||||||
if (!purchase) {
|
if (!purchase) {
|
||||||
const paymentAmount = Amounts.parseOrThrow(contractTerms.amount);
|
// If not already paid, check if we could pay for it.
|
||||||
let wireFeeLimit;
|
const res = await getCoinsForPayment(ws, contractData);
|
||||||
if (contractTerms.max_wire_fee) {
|
|
||||||
wireFeeLimit = Amounts.parseOrThrow(contractTerms.max_wire_fee);
|
|
||||||
} else {
|
|
||||||
wireFeeLimit = Amounts.getZero(paymentAmount.currency);
|
|
||||||
}
|
|
||||||
// If not already payed, check if we could pay for it.
|
|
||||||
const res = await getCoinsForPayment(ws, {
|
|
||||||
allowedAuditors: contractTerms.auditors,
|
|
||||||
allowedExchanges: contractTerms.exchanges,
|
|
||||||
depositFeeLimit: Amounts.parseOrThrow(contractTerms.max_fee),
|
|
||||||
paymentAmount,
|
|
||||||
wireFeeAmortization: contractTerms.wire_fee_amortization || 1,
|
|
||||||
wireFeeLimit,
|
|
||||||
wireFeeTime: extractTalerStampOrThrow(contractTerms.timestamp),
|
|
||||||
wireMethod: contractTerms.wire_method,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!res) {
|
if (!res) {
|
||||||
console.log("not confirming payment, insufficient coins");
|
console.log("not confirming payment, insufficient coins");
|
||||||
return {
|
return {
|
||||||
status: "insufficient-balance",
|
status: "insufficient-balance",
|
||||||
contractTerms: contractTerms,
|
contractTermsRaw: d.contractTermsRaw,
|
||||||
proposalId: proposal.proposalId,
|
proposalId: proposal.proposalId,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
status: "payment-possible",
|
status: "payment-possible",
|
||||||
contractTerms: contractTerms,
|
contractTermsRaw: d.contractTermsRaw,
|
||||||
proposalId: proposal.proposalId,
|
proposalId: proposal.proposalId,
|
||||||
totalFees: res.totalFees,
|
totalFees: res.totalFees,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (uriResult.sessionId && purchase.lastSessionId !== uriResult.sessionId) {
|
if (uriResult.sessionId && purchase.lastSessionId !== uriResult.sessionId) {
|
||||||
console.log("automatically re-submitting payment with different session ID")
|
console.log(
|
||||||
await ws.db.runWithWriteTransaction([Stores.purchases], async (tx) => {
|
"automatically re-submitting payment with different session ID",
|
||||||
|
);
|
||||||
|
await ws.db.runWithWriteTransaction([Stores.purchases], async tx => {
|
||||||
const p = await tx.get(Stores.purchases, proposalId);
|
const p = await tx.get(Stores.purchases, proposalId);
|
||||||
if (!p) {
|
if (!p) {
|
||||||
return;
|
return;
|
||||||
@ -879,8 +897,8 @@ export async function preparePay(
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
status: "paid",
|
status: "paid",
|
||||||
contractTerms: purchase.contractTerms,
|
contractTermsRaw: purchase.contractTermsRaw,
|
||||||
nextUrl: getNextUrl(purchase.contractTerms),
|
nextUrl: getNextUrl(purchase.contractData),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -906,7 +924,10 @@ export async function confirmPay(
|
|||||||
throw Error("proposal is in invalid state");
|
throw Error("proposal is in invalid state");
|
||||||
}
|
}
|
||||||
|
|
||||||
let purchase = await ws.db.get(Stores.purchases, d.contractTermsHash);
|
let purchase = await ws.db.get(
|
||||||
|
Stores.purchases,
|
||||||
|
d.contractData.contractTermsHash,
|
||||||
|
);
|
||||||
|
|
||||||
if (purchase) {
|
if (purchase) {
|
||||||
if (
|
if (
|
||||||
@ -926,25 +947,7 @@ export async function confirmPay(
|
|||||||
|
|
||||||
logger.trace("confirmPay: purchase record does not exist yet");
|
logger.trace("confirmPay: purchase record does not exist yet");
|
||||||
|
|
||||||
const contractAmount = Amounts.parseOrThrow(d.contractTerms.amount);
|
const res = await getCoinsForPayment(ws, d.contractData);
|
||||||
|
|
||||||
let wireFeeLimit;
|
|
||||||
if (!d.contractTerms.max_wire_fee) {
|
|
||||||
wireFeeLimit = Amounts.getZero(contractAmount.currency);
|
|
||||||
} else {
|
|
||||||
wireFeeLimit = Amounts.parseOrThrow(d.contractTerms.max_wire_fee);
|
|
||||||
}
|
|
||||||
|
|
||||||
const res = await getCoinsForPayment(ws, {
|
|
||||||
allowedAuditors: d.contractTerms.auditors,
|
|
||||||
allowedExchanges: d.contractTerms.exchanges,
|
|
||||||
depositFeeLimit: Amounts.parseOrThrow(d.contractTerms.max_fee),
|
|
||||||
paymentAmount: Amounts.parseOrThrow(d.contractTerms.amount),
|
|
||||||
wireFeeAmortization: d.contractTerms.wire_fee_amortization || 1,
|
|
||||||
wireFeeLimit,
|
|
||||||
wireFeeTime: extractTalerStampOrThrow(d.contractTerms.timestamp),
|
|
||||||
wireMethod: d.contractTerms.wire_method,
|
|
||||||
});
|
|
||||||
|
|
||||||
logger.trace("coin selection result", res);
|
logger.trace("coin selection result", res);
|
||||||
|
|
||||||
@ -956,7 +959,8 @@ export async function confirmPay(
|
|||||||
|
|
||||||
const { cds, totalAmount } = res;
|
const { cds, totalAmount } = res;
|
||||||
const payCoinInfo = await ws.cryptoApi.signDeposit(
|
const payCoinInfo = await ws.cryptoApi.signDeposit(
|
||||||
d.contractTerms,
|
d.contractTermsRaw,
|
||||||
|
d.contractData,
|
||||||
cds,
|
cds,
|
||||||
totalAmount,
|
totalAmount,
|
||||||
);
|
);
|
||||||
@ -964,7 +968,7 @@ export async function confirmPay(
|
|||||||
ws,
|
ws,
|
||||||
proposal,
|
proposal,
|
||||||
payCoinInfo,
|
payCoinInfo,
|
||||||
sessionIdOverride
|
sessionIdOverride,
|
||||||
);
|
);
|
||||||
|
|
||||||
logger.trace("confirmPay: submitting payment after creating purchase record");
|
logger.trace("confirmPay: submitting payment after creating purchase record");
|
||||||
|
@ -24,7 +24,7 @@ import { InternalWalletState } from "./state";
|
|||||||
import { Stores, TipRecord, CoinStatus } from "../types/dbTypes";
|
import { Stores, TipRecord, CoinStatus } from "../types/dbTypes";
|
||||||
|
|
||||||
import { Logger } from "../util/logging";
|
import { Logger } from "../util/logging";
|
||||||
import { PaybackConfirmation } from "../types/talerTypes";
|
import { RecoupConfirmation, codecForRecoupConfirmation } from "../types/talerTypes";
|
||||||
import { updateExchangeFromUrl } from "./exchanges";
|
import { updateExchangeFromUrl } from "./exchanges";
|
||||||
import { NotificationType } from "../types/notifications";
|
import { NotificationType } from "../types/notifications";
|
||||||
|
|
||||||
@ -72,7 +72,7 @@ export async function payback(
|
|||||||
if (resp.status !== 200) {
|
if (resp.status !== 200) {
|
||||||
throw Error();
|
throw Error();
|
||||||
}
|
}
|
||||||
const paybackConfirmation = PaybackConfirmation.checked(await resp.json());
|
const paybackConfirmation = codecForRecoupConfirmation().decode(await resp.json());
|
||||||
if (paybackConfirmation.reserve_pub !== coin.reservePub) {
|
if (paybackConfirmation.reserve_pub !== coin.reservePub) {
|
||||||
throw Error(`Coin's reserve doesn't match reserve on payback`);
|
throw Error(`Coin's reserve doesn't match reserve on payback`);
|
||||||
}
|
}
|
||||||
|
@ -27,7 +27,7 @@ import {
|
|||||||
PendingOperationsResponse,
|
PendingOperationsResponse,
|
||||||
PendingOperationType,
|
PendingOperationType,
|
||||||
} from "../types/pending";
|
} from "../types/pending";
|
||||||
import { Duration, getTimestampNow, Timestamp } from "../types/walletTypes";
|
import { Duration, getTimestampNow, Timestamp, getDurationRemaining, durationMin } from "../util/time";
|
||||||
import { TransactionHandle } from "../util/query";
|
import { TransactionHandle } from "../util/query";
|
||||||
import { InternalWalletState } from "./state";
|
import { InternalWalletState } from "./state";
|
||||||
|
|
||||||
@ -36,10 +36,8 @@ function updateRetryDelay(
|
|||||||
now: Timestamp,
|
now: Timestamp,
|
||||||
retryTimestamp: Timestamp,
|
retryTimestamp: Timestamp,
|
||||||
): Duration {
|
): Duration {
|
||||||
if (retryTimestamp.t_ms <= now.t_ms) {
|
const remaining = getDurationRemaining(retryTimestamp, now);
|
||||||
return { d_ms: 0 };
|
return durationMin(oldDelay, remaining);
|
||||||
}
|
|
||||||
return { d_ms: Math.min(oldDelay.d_ms, retryTimestamp.t_ms - now.t_ms) };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function gatherExchangePending(
|
async function gatherExchangePending(
|
||||||
@ -278,7 +276,7 @@ async function gatherProposalPending(
|
|||||||
resp.pendingOperations.push({
|
resp.pendingOperations.push({
|
||||||
type: PendingOperationType.ProposalChoice,
|
type: PendingOperationType.ProposalChoice,
|
||||||
givesLifeness: false,
|
givesLifeness: false,
|
||||||
merchantBaseUrl: proposal.download!!.contractTerms.merchant_base_url,
|
merchantBaseUrl: proposal.download!!.contractData.merchantBaseUrl,
|
||||||
proposalId: proposal.proposalId,
|
proposalId: proposal.proposalId,
|
||||||
proposalTimestamp: proposal.timestamp,
|
proposalTimestamp: proposal.timestamp,
|
||||||
});
|
});
|
||||||
|
@ -34,7 +34,6 @@ import { Logger } from "../util/logging";
|
|||||||
import { getWithdrawDenomList } from "./withdraw";
|
import { getWithdrawDenomList } from "./withdraw";
|
||||||
import { updateExchangeFromUrl } from "./exchanges";
|
import { updateExchangeFromUrl } from "./exchanges";
|
||||||
import {
|
import {
|
||||||
getTimestampNow,
|
|
||||||
OperationError,
|
OperationError,
|
||||||
CoinPublicKey,
|
CoinPublicKey,
|
||||||
RefreshReason,
|
RefreshReason,
|
||||||
@ -43,6 +42,7 @@ import {
|
|||||||
import { guardOperationException } from "./errors";
|
import { guardOperationException } from "./errors";
|
||||||
import { NotificationType } from "../types/notifications";
|
import { NotificationType } from "../types/notifications";
|
||||||
import { getRandomBytes, encodeCrock } from "../crypto/talerCrypto";
|
import { getRandomBytes, encodeCrock } from "../crypto/talerCrypto";
|
||||||
|
import { getTimestampNow } from "../util/time";
|
||||||
|
|
||||||
const logger = new Logger("refresh.ts");
|
const logger = new Logger("refresh.ts");
|
||||||
|
|
||||||
|
@ -26,7 +26,6 @@
|
|||||||
import { InternalWalletState } from "./state";
|
import { InternalWalletState } from "./state";
|
||||||
import {
|
import {
|
||||||
OperationError,
|
OperationError,
|
||||||
getTimestampNow,
|
|
||||||
RefreshReason,
|
RefreshReason,
|
||||||
CoinPublicKey,
|
CoinPublicKey,
|
||||||
} from "../types/walletTypes";
|
} from "../types/walletTypes";
|
||||||
@ -47,12 +46,14 @@ import {
|
|||||||
MerchantRefundPermission,
|
MerchantRefundPermission,
|
||||||
MerchantRefundResponse,
|
MerchantRefundResponse,
|
||||||
RefundRequest,
|
RefundRequest,
|
||||||
|
codecForMerchantRefundResponse,
|
||||||
} from "../types/talerTypes";
|
} from "../types/talerTypes";
|
||||||
import { AmountJson } from "../util/amounts";
|
import { AmountJson } from "../util/amounts";
|
||||||
import { guardOperationException, OperationFailedError } from "./errors";
|
import { guardOperationException, OperationFailedError } from "./errors";
|
||||||
import { randomBytes } from "../crypto/primitives/nacl-fast";
|
import { randomBytes } from "../crypto/primitives/nacl-fast";
|
||||||
import { encodeCrock } from "../crypto/talerCrypto";
|
import { encodeCrock } from "../crypto/talerCrypto";
|
||||||
import { HttpResponseStatus } from "../util/http";
|
import { HttpResponseStatus } from "../util/http";
|
||||||
|
import { getTimestampNow } from "../util/time";
|
||||||
|
|
||||||
async function incrementPurchaseQueryRefundRetry(
|
async function incrementPurchaseQueryRefundRetry(
|
||||||
ws: InternalWalletState,
|
ws: InternalWalletState,
|
||||||
@ -288,7 +289,7 @@ export async function applyRefund(
|
|||||||
console.log("processing purchase for refund");
|
console.log("processing purchase for refund");
|
||||||
await startRefundQuery(ws, purchase.proposalId);
|
await startRefundQuery(ws, purchase.proposalId);
|
||||||
|
|
||||||
return purchase.contractTermsHash;
|
return purchase.contractData.contractTermsHash;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function processPurchaseQueryRefund(
|
export async function processPurchaseQueryRefund(
|
||||||
@ -334,9 +335,9 @@ async function processPurchaseQueryRefundImpl(
|
|||||||
|
|
||||||
const refundUrlObj = new URL(
|
const refundUrlObj = new URL(
|
||||||
"refund",
|
"refund",
|
||||||
purchase.contractTerms.merchant_base_url,
|
purchase.contractData.merchantBaseUrl,
|
||||||
);
|
);
|
||||||
refundUrlObj.searchParams.set("order_id", purchase.contractTerms.order_id);
|
refundUrlObj.searchParams.set("order_id", purchase.contractData.orderId);
|
||||||
const refundUrl = refundUrlObj.href;
|
const refundUrl = refundUrlObj.href;
|
||||||
let resp;
|
let resp;
|
||||||
try {
|
try {
|
||||||
@ -349,7 +350,7 @@ async function processPurchaseQueryRefundImpl(
|
|||||||
throw Error(`unexpected status code (${resp.status}) for /refund`);
|
throw Error(`unexpected status code (${resp.status}) for /refund`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const refundResponse = MerchantRefundResponse.checked(await resp.json());
|
const refundResponse = codecForMerchantRefundResponse().decode(await resp.json());
|
||||||
await acceptRefundResponse(
|
await acceptRefundResponse(
|
||||||
ws,
|
ws,
|
||||||
proposalId,
|
proposalId,
|
||||||
@ -409,8 +410,8 @@ async function processPurchaseApplyRefundImpl(
|
|||||||
const perm = info.perm;
|
const perm = info.perm;
|
||||||
const req: RefundRequest = {
|
const req: RefundRequest = {
|
||||||
coin_pub: perm.coin_pub,
|
coin_pub: perm.coin_pub,
|
||||||
h_contract_terms: purchase.contractTermsHash,
|
h_contract_terms: purchase.contractData.contractTermsHash,
|
||||||
merchant_pub: purchase.contractTerms.merchant_pub,
|
merchant_pub: purchase.contractData.merchantPub,
|
||||||
merchant_sig: perm.merchant_sig,
|
merchant_sig: perm.merchant_sig,
|
||||||
refund_amount: perm.refund_amount,
|
refund_amount: perm.refund_amount,
|
||||||
refund_fee: perm.refund_fee,
|
refund_fee: perm.refund_fee,
|
||||||
|
@ -17,7 +17,6 @@
|
|||||||
import {
|
import {
|
||||||
CreateReserveRequest,
|
CreateReserveRequest,
|
||||||
CreateReserveResponse,
|
CreateReserveResponse,
|
||||||
getTimestampNow,
|
|
||||||
ConfirmReserveRequest,
|
ConfirmReserveRequest,
|
||||||
OperationError,
|
OperationError,
|
||||||
AcceptWithdrawalResponse,
|
AcceptWithdrawalResponse,
|
||||||
@ -42,7 +41,7 @@ import {
|
|||||||
getExchangeTrust,
|
getExchangeTrust,
|
||||||
getExchangePaytoUri,
|
getExchangePaytoUri,
|
||||||
} from "./exchanges";
|
} from "./exchanges";
|
||||||
import { WithdrawOperationStatusResponse } from "../types/talerTypes";
|
import { WithdrawOperationStatusResponse, codecForWithdrawOperationStatusResponse } from "../types/talerTypes";
|
||||||
import { assertUnreachable } from "../util/assertUnreachable";
|
import { assertUnreachable } from "../util/assertUnreachable";
|
||||||
import { encodeCrock, getRandomBytes } from "../crypto/talerCrypto";
|
import { encodeCrock, getRandomBytes } from "../crypto/talerCrypto";
|
||||||
import { randomBytes } from "../crypto/primitives/nacl-fast";
|
import { randomBytes } from "../crypto/primitives/nacl-fast";
|
||||||
@ -57,6 +56,7 @@ import {
|
|||||||
} from "./errors";
|
} from "./errors";
|
||||||
import { NotificationType } from "../types/notifications";
|
import { NotificationType } from "../types/notifications";
|
||||||
import { codecForReserveStatus } from "../types/ReserveStatus";
|
import { codecForReserveStatus } from "../types/ReserveStatus";
|
||||||
|
import { getTimestampNow } from "../util/time";
|
||||||
|
|
||||||
const logger = new Logger("reserves.ts");
|
const logger = new Logger("reserves.ts");
|
||||||
|
|
||||||
@ -289,7 +289,7 @@ async function processReserveBankStatusImpl(
|
|||||||
`unexpected status ${statusResp.status} for bank status query`,
|
`unexpected status ${statusResp.status} for bank status query`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
status = WithdrawOperationStatusResponse.checked(await statusResp.json());
|
status = codecForWithdrawOperationStatusResponse().decode(await statusResp.json());
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
@ -390,6 +390,7 @@ async function updateReserve(
|
|||||||
let resp;
|
let resp;
|
||||||
try {
|
try {
|
||||||
resp = await ws.http.get(reqUrl.href);
|
resp = await ws.http.get(reqUrl.href);
|
||||||
|
console.log("got reserve/status response", await resp.json());
|
||||||
if (resp.status === 404) {
|
if (resp.status === 404) {
|
||||||
const m = "The exchange does not know about this reserve (yet).";
|
const m = "The exchange does not know about this reserve (yet).";
|
||||||
await incrementReserveRetry(ws, reservePub, undefined);
|
await incrementReserveRetry(ws, reservePub, undefined);
|
||||||
@ -408,7 +409,7 @@ async function updateReserve(
|
|||||||
throw new OperationFailedAndReportedError(m);
|
throw new OperationFailedAndReportedError(m);
|
||||||
}
|
}
|
||||||
const respJson = await resp.json();
|
const respJson = await resp.json();
|
||||||
const reserveInfo = codecForReserveStatus.decode(respJson);
|
const reserveInfo = codecForReserveStatus().decode(respJson);
|
||||||
const balance = Amounts.parseOrThrow(reserveInfo.balance);
|
const balance = Amounts.parseOrThrow(reserveInfo.balance);
|
||||||
await ws.db.runWithWriteTransaction(
|
await ws.db.runWithWriteTransaction(
|
||||||
[Stores.reserves, Stores.reserveUpdatedEvents],
|
[Stores.reserves, Stores.reserveUpdatedEvents],
|
||||||
|
@ -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 { parseTipUri } from "../util/taleruri";
|
||||||
import {
|
import {
|
||||||
TipStatus,
|
TipStatus,
|
||||||
getTimestampNow,
|
|
||||||
OperationError,
|
OperationError,
|
||||||
} from "../types/walletTypes";
|
} from "../types/walletTypes";
|
||||||
import {
|
import {
|
||||||
TipPickupGetResponse,
|
TipPickupGetResponse,
|
||||||
TipPlanchetDetail,
|
TipPlanchetDetail,
|
||||||
TipResponse,
|
TipResponse,
|
||||||
|
codecForTipPickupGetResponse,
|
||||||
|
codecForTipResponse,
|
||||||
} from "../types/talerTypes";
|
} from "../types/talerTypes";
|
||||||
import * as Amounts from "../util/amounts";
|
import * as Amounts from "../util/amounts";
|
||||||
import {
|
import {
|
||||||
@ -39,11 +40,11 @@ import {
|
|||||||
getVerifiedWithdrawDenomList,
|
getVerifiedWithdrawDenomList,
|
||||||
processWithdrawSession,
|
processWithdrawSession,
|
||||||
} from "./withdraw";
|
} from "./withdraw";
|
||||||
import { getTalerStampSec, extractTalerStampOrThrow } from "../util/helpers";
|
|
||||||
import { updateExchangeFromUrl } from "./exchanges";
|
import { updateExchangeFromUrl } from "./exchanges";
|
||||||
import { getRandomBytes, encodeCrock } from "../crypto/talerCrypto";
|
import { getRandomBytes, encodeCrock } from "../crypto/talerCrypto";
|
||||||
import { guardOperationException } from "./errors";
|
import { guardOperationException } from "./errors";
|
||||||
import { NotificationType } from "../types/notifications";
|
import { NotificationType } from "../types/notifications";
|
||||||
|
import { getTimestampNow } from "../util/time";
|
||||||
|
|
||||||
export async function getTipStatus(
|
export async function getTipStatus(
|
||||||
ws: InternalWalletState,
|
ws: InternalWalletState,
|
||||||
@ -63,7 +64,7 @@ export async function getTipStatus(
|
|||||||
}
|
}
|
||||||
const respJson = await merchantResp.json();
|
const respJson = await merchantResp.json();
|
||||||
console.log("resp:", respJson);
|
console.log("resp:", respJson);
|
||||||
const tipPickupStatus = TipPickupGetResponse.checked(respJson);
|
const tipPickupStatus = codecForTipPickupGetResponse().decode(respJson);
|
||||||
|
|
||||||
console.log("status", tipPickupStatus);
|
console.log("status", tipPickupStatus);
|
||||||
|
|
||||||
@ -88,7 +89,7 @@ export async function getTipStatus(
|
|||||||
acceptedTimestamp: undefined,
|
acceptedTimestamp: undefined,
|
||||||
rejectedTimestamp: undefined,
|
rejectedTimestamp: undefined,
|
||||||
amount,
|
amount,
|
||||||
deadline: extractTalerStampOrThrow(tipPickupStatus.stamp_expire),
|
deadline: tipPickupStatus.stamp_expire,
|
||||||
exchangeUrl: tipPickupStatus.exchange_url,
|
exchangeUrl: tipPickupStatus.exchange_url,
|
||||||
merchantBaseUrl: res.merchantBaseUrl,
|
merchantBaseUrl: res.merchantBaseUrl,
|
||||||
nextUrl: undefined,
|
nextUrl: undefined,
|
||||||
@ -115,8 +116,8 @@ export async function getTipStatus(
|
|||||||
nextUrl: tipPickupStatus.extra.next_url,
|
nextUrl: tipPickupStatus.extra.next_url,
|
||||||
merchantOrigin: res.merchantOrigin,
|
merchantOrigin: res.merchantOrigin,
|
||||||
merchantTipId: res.merchantTipId,
|
merchantTipId: res.merchantTipId,
|
||||||
expirationTimestamp: getTalerStampSec(tipPickupStatus.stamp_expire)!,
|
expirationTimestamp: tipPickupStatus.stamp_expire,
|
||||||
timestamp: getTalerStampSec(tipPickupStatus.stamp_created)!,
|
timestamp: tipPickupStatus.stamp_created,
|
||||||
totalFees: tipRecord.totalFees,
|
totalFees: tipRecord.totalFees,
|
||||||
tipId: tipRecord.tipId,
|
tipId: tipRecord.tipId,
|
||||||
};
|
};
|
||||||
@ -240,7 +241,7 @@ async function processTipImpl(
|
|||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = TipResponse.checked(await merchantResp.json());
|
const response = codecForTipResponse().decode(await merchantResp.json());
|
||||||
|
|
||||||
if (response.reserve_sigs.length !== tipRecord.planchets.length) {
|
if (response.reserve_sigs.length !== tipRecord.planchets.length) {
|
||||||
throw Error("number of tip responses does not match requested planchets");
|
throw Error("number of tip responses does not match requested planchets");
|
||||||
|
@ -27,33 +27,39 @@ import {
|
|||||||
} from "../types/dbTypes";
|
} from "../types/dbTypes";
|
||||||
import * as Amounts from "../util/amounts";
|
import * as Amounts from "../util/amounts";
|
||||||
import {
|
import {
|
||||||
getTimestampNow,
|
|
||||||
AcceptWithdrawalResponse,
|
|
||||||
BankWithdrawDetails,
|
BankWithdrawDetails,
|
||||||
ExchangeWithdrawDetails,
|
ExchangeWithdrawDetails,
|
||||||
WithdrawDetails,
|
WithdrawDetails,
|
||||||
OperationError,
|
OperationError,
|
||||||
} from "../types/walletTypes";
|
} from "../types/walletTypes";
|
||||||
import { WithdrawOperationStatusResponse } from "../types/talerTypes";
|
import { WithdrawOperationStatusResponse, codecForWithdrawOperationStatusResponse } from "../types/talerTypes";
|
||||||
import { InternalWalletState } from "./state";
|
import { InternalWalletState } from "./state";
|
||||||
import { parseWithdrawUri } from "../util/taleruri";
|
import { parseWithdrawUri } from "../util/taleruri";
|
||||||
import { Logger } from "../util/logging";
|
import { Logger } from "../util/logging";
|
||||||
import {
|
import { updateExchangeFromUrl, getExchangeTrust } from "./exchanges";
|
||||||
updateExchangeFromUrl,
|
|
||||||
getExchangeTrust,
|
|
||||||
} from "./exchanges";
|
|
||||||
import { WALLET_EXCHANGE_PROTOCOL_VERSION } from "./versions";
|
import { WALLET_EXCHANGE_PROTOCOL_VERSION } from "./versions";
|
||||||
|
|
||||||
import * as LibtoolVersion from "../util/libtoolVersion";
|
import * as LibtoolVersion from "../util/libtoolVersion";
|
||||||
import { guardOperationException } from "./errors";
|
import { guardOperationException } from "./errors";
|
||||||
import { NotificationType } from "../types/notifications";
|
import { NotificationType } from "../types/notifications";
|
||||||
|
import {
|
||||||
|
getTimestampNow,
|
||||||
|
getDurationRemaining,
|
||||||
|
timestampCmp,
|
||||||
|
timestampSubtractDuraction,
|
||||||
|
} from "../util/time";
|
||||||
|
|
||||||
const logger = new Logger("withdraw.ts");
|
const logger = new Logger("withdraw.ts");
|
||||||
|
|
||||||
function isWithdrawableDenom(d: DenominationRecord) {
|
function isWithdrawableDenom(d: DenominationRecord) {
|
||||||
const now = getTimestampNow();
|
const now = getTimestampNow();
|
||||||
const started = now.t_ms >= d.stampStart.t_ms;
|
const started = timestampCmp(now, d.stampStart) >= 0;
|
||||||
const stillOkay = d.stampExpireWithdraw.t_ms + 60 * 1000 > now.t_ms;
|
const lastPossibleWithdraw = timestampSubtractDuraction(
|
||||||
|
d.stampExpireWithdraw,
|
||||||
|
{ d_ms: 50 * 1000 },
|
||||||
|
);
|
||||||
|
const remaining = getDurationRemaining(lastPossibleWithdraw, now);
|
||||||
|
const stillOkay = remaining.d_ms !== 0;
|
||||||
return started && stillOkay;
|
return started && stillOkay;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -108,11 +114,14 @@ export async function getBankWithdrawalInfo(
|
|||||||
}
|
}
|
||||||
const resp = await ws.http.get(uriResult.statusUrl);
|
const resp = await ws.http.get(uriResult.statusUrl);
|
||||||
if (resp.status !== 200) {
|
if (resp.status !== 200) {
|
||||||
throw Error(`unexpected status (${resp.status}) from bank for ${uriResult.statusUrl}`);
|
throw Error(
|
||||||
|
`unexpected status (${resp.status}) from bank for ${uriResult.statusUrl}`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
const respJson = await resp.json();
|
const respJson = await resp.json();
|
||||||
console.log("resp:", respJson);
|
console.log("resp:", respJson);
|
||||||
const status = WithdrawOperationStatusResponse.checked(respJson);
|
|
||||||
|
const status = codecForWithdrawOperationStatusResponse().decode(respJson);
|
||||||
return {
|
return {
|
||||||
amount: Amounts.parseOrThrow(status.amount),
|
amount: Amounts.parseOrThrow(status.amount),
|
||||||
confirmTransferUrl: status.confirm_transfer_url,
|
confirmTransferUrl: status.confirm_transfer_url,
|
||||||
@ -125,15 +134,13 @@ export async function getBankWithdrawalInfo(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
async function getPossibleDenoms(
|
async function getPossibleDenoms(
|
||||||
ws: InternalWalletState,
|
ws: InternalWalletState,
|
||||||
exchangeBaseUrl: string,
|
exchangeBaseUrl: string,
|
||||||
): Promise<DenominationRecord[]> {
|
): Promise<DenominationRecord[]> {
|
||||||
return await ws.db.iterIndex(
|
return await ws.db
|
||||||
Stores.denominations.exchangeBaseUrlIndex,
|
.iterIndex(Stores.denominations.exchangeBaseUrlIndex, exchangeBaseUrl)
|
||||||
exchangeBaseUrl,
|
.filter(d => {
|
||||||
).filter(d => {
|
|
||||||
return (
|
return (
|
||||||
d.status === DenominationStatus.Unverified ||
|
d.status === DenominationStatus.Unverified ||
|
||||||
d.status === DenominationStatus.VerifiedGood
|
d.status === DenominationStatus.VerifiedGood
|
||||||
@ -204,8 +211,11 @@ async function processPlanchet(
|
|||||||
planchet.denomPub,
|
planchet.denomPub,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const isValid = await ws.cryptoApi.rsaVerify(
|
||||||
const isValid = await ws.cryptoApi.rsaVerify(planchet.coinPub, denomSig, planchet.denomPub);
|
planchet.coinPub,
|
||||||
|
denomSig,
|
||||||
|
planchet.denomPub,
|
||||||
|
);
|
||||||
if (!isValid) {
|
if (!isValid) {
|
||||||
throw Error("invalid RSA signature by the exchange");
|
throw Error("invalid RSA signature by the exchange");
|
||||||
}
|
}
|
||||||
@ -261,7 +271,10 @@ async function processPlanchet(
|
|||||||
r.amountWithdrawCompleted,
|
r.amountWithdrawCompleted,
|
||||||
Amounts.add(denom.value, denom.feeWithdraw).amount,
|
Amounts.add(denom.value, denom.feeWithdraw).amount,
|
||||||
).amount;
|
).amount;
|
||||||
if (Amounts.cmp(r.amountWithdrawCompleted, r.amountWithdrawAllocated) == 0) {
|
if (
|
||||||
|
Amounts.cmp(r.amountWithdrawCompleted, r.amountWithdrawAllocated) ==
|
||||||
|
0
|
||||||
|
) {
|
||||||
reserveDepleted = true;
|
reserveDepleted = true;
|
||||||
}
|
}
|
||||||
await tx.put(Stores.reserves, r);
|
await tx.put(Stores.reserves, r);
|
||||||
@ -436,10 +449,10 @@ async function processWithdrawCoin(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const coin = await ws.db.getIndexed(
|
const coin = await ws.db.getIndexed(Stores.coins.byWithdrawalWithIdx, [
|
||||||
Stores.coins.byWithdrawalWithIdx,
|
withdrawalSessionId,
|
||||||
[withdrawalSessionId, coinIndex],
|
coinIndex,
|
||||||
);
|
]);
|
||||||
|
|
||||||
if (coin) {
|
if (coin) {
|
||||||
console.log("coin already exists");
|
console.log("coin already exists");
|
||||||
@ -494,7 +507,7 @@ async function resetWithdrawSessionRetry(
|
|||||||
ws: InternalWalletState,
|
ws: InternalWalletState,
|
||||||
withdrawalSessionId: string,
|
withdrawalSessionId: string,
|
||||||
) {
|
) {
|
||||||
await ws.db.mutate(Stores.withdrawalSession, withdrawalSessionId, (x) => {
|
await ws.db.mutate(Stores.withdrawalSession, withdrawalSessionId, x => {
|
||||||
if (x.retryInfo.active) {
|
if (x.retryInfo.active) {
|
||||||
x.retryInfo = initRetryInfo();
|
x.retryInfo = initRetryInfo();
|
||||||
}
|
}
|
||||||
@ -570,16 +583,12 @@ export async function getExchangeWithdrawalInfo(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const possibleDenoms = await ws.db.iterIndex(
|
const possibleDenoms = await ws.db
|
||||||
Stores.denominations.exchangeBaseUrlIndex,
|
.iterIndex(Stores.denominations.exchangeBaseUrlIndex, baseUrl)
|
||||||
baseUrl,
|
.filter(d => d.isOffered);
|
||||||
).filter(d => d.isOffered);
|
|
||||||
|
|
||||||
const trustedAuditorPubs = [];
|
const trustedAuditorPubs = [];
|
||||||
const currencyRecord = await ws.db.get(
|
const currencyRecord = await ws.db.get(Stores.currencies, amount.currency);
|
||||||
Stores.currencies,
|
|
||||||
amount.currency,
|
|
||||||
);
|
|
||||||
if (currencyRecord) {
|
if (currencyRecord) {
|
||||||
trustedAuditorPubs.push(...currencyRecord.auditors.map(a => a.auditorPub));
|
trustedAuditorPubs.push(...currencyRecord.auditors.map(a => a.auditorPub));
|
||||||
}
|
}
|
||||||
@ -606,7 +615,10 @@ export async function getExchangeWithdrawalInfo(
|
|||||||
let tosAccepted = false;
|
let tosAccepted = false;
|
||||||
|
|
||||||
if (exchangeInfo.termsOfServiceAcceptedTimestamp) {
|
if (exchangeInfo.termsOfServiceAcceptedTimestamp) {
|
||||||
if (exchangeInfo.termsOfServiceAcceptedEtag == exchangeInfo.termsOfServiceLastEtag) {
|
if (
|
||||||
|
exchangeInfo.termsOfServiceAcceptedEtag ==
|
||||||
|
exchangeInfo.termsOfServiceLastEtag
|
||||||
|
) {
|
||||||
tosAccepted = true;
|
tosAccepted = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -29,10 +29,11 @@ import {
|
|||||||
makeCodecForUnion,
|
makeCodecForUnion,
|
||||||
makeCodecForList,
|
makeCodecForList,
|
||||||
} from "../util/codec";
|
} from "../util/codec";
|
||||||
import { runBlock } from "../util/helpers";
|
|
||||||
import { AmountString } from "./talerTypes";
|
import { AmountString } from "./talerTypes";
|
||||||
import { ReserveTransaction, codecForReserveTransaction } from "./ReserveTransaction";
|
import {
|
||||||
|
ReserveTransaction,
|
||||||
|
codecForReserveTransaction,
|
||||||
|
} from "./ReserveTransaction";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Status of a reserve.
|
* Status of a reserve.
|
||||||
@ -51,11 +52,10 @@ export interface ReserveStatus {
|
|||||||
history: ReserveTransaction[];
|
history: ReserveTransaction[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export const codecForReserveStatus = runBlock(() => (
|
export const codecForReserveStatus = () =>
|
||||||
typecheckedCodec<ReserveStatus>(
|
typecheckedCodec<ReserveStatus>(
|
||||||
makeCodecForObject<ReserveStatus>()
|
makeCodecForObject<ReserveStatus>()
|
||||||
.property("balance", codecForString)
|
.property("balance", codecForString)
|
||||||
.property("history", makeCodecForList(codecForReserveTransaction))
|
.property("history", makeCodecForList(codecForReserveTransaction()))
|
||||||
.build("ReserveStatus")
|
.build("ReserveStatus"),
|
||||||
)
|
);
|
||||||
));
|
|
||||||
|
@ -28,15 +28,14 @@ import {
|
|||||||
makeCodecForConstString,
|
makeCodecForConstString,
|
||||||
makeCodecForUnion,
|
makeCodecForUnion,
|
||||||
} from "../util/codec";
|
} from "../util/codec";
|
||||||
import { runBlock } from "../util/helpers";
|
|
||||||
import {
|
import {
|
||||||
AmountString,
|
AmountString,
|
||||||
Base32String,
|
Base32String,
|
||||||
EddsaSignatureString,
|
EddsaSignatureString,
|
||||||
TimestampString,
|
|
||||||
EddsaPublicKeyString,
|
EddsaPublicKeyString,
|
||||||
CoinPublicKeyString,
|
CoinPublicKeyString,
|
||||||
} from "./talerTypes";
|
} from "./talerTypes";
|
||||||
|
import { Timestamp, codecForTimestamp } from "../util/time";
|
||||||
|
|
||||||
export const enum ReserveTransactionType {
|
export const enum ReserveTransactionType {
|
||||||
Withdraw = "WITHDRAW",
|
Withdraw = "WITHDRAW",
|
||||||
@ -96,7 +95,7 @@ export interface ReserveDepositTransaction {
|
|||||||
/**
|
/**
|
||||||
* Timestamp of the incoming wire transfer.
|
* Timestamp of the incoming wire transfer.
|
||||||
*/
|
*/
|
||||||
timestamp: TimestampString;
|
timestamp: Timestamp;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ReserveClosingTransaction {
|
export interface ReserveClosingTransaction {
|
||||||
@ -137,7 +136,7 @@ export interface ReserveClosingTransaction {
|
|||||||
/**
|
/**
|
||||||
* Time when the reserve was closed.
|
* Time when the reserve was closed.
|
||||||
*/
|
*/
|
||||||
timestamp: TimestampString;
|
timestamp: Timestamp;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ReservePaybackTransaction {
|
export interface ReservePaybackTransaction {
|
||||||
@ -173,7 +172,7 @@ export interface ReservePaybackTransaction {
|
|||||||
/**
|
/**
|
||||||
* Time when the funds were paid back into the reserve.
|
* Time when the funds were paid back into the reserve.
|
||||||
*/
|
*/
|
||||||
timestamp: TimestampString;
|
timestamp: Timestamp;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Public key of the coin that was paid back.
|
* Public key of the coin that was paid back.
|
||||||
@ -190,7 +189,7 @@ export type ReserveTransaction =
|
|||||||
| ReserveClosingTransaction
|
| ReserveClosingTransaction
|
||||||
| ReservePaybackTransaction;
|
| ReservePaybackTransaction;
|
||||||
|
|
||||||
export const codecForReserveWithdrawTransaction = runBlock(() =>
|
export const codecForReserveWithdrawTransaction = () =>
|
||||||
typecheckedCodec<ReserveWithdrawTransaction>(
|
typecheckedCodec<ReserveWithdrawTransaction>(
|
||||||
makeCodecForObject<ReserveWithdrawTransaction>()
|
makeCodecForObject<ReserveWithdrawTransaction>()
|
||||||
.property("amount", codecForString)
|
.property("amount", codecForString)
|
||||||
@ -203,22 +202,20 @@ export const codecForReserveWithdrawTransaction = runBlock(() =>
|
|||||||
)
|
)
|
||||||
.property("withdraw_fee", codecForString)
|
.property("withdraw_fee", codecForString)
|
||||||
.build("ReserveWithdrawTransaction"),
|
.build("ReserveWithdrawTransaction"),
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
export const codecForReserveDepositTransaction = runBlock(() =>
|
export const codecForReserveDepositTransaction = () =>
|
||||||
typecheckedCodec<ReserveDepositTransaction>(
|
typecheckedCodec<ReserveDepositTransaction>(
|
||||||
makeCodecForObject<ReserveDepositTransaction>()
|
makeCodecForObject<ReserveDepositTransaction>()
|
||||||
.property("amount", codecForString)
|
.property("amount", codecForString)
|
||||||
.property("sender_account_url", codecForString)
|
.property("sender_account_url", codecForString)
|
||||||
.property("timestamp", codecForString)
|
.property("timestamp", codecForTimestamp)
|
||||||
.property("wire_reference", codecForString)
|
.property("wire_reference", codecForString)
|
||||||
.property("type", makeCodecForConstString(ReserveTransactionType.Deposit))
|
.property("type", makeCodecForConstString(ReserveTransactionType.Deposit))
|
||||||
.build("ReserveDepositTransaction"),
|
.build("ReserveDepositTransaction"),
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
export const codecForReserveClosingTransaction = runBlock(() =>
|
export const codecForReserveClosingTransaction = () =>
|
||||||
typecheckedCodec<ReserveClosingTransaction>(
|
typecheckedCodec<ReserveClosingTransaction>(
|
||||||
makeCodecForObject<ReserveClosingTransaction>()
|
makeCodecForObject<ReserveClosingTransaction>()
|
||||||
.property("amount", codecForString)
|
.property("amount", codecForString)
|
||||||
@ -226,14 +223,13 @@ export const codecForReserveClosingTransaction = runBlock(() =>
|
|||||||
.property("exchange_pub", codecForString)
|
.property("exchange_pub", codecForString)
|
||||||
.property("exchange_sig", codecForString)
|
.property("exchange_sig", codecForString)
|
||||||
.property("h_wire", codecForString)
|
.property("h_wire", codecForString)
|
||||||
.property("timestamp", codecForString)
|
.property("timestamp", codecForTimestamp)
|
||||||
.property("type", makeCodecForConstString(ReserveTransactionType.Closing))
|
.property("type", makeCodecForConstString(ReserveTransactionType.Closing))
|
||||||
.property("wtid", codecForString)
|
.property("wtid", codecForString)
|
||||||
.build("ReserveClosingTransaction"),
|
.build("ReserveClosingTransaction"),
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
export const codecForReservePaybackTransaction = runBlock(() =>
|
export const codecForReservePaybackTransaction = () =>
|
||||||
typecheckedCodec<ReservePaybackTransaction>(
|
typecheckedCodec<ReservePaybackTransaction>(
|
||||||
makeCodecForObject<ReservePaybackTransaction>()
|
makeCodecForObject<ReservePaybackTransaction>()
|
||||||
.property("amount", codecForString)
|
.property("amount", codecForString)
|
||||||
@ -241,33 +237,31 @@ export const codecForReservePaybackTransaction = runBlock(() =>
|
|||||||
.property("exchange_pub", codecForString)
|
.property("exchange_pub", codecForString)
|
||||||
.property("exchange_sig", codecForString)
|
.property("exchange_sig", codecForString)
|
||||||
.property("receiver_account_details", codecForString)
|
.property("receiver_account_details", codecForString)
|
||||||
.property("timestamp", codecForString)
|
.property("timestamp", codecForTimestamp)
|
||||||
.property("type", makeCodecForConstString(ReserveTransactionType.Payback))
|
.property("type", makeCodecForConstString(ReserveTransactionType.Payback))
|
||||||
.property("wire_transfer", codecForString)
|
.property("wire_transfer", codecForString)
|
||||||
.build("ReservePaybackTransaction"),
|
.build("ReservePaybackTransaction"),
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
export const codecForReserveTransaction = runBlock(() =>
|
export const codecForReserveTransaction = () =>
|
||||||
typecheckedCodec<ReserveTransaction>(
|
typecheckedCodec<ReserveTransaction>(
|
||||||
makeCodecForUnion<ReserveTransaction>()
|
makeCodecForUnion<ReserveTransaction>()
|
||||||
.discriminateOn("type")
|
.discriminateOn("type")
|
||||||
.alternative(
|
.alternative(
|
||||||
ReserveTransactionType.Withdraw,
|
ReserveTransactionType.Withdraw,
|
||||||
codecForReserveWithdrawTransaction,
|
codecForReserveWithdrawTransaction(),
|
||||||
)
|
)
|
||||||
.alternative(
|
.alternative(
|
||||||
ReserveTransactionType.Closing,
|
ReserveTransactionType.Closing,
|
||||||
codecForReserveClosingTransaction,
|
codecForReserveClosingTransaction(),
|
||||||
)
|
)
|
||||||
.alternative(
|
.alternative(
|
||||||
ReserveTransactionType.Payback,
|
ReserveTransactionType.Payback,
|
||||||
codecForReservePaybackTransaction,
|
codecForReservePaybackTransaction(),
|
||||||
)
|
)
|
||||||
.alternative(
|
.alternative(
|
||||||
ReserveTransactionType.Deposit,
|
ReserveTransactionType.Deposit,
|
||||||
codecForReserveDepositTransaction,
|
codecForReserveDepositTransaction(),
|
||||||
)
|
)
|
||||||
.build<ReserveTransaction>("ReserveTransaction"),
|
.build<ReserveTransaction>("ReserveTransaction"),
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
@ -24,7 +24,6 @@
|
|||||||
* Imports.
|
* Imports.
|
||||||
*/
|
*/
|
||||||
import { AmountJson } from "../util/amounts";
|
import { AmountJson } from "../util/amounts";
|
||||||
import { Checkable } from "../util/checkable";
|
|
||||||
import {
|
import {
|
||||||
Auditor,
|
Auditor,
|
||||||
CoinPaySig,
|
CoinPaySig,
|
||||||
@ -33,17 +32,16 @@ import {
|
|||||||
MerchantRefundPermission,
|
MerchantRefundPermission,
|
||||||
PayReq,
|
PayReq,
|
||||||
TipResponse,
|
TipResponse,
|
||||||
|
ExchangeHandle,
|
||||||
} from "./talerTypes";
|
} from "./talerTypes";
|
||||||
|
|
||||||
import { Index, Store } from "../util/query";
|
import { Index, Store } from "../util/query";
|
||||||
import {
|
import {
|
||||||
Timestamp,
|
|
||||||
OperationError,
|
OperationError,
|
||||||
Duration,
|
|
||||||
getTimestampNow,
|
|
||||||
RefreshReason,
|
RefreshReason,
|
||||||
} from "./walletTypes";
|
} from "./walletTypes";
|
||||||
import { ReserveTransaction } from "./ReserveTransaction";
|
import { ReserveTransaction } from "./ReserveTransaction";
|
||||||
|
import { Timestamp, Duration, getTimestampNow } from "../util/time";
|
||||||
|
|
||||||
export enum ReserveRecordStatus {
|
export enum ReserveRecordStatus {
|
||||||
/**
|
/**
|
||||||
@ -104,6 +102,13 @@ export function updateRetryInfoTimeout(
|
|||||||
p: RetryPolicy = defaultRetryPolicy,
|
p: RetryPolicy = defaultRetryPolicy,
|
||||||
): void {
|
): void {
|
||||||
const now = getTimestampNow();
|
const now = getTimestampNow();
|
||||||
|
if (now.t_ms === "never") {
|
||||||
|
throw Error("assertion failed");
|
||||||
|
}
|
||||||
|
if (p.backoffDelta.d_ms === "forever") {
|
||||||
|
r.nextRetry = { t_ms: "never" };
|
||||||
|
return;
|
||||||
|
}
|
||||||
const t =
|
const t =
|
||||||
now.t_ms + p.backoffDelta.d_ms * Math.pow(p.backoffBase, r.retryCounter);
|
now.t_ms + p.backoffDelta.d_ms * Math.pow(p.backoffBase, r.retryCounter);
|
||||||
r.nextRetry = { t_ms: t };
|
r.nextRetry = { t_ms: t };
|
||||||
@ -319,86 +324,72 @@ export enum DenominationStatus {
|
|||||||
/**
|
/**
|
||||||
* Denomination record as stored in the wallet's database.
|
* Denomination record as stored in the wallet's database.
|
||||||
*/
|
*/
|
||||||
@Checkable.Class()
|
export interface DenominationRecord {
|
||||||
export class DenominationRecord {
|
|
||||||
/**
|
/**
|
||||||
* Value of one coin of the denomination.
|
* Value of one coin of the denomination.
|
||||||
*/
|
*/
|
||||||
@Checkable.Value(() => AmountJson)
|
|
||||||
value: AmountJson;
|
value: AmountJson;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The denomination public key.
|
* The denomination public key.
|
||||||
*/
|
*/
|
||||||
@Checkable.String()
|
|
||||||
denomPub: string;
|
denomPub: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Hash of the denomination public key.
|
* Hash of the denomination public key.
|
||||||
* Stored in the database for faster lookups.
|
* Stored in the database for faster lookups.
|
||||||
*/
|
*/
|
||||||
@Checkable.String()
|
|
||||||
denomPubHash: string;
|
denomPubHash: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fee for withdrawing.
|
* Fee for withdrawing.
|
||||||
*/
|
*/
|
||||||
@Checkable.Value(() => AmountJson)
|
|
||||||
feeWithdraw: AmountJson;
|
feeWithdraw: AmountJson;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fee for depositing.
|
* Fee for depositing.
|
||||||
*/
|
*/
|
||||||
@Checkable.Value(() => AmountJson)
|
|
||||||
feeDeposit: AmountJson;
|
feeDeposit: AmountJson;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fee for refreshing.
|
* Fee for refreshing.
|
||||||
*/
|
*/
|
||||||
@Checkable.Value(() => AmountJson)
|
|
||||||
feeRefresh: AmountJson;
|
feeRefresh: AmountJson;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fee for refunding.
|
* Fee for refunding.
|
||||||
*/
|
*/
|
||||||
@Checkable.Value(() => AmountJson)
|
|
||||||
feeRefund: AmountJson;
|
feeRefund: AmountJson;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validity start date of the denomination.
|
* Validity start date of the denomination.
|
||||||
*/
|
*/
|
||||||
@Checkable.Value(() => Timestamp)
|
|
||||||
stampStart: Timestamp;
|
stampStart: Timestamp;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Date after which the currency can't be withdrawn anymore.
|
* Date after which the currency can't be withdrawn anymore.
|
||||||
*/
|
*/
|
||||||
@Checkable.Value(() => Timestamp)
|
|
||||||
stampExpireWithdraw: Timestamp;
|
stampExpireWithdraw: Timestamp;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Date after the denomination officially doesn't exist anymore.
|
* Date after the denomination officially doesn't exist anymore.
|
||||||
*/
|
*/
|
||||||
@Checkable.Value(() => Timestamp)
|
|
||||||
stampExpireLegal: Timestamp;
|
stampExpireLegal: Timestamp;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Data after which coins of this denomination can't be deposited anymore.
|
* Data after which coins of this denomination can't be deposited anymore.
|
||||||
*/
|
*/
|
||||||
@Checkable.Value(() => Timestamp)
|
|
||||||
stampExpireDeposit: Timestamp;
|
stampExpireDeposit: Timestamp;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Signature by the exchange's master key over the denomination
|
* Signature by the exchange's master key over the denomination
|
||||||
* information.
|
* information.
|
||||||
*/
|
*/
|
||||||
@Checkable.String()
|
|
||||||
masterSig: string;
|
masterSig: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Did we verify the signature on the denomination?
|
* Did we verify the signature on the denomination?
|
||||||
*/
|
*/
|
||||||
@Checkable.Number()
|
|
||||||
status: DenominationStatus;
|
status: DenominationStatus;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -406,20 +397,12 @@ export class DenominationRecord {
|
|||||||
* we checked?
|
* we checked?
|
||||||
* Only false when the exchange redacts a previously published denomination.
|
* Only false when the exchange redacts a previously published denomination.
|
||||||
*/
|
*/
|
||||||
@Checkable.Boolean()
|
|
||||||
isOffered: boolean;
|
isOffered: boolean;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Base URL of the exchange.
|
* Base URL of the exchange.
|
||||||
*/
|
*/
|
||||||
@Checkable.String()
|
|
||||||
exchangeBaseUrl: string;
|
exchangeBaseUrl: string;
|
||||||
|
|
||||||
/**
|
|
||||||
* Verify that a value matches the schema of this class and convert it into a
|
|
||||||
* member.
|
|
||||||
*/
|
|
||||||
static checked: (obj: any) => Denomination;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -713,36 +696,21 @@ export const enum ProposalStatus {
|
|||||||
REPURCHASE = "repurchase",
|
REPURCHASE = "repurchase",
|
||||||
}
|
}
|
||||||
|
|
||||||
@Checkable.Class()
|
export interface ProposalDownload {
|
||||||
export class ProposalDownload {
|
|
||||||
/**
|
/**
|
||||||
* The contract that was offered by the merchant.
|
* The contract that was offered by the merchant.
|
||||||
*/
|
*/
|
||||||
@Checkable.Value(() => ContractTerms)
|
contractTermsRaw: string;
|
||||||
contractTerms: ContractTerms;
|
|
||||||
|
|
||||||
/**
|
contractData: WalletContractData;
|
||||||
* Signature by the merchant over the contract details.
|
|
||||||
*/
|
|
||||||
@Checkable.String()
|
|
||||||
merchantSig: string;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Signature by the merchant over the contract details.
|
|
||||||
*/
|
|
||||||
@Checkable.String()
|
|
||||||
contractTermsHash: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Record for a downloaded order, stored in the wallet's database.
|
* Record for a downloaded order, stored in the wallet's database.
|
||||||
*/
|
*/
|
||||||
@Checkable.Class()
|
export interface ProposalRecord {
|
||||||
export class ProposalRecord {
|
|
||||||
@Checkable.String()
|
|
||||||
orderId: string;
|
orderId: string;
|
||||||
|
|
||||||
@Checkable.String()
|
|
||||||
merchantBaseUrl: string;
|
merchantBaseUrl: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -753,38 +721,31 @@ export class ProposalRecord {
|
|||||||
/**
|
/**
|
||||||
* Unique ID when the order is stored in the wallet DB.
|
* Unique ID when the order is stored in the wallet DB.
|
||||||
*/
|
*/
|
||||||
@Checkable.String()
|
|
||||||
proposalId: string;
|
proposalId: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Timestamp (in ms) of when the record
|
* Timestamp (in ms) of when the record
|
||||||
* was created.
|
* was created.
|
||||||
*/
|
*/
|
||||||
@Checkable.Number()
|
|
||||||
timestamp: Timestamp;
|
timestamp: Timestamp;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Private key for the nonce.
|
* Private key for the nonce.
|
||||||
*/
|
*/
|
||||||
@Checkable.String()
|
|
||||||
noncePriv: string;
|
noncePriv: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Public key for the nonce.
|
* Public key for the nonce.
|
||||||
*/
|
*/
|
||||||
@Checkable.String()
|
|
||||||
noncePub: string;
|
noncePub: string;
|
||||||
|
|
||||||
@Checkable.String()
|
|
||||||
proposalStatus: ProposalStatus;
|
proposalStatus: ProposalStatus;
|
||||||
|
|
||||||
@Checkable.String()
|
|
||||||
repurchaseProposalId: string | undefined;
|
repurchaseProposalId: string | undefined;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Session ID we got when downloading the contract.
|
* Session ID we got when downloading the contract.
|
||||||
*/
|
*/
|
||||||
@Checkable.Optional(Checkable.String())
|
|
||||||
downloadSessionId?: string;
|
downloadSessionId?: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -793,12 +754,6 @@ export class ProposalRecord {
|
|||||||
*/
|
*/
|
||||||
retryInfo: RetryInfo;
|
retryInfo: RetryInfo;
|
||||||
|
|
||||||
/**
|
|
||||||
* Verify that a value matches the schema of this class and convert it into a
|
|
||||||
* member.
|
|
||||||
*/
|
|
||||||
static checked: (obj: any) => ProposalRecord;
|
|
||||||
|
|
||||||
lastError: OperationError | undefined;
|
lastError: OperationError | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1120,6 +1075,38 @@ export interface ReserveUpdatedEventRecord {
|
|||||||
newHistoryTransactions: ReserveTransaction[];
|
newHistoryTransactions: ReserveTransaction[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface AllowedAuditorInfo {
|
||||||
|
auditorBaseUrl: string;
|
||||||
|
auditorPub: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AllowedExchangeInfo {
|
||||||
|
exchangeBaseUrl: string;
|
||||||
|
exchangePub: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WalletContractData {
|
||||||
|
fulfillmentUrl: string;
|
||||||
|
contractTermsHash: string;
|
||||||
|
merchantSig: string;
|
||||||
|
merchantPub: string;
|
||||||
|
amount: AmountJson;
|
||||||
|
orderId: string;
|
||||||
|
merchantBaseUrl: string;
|
||||||
|
summary: string;
|
||||||
|
autoRefund: Duration | undefined;
|
||||||
|
maxWireFee: AmountJson;
|
||||||
|
wireFeeAmortization: number;
|
||||||
|
payDeadline: Timestamp;
|
||||||
|
refundDeadline: Timestamp;
|
||||||
|
allowedAuditors: AllowedAuditorInfo[];
|
||||||
|
allowedExchanges: AllowedExchangeInfo[];
|
||||||
|
timestamp: Timestamp;
|
||||||
|
wireMethod: string;
|
||||||
|
wireInfoHash: string;
|
||||||
|
maxDepositFee: AmountJson;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Record that stores status information about one purchase, starting from when
|
* Record that stores status information about one purchase, starting from when
|
||||||
* the customer accepts a proposal. Includes refund status if applicable.
|
* the customer accepts a proposal. Includes refund status if applicable.
|
||||||
@ -1131,15 +1118,12 @@ export interface PurchaseRecord {
|
|||||||
*/
|
*/
|
||||||
proposalId: string;
|
proposalId: string;
|
||||||
|
|
||||||
/**
|
|
||||||
* Hash of the contract terms.
|
|
||||||
*/
|
|
||||||
contractTermsHash: string;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Contract terms we got from the merchant.
|
* Contract terms we got from the merchant.
|
||||||
*/
|
*/
|
||||||
contractTerms: ContractTerms;
|
contractTermsRaw: string;
|
||||||
|
|
||||||
|
contractData: WalletContractData;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The payment request, ready to be send to the merchant's
|
* The payment request, ready to be send to the merchant's
|
||||||
@ -1147,11 +1131,6 @@ export interface PurchaseRecord {
|
|||||||
*/
|
*/
|
||||||
payReq: PayReq;
|
payReq: PayReq;
|
||||||
|
|
||||||
/**
|
|
||||||
* Signature from the merchant over the contract terms.
|
|
||||||
*/
|
|
||||||
merchantSig: string;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Timestamp of the first time that sending a payment to the merchant
|
* Timestamp of the first time that sending a payment to the merchant
|
||||||
* for this purchase was successful.
|
* for this purchase was successful.
|
||||||
@ -1266,12 +1245,9 @@ export interface DepositCoin {
|
|||||||
* the wallet itself, where the wallet acts as a "merchant" for the customer.
|
* the wallet itself, where the wallet acts as a "merchant" for the customer.
|
||||||
*/
|
*/
|
||||||
export interface CoinsReturnRecord {
|
export interface CoinsReturnRecord {
|
||||||
/**
|
contractTermsRaw: string;
|
||||||
* Hash of the contract for sending coins to our own bank account.
|
|
||||||
*/
|
|
||||||
contractTermsHash: string;
|
|
||||||
|
|
||||||
contractTerms: ContractTerms;
|
contractData: WalletContractData;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Private key where corresponding
|
* Private key where corresponding
|
||||||
@ -1446,11 +1422,11 @@ export namespace Stores {
|
|||||||
fulfillmentUrlIndex = new Index<string, PurchaseRecord>(
|
fulfillmentUrlIndex = new Index<string, PurchaseRecord>(
|
||||||
this,
|
this,
|
||||||
"fulfillmentUrlIndex",
|
"fulfillmentUrlIndex",
|
||||||
"contractTerms.fulfillment_url",
|
"contractData.fulfillmentUrl",
|
||||||
);
|
);
|
||||||
orderIdIndex = new Index<string, PurchaseRecord>(this, "orderIdIndex", [
|
orderIdIndex = new Index<string, PurchaseRecord>(this, "orderIdIndex", [
|
||||||
"contractTerms.merchant_base_url",
|
"contractData.merchantBaseUrl",
|
||||||
"contractTerms.order_id",
|
"contractData.orderId",
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -18,9 +18,10 @@
|
|||||||
* Type and schema definitions for the wallet's history.
|
* Type and schema definitions for the wallet's history.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Timestamp, RefreshReason } from "./walletTypes";
|
import { RefreshReason } from "./walletTypes";
|
||||||
import { ReserveTransaction } from "./ReserveTransaction";
|
import { ReserveTransaction } from "./ReserveTransaction";
|
||||||
import { WithdrawalSource } from "./dbTypes";
|
import { WithdrawalSource } from "./dbTypes";
|
||||||
|
import { Timestamp } from "../util/time";
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -21,8 +21,9 @@
|
|||||||
/**
|
/**
|
||||||
* Imports.
|
* Imports.
|
||||||
*/
|
*/
|
||||||
import { OperationError, Timestamp, Duration } from "./walletTypes";
|
import { OperationError } from "./walletTypes";
|
||||||
import { WithdrawalSource, RetryInfo } from "./dbTypes";
|
import { WithdrawalSource, RetryInfo } from "./dbTypes";
|
||||||
|
import { Timestamp, Duration } from "../util/time";
|
||||||
|
|
||||||
export const enum PendingOperationType {
|
export const enum PendingOperationType {
|
||||||
Bug = "bug",
|
Bug = "bug",
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -16,7 +16,7 @@
|
|||||||
|
|
||||||
import test from "ava";
|
import test from "ava";
|
||||||
import * as Amounts from "../util/amounts";
|
import * as Amounts from "../util/amounts";
|
||||||
import { ContractTerms } from "./talerTypes";
|
import { ContractTerms, codecForContractTerms } from "./talerTypes";
|
||||||
|
|
||||||
const amt = (
|
const amt = (
|
||||||
value: number,
|
value: number,
|
||||||
@ -130,6 +130,7 @@ test("amount stringification", t => {
|
|||||||
|
|
||||||
test("contract terms validation", t => {
|
test("contract terms validation", t => {
|
||||||
const c = {
|
const c = {
|
||||||
|
nonce: "123123123",
|
||||||
H_wire: "123",
|
H_wire: "123",
|
||||||
amount: "EUR:1.5",
|
amount: "EUR:1.5",
|
||||||
auditors: [],
|
auditors: [],
|
||||||
@ -138,23 +139,23 @@ test("contract terms validation", t => {
|
|||||||
max_fee: "EUR:1.5",
|
max_fee: "EUR:1.5",
|
||||||
merchant_pub: "12345",
|
merchant_pub: "12345",
|
||||||
order_id: "test_order",
|
order_id: "test_order",
|
||||||
pay_deadline: "Date(12346)",
|
pay_deadline: { t_ms: 42 },
|
||||||
wire_transfer_deadline: "Date(12346)",
|
wire_transfer_deadline: { t_ms: 42 },
|
||||||
merchant_base_url: "https://example.com/pay",
|
merchant_base_url: "https://example.com/pay",
|
||||||
products: [],
|
products: [],
|
||||||
refund_deadline: "Date(12345)",
|
refund_deadline: { t_ms: 42 },
|
||||||
summary: "hello",
|
summary: "hello",
|
||||||
timestamp: "Date(12345)",
|
timestamp: { t_ms: 42 },
|
||||||
wire_method: "test",
|
wire_method: "test",
|
||||||
};
|
};
|
||||||
|
|
||||||
ContractTerms.checked(c);
|
codecForContractTerms().decode(c);
|
||||||
|
|
||||||
const c1 = JSON.parse(JSON.stringify(c));
|
const c1 = JSON.parse(JSON.stringify(c));
|
||||||
c1.exchanges = [];
|
c1.pay_deadline = "foo";
|
||||||
|
|
||||||
try {
|
try {
|
||||||
ContractTerms.checked(c1);
|
codecForContractTerms().decode(c1);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
t.pass();
|
t.pass();
|
||||||
return;
|
return;
|
||||||
|
@ -25,8 +25,7 @@
|
|||||||
/**
|
/**
|
||||||
* Imports.
|
* Imports.
|
||||||
*/
|
*/
|
||||||
import { AmountJson } from "../util/amounts";
|
import { AmountJson, codecForAmountJson } from "../util/amounts";
|
||||||
import { Checkable } from "../util/checkable";
|
|
||||||
import * as LibtoolVersion from "../util/libtoolVersion";
|
import * as LibtoolVersion from "../util/libtoolVersion";
|
||||||
import {
|
import {
|
||||||
CoinRecord,
|
CoinRecord,
|
||||||
@ -35,30 +34,23 @@ import {
|
|||||||
ExchangeWireInfo,
|
ExchangeWireInfo,
|
||||||
} from "./dbTypes";
|
} from "./dbTypes";
|
||||||
import { CoinPaySig, ContractTerms } from "./talerTypes";
|
import { CoinPaySig, ContractTerms } from "./talerTypes";
|
||||||
|
import { Timestamp } from "../util/time";
|
||||||
|
import { typecheckedCodec, makeCodecForObject, codecForString, makeCodecOptional } from "../util/codec";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Response for the create reserve request to the wallet.
|
* Response for the create reserve request to the wallet.
|
||||||
*/
|
*/
|
||||||
@Checkable.Class()
|
|
||||||
export class CreateReserveResponse {
|
export class CreateReserveResponse {
|
||||||
/**
|
/**
|
||||||
* Exchange URL where the bank should create the reserve.
|
* Exchange URL where the bank should create the reserve.
|
||||||
* The URL is canonicalized in the response.
|
* The URL is canonicalized in the response.
|
||||||
*/
|
*/
|
||||||
@Checkable.String()
|
|
||||||
exchange: string;
|
exchange: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reserve public key of the newly created reserve.
|
* Reserve public key of the newly created reserve.
|
||||||
*/
|
*/
|
||||||
@Checkable.String()
|
|
||||||
reservePub: string;
|
reservePub: string;
|
||||||
|
|
||||||
/**
|
|
||||||
* Verify that a value matches the schema of this class and convert it into a
|
|
||||||
* member.
|
|
||||||
*/
|
|
||||||
static checked: (obj: any) => CreateReserveResponse;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -259,88 +251,83 @@ export interface SenderWireInfos {
|
|||||||
/**
|
/**
|
||||||
* Request to mark a reserve as confirmed.
|
* Request to mark a reserve as confirmed.
|
||||||
*/
|
*/
|
||||||
@Checkable.Class()
|
export interface CreateReserveRequest {
|
||||||
export class CreateReserveRequest {
|
|
||||||
/**
|
/**
|
||||||
* The initial amount for the reserve.
|
* The initial amount for the reserve.
|
||||||
*/
|
*/
|
||||||
@Checkable.Value(() => AmountJson)
|
|
||||||
amount: AmountJson;
|
amount: AmountJson;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Exchange URL where the bank should create the reserve.
|
* Exchange URL where the bank should create the reserve.
|
||||||
*/
|
*/
|
||||||
@Checkable.String()
|
|
||||||
exchange: string;
|
exchange: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Payto URI that identifies the exchange's account that the funds
|
* Payto URI that identifies the exchange's account that the funds
|
||||||
* for this reserve go into.
|
* for this reserve go into.
|
||||||
*/
|
*/
|
||||||
@Checkable.String()
|
|
||||||
exchangeWire: string;
|
exchangeWire: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Wire details (as a payto URI) for the bank account that sent the funds to
|
* Wire details (as a payto URI) for the bank account that sent the funds to
|
||||||
* the exchange.
|
* the exchange.
|
||||||
*/
|
*/
|
||||||
@Checkable.Optional(Checkable.String())
|
|
||||||
senderWire?: string;
|
senderWire?: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* URL to fetch the withdraw status from the bank.
|
* URL to fetch the withdraw status from the bank.
|
||||||
*/
|
*/
|
||||||
@Checkable.Optional(Checkable.String())
|
|
||||||
bankWithdrawStatusUrl?: string;
|
bankWithdrawStatusUrl?: string;
|
||||||
|
|
||||||
/**
|
|
||||||
* Verify that a value matches the schema of this class and convert it into a
|
|
||||||
* member.
|
|
||||||
*/
|
|
||||||
static checked: (obj: any) => CreateReserveRequest;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const codecForCreateReserveRequest = () =>
|
||||||
|
typecheckedCodec<CreateReserveRequest>(
|
||||||
|
makeCodecForObject<CreateReserveRequest>()
|
||||||
|
.property("amount", codecForAmountJson())
|
||||||
|
.property("exchange", codecForString)
|
||||||
|
.property("exchangeWire", codecForString)
|
||||||
|
.property("senderWire", makeCodecOptional(codecForString))
|
||||||
|
.property("bankWithdrawStatusUrl", makeCodecOptional(codecForString))
|
||||||
|
.build("CreateReserveRequest"),
|
||||||
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Request to mark a reserve as confirmed.
|
* Request to mark a reserve as confirmed.
|
||||||
*/
|
*/
|
||||||
@Checkable.Class()
|
export interface ConfirmReserveRequest {
|
||||||
export class ConfirmReserveRequest {
|
|
||||||
/**
|
/**
|
||||||
* Public key of then reserve that should be marked
|
* Public key of then reserve that should be marked
|
||||||
* as confirmed.
|
* as confirmed.
|
||||||
*/
|
*/
|
||||||
@Checkable.String()
|
|
||||||
reservePub: string;
|
reservePub: string;
|
||||||
|
|
||||||
/**
|
|
||||||
* Verify that a value matches the schema of this class and convert it into a
|
|
||||||
* member.
|
|
||||||
*/
|
|
||||||
static checked: (obj: any) => ConfirmReserveRequest;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export const codecForConfirmReserveRequest = () =>
|
||||||
|
typecheckedCodec<ConfirmReserveRequest>(
|
||||||
|
makeCodecForObject<ConfirmReserveRequest>()
|
||||||
|
.property("reservePub", codecForString)
|
||||||
|
.build("ConfirmReserveRequest"),
|
||||||
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Wire coins to the user's own bank account.
|
* Wire coins to the user's own bank account.
|
||||||
*/
|
*/
|
||||||
@Checkable.Class()
|
|
||||||
export class ReturnCoinsRequest {
|
export class ReturnCoinsRequest {
|
||||||
/**
|
/**
|
||||||
* The amount to wire.
|
* The amount to wire.
|
||||||
*/
|
*/
|
||||||
@Checkable.Value(() => AmountJson)
|
|
||||||
amount: AmountJson;
|
amount: AmountJson;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The exchange to take the coins from.
|
* The exchange to take the coins from.
|
||||||
*/
|
*/
|
||||||
@Checkable.String()
|
|
||||||
exchange: string;
|
exchange: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Wire details for the bank account of the customer that will
|
* Wire details for the bank account of the customer that will
|
||||||
* receive the funds.
|
* receive the funds.
|
||||||
*/
|
*/
|
||||||
@Checkable.Any()
|
|
||||||
senderWire?: object;
|
senderWire?: object;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -391,8 +378,8 @@ export interface TipStatus {
|
|||||||
tipId: string;
|
tipId: string;
|
||||||
merchantTipId: string;
|
merchantTipId: string;
|
||||||
merchantOrigin: string;
|
merchantOrigin: string;
|
||||||
expirationTimestamp: number;
|
expirationTimestamp: Timestamp;
|
||||||
timestamp: number;
|
timestamp: Timestamp;
|
||||||
totalFees: AmountJson;
|
totalFees: AmountJson;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -418,14 +405,14 @@ export type PreparePayResult =
|
|||||||
export interface PreparePayResultPaymentPossible {
|
export interface PreparePayResultPaymentPossible {
|
||||||
status: "payment-possible";
|
status: "payment-possible";
|
||||||
proposalId: string;
|
proposalId: string;
|
||||||
contractTerms: ContractTerms;
|
contractTermsRaw: string;
|
||||||
totalFees: AmountJson;
|
totalFees: AmountJson;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PreparePayResultInsufficientBalance {
|
export interface PreparePayResultInsufficientBalance {
|
||||||
status: "insufficient-balance";
|
status: "insufficient-balance";
|
||||||
proposalId: string;
|
proposalId: string;
|
||||||
contractTerms: ContractTerms;
|
contractTermsRaw: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PreparePayResultError {
|
export interface PreparePayResultError {
|
||||||
@ -435,7 +422,7 @@ export interface PreparePayResultError {
|
|||||||
|
|
||||||
export interface PreparePayResultPaid {
|
export interface PreparePayResultPaid {
|
||||||
status: "paid";
|
status: "paid";
|
||||||
contractTerms: ContractTerms;
|
contractTermsRaw: any;
|
||||||
nextUrl: string;
|
nextUrl: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -459,7 +446,7 @@ export interface AcceptWithdrawalResponse {
|
|||||||
* Details about a purchase, including refund status.
|
* Details about a purchase, including refund status.
|
||||||
*/
|
*/
|
||||||
export interface PurchaseDetails {
|
export interface PurchaseDetails {
|
||||||
contractTerms: ContractTerms;
|
contractTerms: any;
|
||||||
hasRefund: boolean;
|
hasRefund: boolean;
|
||||||
totalRefundAmount: AmountJson;
|
totalRefundAmount: AmountJson;
|
||||||
totalRefundAndRefreshFees: AmountJson;
|
totalRefundAndRefreshFees: AmountJson;
|
||||||
@ -479,30 +466,6 @@ export interface OperationError {
|
|||||||
details: any;
|
details: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Checkable.Class()
|
|
||||||
export class Timestamp {
|
|
||||||
/**
|
|
||||||
* Timestamp in milliseconds.
|
|
||||||
*/
|
|
||||||
@Checkable.Number()
|
|
||||||
readonly t_ms: number;
|
|
||||||
|
|
||||||
static checked: (obj: any) => Timestamp;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Duration {
|
|
||||||
/**
|
|
||||||
* Duration in milliseconds.
|
|
||||||
*/
|
|
||||||
readonly d_ms: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getTimestampNow(): Timestamp {
|
|
||||||
return {
|
|
||||||
t_ms: new Date().getTime(),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface PlanchetCreationResult {
|
export interface PlanchetCreationResult {
|
||||||
coinPub: string;
|
coinPub: string;
|
||||||
coinPriv: string;
|
coinPriv: string;
|
||||||
|
@ -21,7 +21,7 @@
|
|||||||
/**
|
/**
|
||||||
* Imports.
|
* Imports.
|
||||||
*/
|
*/
|
||||||
import { getTimestampNow, Timestamp } from "../types/walletTypes";
|
import { getTimestampNow, Timestamp, timestampSubtractDuraction, timestampDifference } from "../util/time";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Maximum request per second, per origin.
|
* Maximum request per second, per origin.
|
||||||
@ -50,10 +50,14 @@ class OriginState {
|
|||||||
|
|
||||||
private refill(): void {
|
private refill(): void {
|
||||||
const now = getTimestampNow();
|
const now = getTimestampNow();
|
||||||
const d = now.t_ms - this.lastUpdate.t_ms;
|
const d = timestampDifference(now, this.lastUpdate);
|
||||||
this.tokensSecond = Math.min(MAX_PER_SECOND, this.tokensSecond + (d / 1000));
|
if (d.d_ms === "forever") {
|
||||||
this.tokensMinute = Math.min(MAX_PER_MINUTE, this.tokensMinute + (d / 1000 * 60));
|
throw Error("assertion failed")
|
||||||
this.tokensHour = Math.min(MAX_PER_HOUR, this.tokensHour + (d / 1000 * 60 * 60));
|
}
|
||||||
|
const d_s = d.d_ms / 1000;
|
||||||
|
this.tokensSecond = Math.min(MAX_PER_SECOND, this.tokensSecond + (d_s / 1000));
|
||||||
|
this.tokensMinute = Math.min(MAX_PER_MINUTE, this.tokensMinute + (d_s / 1000 * 60));
|
||||||
|
this.tokensHour = Math.min(MAX_PER_HOUR, this.tokensHour + (d_s / 1000 * 60 * 60));
|
||||||
this.lastUpdate = now;
|
this.lastUpdate = now;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,17 +1,17 @@
|
|||||||
/*
|
/*
|
||||||
This file is part of TALER
|
This file is part of GNU Taler
|
||||||
(C) 2018 GNUnet e.V. and INRIA
|
(C) 2019 Taler Systems S.A.
|
||||||
|
|
||||||
TALER is free software; you can redistribute it and/or modify it under the
|
GNU Taler is free software; you can redistribute it and/or modify it under the
|
||||||
terms of the GNU General Public License as published by the Free Software
|
terms of the GNU General Public License as published by the Free Software
|
||||||
Foundation; either version 3, or (at your option) any later version.
|
Foundation; either version 3, or (at your option) any later version.
|
||||||
|
|
||||||
TALER is distributed in the hope that it will be useful, but WITHOUT ANY
|
GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||||
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
||||||
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||||
|
|
||||||
You should have received a copy of the GNU General Public License along with
|
You should have received a copy of the GNU General Public License along with
|
||||||
TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
|
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -21,7 +21,12 @@
|
|||||||
/**
|
/**
|
||||||
* Imports.
|
* Imports.
|
||||||
*/
|
*/
|
||||||
import { Checkable } from "./checkable";
|
import {
|
||||||
|
typecheckedCodec,
|
||||||
|
makeCodecForObject,
|
||||||
|
codecForString,
|
||||||
|
codecForNumber,
|
||||||
|
} from "./codec";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Number of fractional units that one value unit represents.
|
* Number of fractional units that one value unit represents.
|
||||||
@ -44,29 +49,32 @@ export const maxAmountValue = 2 ** 52;
|
|||||||
* Non-negative financial amount. Fractional values are expressed as multiples
|
* Non-negative financial amount. Fractional values are expressed as multiples
|
||||||
* of 1e-8.
|
* of 1e-8.
|
||||||
*/
|
*/
|
||||||
@Checkable.Class()
|
export interface AmountJson {
|
||||||
export class AmountJson {
|
|
||||||
/**
|
/**
|
||||||
* Value, must be an integer.
|
* Value, must be an integer.
|
||||||
*/
|
*/
|
||||||
@Checkable.Number()
|
|
||||||
readonly value: number;
|
readonly value: number;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fraction, must be an integer. Represent 1/1e8 of a unit.
|
* Fraction, must be an integer. Represent 1/1e8 of a unit.
|
||||||
*/
|
*/
|
||||||
@Checkable.Number()
|
|
||||||
readonly fraction: number;
|
readonly fraction: number;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Currency of the amount.
|
* Currency of the amount.
|
||||||
*/
|
*/
|
||||||
@Checkable.String()
|
|
||||||
readonly currency: string;
|
readonly currency: string;
|
||||||
|
|
||||||
static checked: (obj: any) => AmountJson;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const codecForAmountJson = () =>
|
||||||
|
typecheckedCodec<AmountJson>(
|
||||||
|
makeCodecForObject<AmountJson>()
|
||||||
|
.property("currency", codecForString)
|
||||||
|
.property("value", codecForNumber)
|
||||||
|
.property("fraction", codecForNumber)
|
||||||
|
.build("AmountJson"),
|
||||||
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Result of a possibly overflowing operation.
|
* Result of a possibly overflowing operation.
|
||||||
*/
|
*/
|
||||||
|
@ -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.
|
* Context information to show nicer error messages when decoding fails.
|
||||||
*/
|
*/
|
||||||
interface Context {
|
export interface Context {
|
||||||
readonly path?: string[];
|
readonly path?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderContext(c?: Context): string {
|
export function renderContext(c?: Context): string {
|
||||||
const p = c?.path;
|
const p = c?.path;
|
||||||
if (p) {
|
if (p) {
|
||||||
return p.join(".");
|
return p.join(".");
|
||||||
@ -84,6 +84,9 @@ class ObjectCodecBuilder<OutputType, PartialOutputType> {
|
|||||||
x: K,
|
x: K,
|
||||||
codec: Codec<V>,
|
codec: Codec<V>,
|
||||||
): ObjectCodecBuilder<OutputType, PartialOutputType & SingletonRecord<K, V>> {
|
): ObjectCodecBuilder<OutputType, PartialOutputType & SingletonRecord<K, V>> {
|
||||||
|
if (!codec) {
|
||||||
|
throw Error("inner codec must be defined");
|
||||||
|
}
|
||||||
this.propList.push({ name: x, codec: codec });
|
this.propList.push({ name: x, codec: codec });
|
||||||
return this as any;
|
return this as any;
|
||||||
}
|
}
|
||||||
@ -143,6 +146,9 @@ class UnionCodecBuilder<
|
|||||||
CommonBaseType,
|
CommonBaseType,
|
||||||
PartialTargetType | V
|
PartialTargetType | V
|
||||||
> {
|
> {
|
||||||
|
if (!codec) {
|
||||||
|
throw Error("inner codec must be defined");
|
||||||
|
}
|
||||||
this.alternatives.set(tagValue, { codec, tagValue });
|
this.alternatives.set(tagValue, { codec, tagValue });
|
||||||
return this as any;
|
return this as any;
|
||||||
}
|
}
|
||||||
@ -215,6 +221,9 @@ export function makeCodecForUnion<T>(): UnionCodecPreBuilder<T> {
|
|||||||
export function makeCodecForMap<T>(
|
export function makeCodecForMap<T>(
|
||||||
innerCodec: Codec<T>,
|
innerCodec: Codec<T>,
|
||||||
): Codec<{ [x: string]: T }> {
|
): Codec<{ [x: string]: T }> {
|
||||||
|
if (!innerCodec) {
|
||||||
|
throw Error("inner codec must be defined");
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
decode(x: any, c?: Context): { [x: string]: T } {
|
decode(x: any, c?: Context): { [x: string]: T } {
|
||||||
const map: { [x: string]: T } = {};
|
const map: { [x: string]: T } = {};
|
||||||
@ -233,6 +242,9 @@ export function makeCodecForMap<T>(
|
|||||||
* Return a codec for a list, containing values described by the inner codec.
|
* Return a codec for a list, containing values described by the inner codec.
|
||||||
*/
|
*/
|
||||||
export function makeCodecForList<T>(innerCodec: Codec<T>): Codec<T[]> {
|
export function makeCodecForList<T>(innerCodec: Codec<T>): Codec<T[]> {
|
||||||
|
if (!innerCodec) {
|
||||||
|
throw Error("inner codec must be defined");
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
decode(x: any, c?: Context): T[] {
|
decode(x: any, c?: Context): T[] {
|
||||||
const arr: T[] = [];
|
const arr: T[] = [];
|
||||||
@ -255,7 +267,19 @@ export const codecForNumber: Codec<number> = {
|
|||||||
if (typeof x === "number") {
|
if (typeof x === "number") {
|
||||||
return x;
|
return x;
|
||||||
}
|
}
|
||||||
throw new DecodingError(`expected number at ${renderContext(c)}`);
|
throw new DecodingError(`expected number at ${renderContext(c)} but got ${typeof x}`);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return a codec for a value that must be a number.
|
||||||
|
*/
|
||||||
|
export const codecForBoolean: Codec<boolean> = {
|
||||||
|
decode(x: any, c?: Context): boolean {
|
||||||
|
if (typeof x === "boolean") {
|
||||||
|
return x;
|
||||||
|
}
|
||||||
|
throw new DecodingError(`expected boolean at ${renderContext(c)} but got ${typeof x}`);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -267,7 +291,16 @@ export const codecForString: Codec<string> = {
|
|||||||
if (typeof x === "string") {
|
if (typeof x === "string") {
|
||||||
return x;
|
return x;
|
||||||
}
|
}
|
||||||
throw new DecodingError(`expected string at ${renderContext(c)}`);
|
throw new DecodingError(`expected string at ${renderContext(c)} but got ${typeof x}`);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Codec that allows any value.
|
||||||
|
*/
|
||||||
|
export const codecForAny: Codec<any> = {
|
||||||
|
decode(x: any, c?: Context): any {
|
||||||
|
return x;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -281,12 +314,23 @@ export function makeCodecForConstString<V extends string>(s: V): Codec<V> {
|
|||||||
return x;
|
return x;
|
||||||
}
|
}
|
||||||
throw new DecodingError(
|
throw new DecodingError(
|
||||||
`expected string constant "${s}" at ${renderContext(c)}`,
|
`expected string constant "${s}" at ${renderContext(c)} but got ${typeof x}`,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function makeCodecOptional<V>(innerCodec: Codec<V>): Codec<V | undefined> {
|
||||||
|
return {
|
||||||
|
decode(x: any, c?: Context): V | undefined {
|
||||||
|
if (x === undefined || x === null) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return innerCodec.decode(x, c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function typecheckedCodec<T = undefined>(c: Codec<T>): Codec<T> {
|
export function typecheckedCodec<T = undefined>(c: Codec<T>): Codec<T> {
|
||||||
return c;
|
return c;
|
||||||
}
|
}
|
||||||
|
@ -24,8 +24,6 @@
|
|||||||
import { AmountJson } from "./amounts";
|
import { AmountJson } from "./amounts";
|
||||||
import * as Amounts from "./amounts";
|
import * as Amounts from "./amounts";
|
||||||
|
|
||||||
import { Timestamp, Duration } from "../types/walletTypes";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Show an amount in a form suitable for the user.
|
* Show an amount in a form suitable for the user.
|
||||||
* FIXME: In the future, this should consider currency-specific
|
* FIXME: In the future, this should consider currency-specific
|
||||||
@ -114,75 +112,6 @@ export function flatMap<T, U>(xs: T[], f: (x: T) => U[]): U[] {
|
|||||||
return xs.reduce((acc: U[], next: T) => [...f(next), ...acc], []);
|
return xs.reduce((acc: U[], next: T) => [...f(next), ...acc], []);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Extract a numeric timstamp (in seconds) from the Taler date format
|
|
||||||
* ("/Date([n])/"). Returns null if input is not in the right format.
|
|
||||||
*/
|
|
||||||
export function getTalerStampSec(stamp: string): number | null {
|
|
||||||
const m = stamp.match(/\/?Date\(([0-9]*)\)\/?/);
|
|
||||||
if (!m || !m[1]) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return parseInt(m[1], 10);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Extract a timestamp from a Taler timestamp string.
|
|
||||||
*/
|
|
||||||
export function extractTalerStamp(stamp: string): Timestamp | undefined {
|
|
||||||
const m = stamp.match(/\/?Date\(([0-9]*)\)\/?/);
|
|
||||||
if (!m || !m[1]) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
t_ms: parseInt(m[1], 10) * 1000,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Extract a timestamp from a Taler timestamp string.
|
|
||||||
*/
|
|
||||||
export function extractTalerStampOrThrow(stamp: string): Timestamp {
|
|
||||||
const r = extractTalerStamp(stamp);
|
|
||||||
if (!r) {
|
|
||||||
throw Error("invalid time stamp");
|
|
||||||
}
|
|
||||||
return r;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Extract a duration from a Taler duration string.
|
|
||||||
*/
|
|
||||||
export function extractTalerDuration(duration: string): Duration | undefined {
|
|
||||||
const m = duration.match(/\/?Delay\(([0-9]*)\)\/?/);
|
|
||||||
if (!m || !m[1]) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
d_ms: parseInt(m[1], 10) * 1000,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Extract a duration from a Taler duration string.
|
|
||||||
*/
|
|
||||||
export function extractTalerDurationOrThrow(duration: string): Duration {
|
|
||||||
const r = extractTalerDuration(duration);
|
|
||||||
if (!r) {
|
|
||||||
throw Error("invalid duration");
|
|
||||||
}
|
|
||||||
return r;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if a timestamp is in the right format.
|
|
||||||
*/
|
|
||||||
export function timestampCheck(stamp: string): boolean {
|
|
||||||
return getTalerStampSec(stamp) !== null;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Compute the hash function of a JSON object.
|
* Compute the hash function of a JSON object.
|
||||||
*/
|
*/
|
||||||
|
165
src/util/time.ts
Normal file
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 @@
|
|||||||
/*
|
import { Duration } from "./time";
|
||||||
This file is part of TALER
|
|
||||||
(C) 2017 GNUnet e.V.
|
|
||||||
|
|
||||||
TALER is free software; you can redistribute it and/or modify it under the
|
/*
|
||||||
|
This file is part of GNU Taler
|
||||||
|
(C) 2017-2019 Taler Systems S.A.
|
||||||
|
|
||||||
|
GNU Taler is free software; you can redistribute it and/or modify it under the
|
||||||
terms of the GNU General Public License as published by the Free Software
|
terms of the GNU General Public License as published by the Free Software
|
||||||
Foundation; either version 3, or (at your option) any later version.
|
Foundation; either version 3, or (at your option) any later version.
|
||||||
|
|
||||||
TALER is distributed in the hope that it will be useful, but WITHOUT ANY
|
GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||||
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
||||||
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||||
|
|
||||||
You should have received a copy of the GNU General Public License along with
|
You should have received a copy of the GNU General Public License along with
|
||||||
TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
|
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -105,11 +107,13 @@ export class TimerGroup {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
resolveAfter(delayMs: number): Promise<void> {
|
resolveAfter(delayMs: Duration): Promise<void> {
|
||||||
return new Promise<void>((resolve, reject) => {
|
return new Promise<void>((resolve, reject) => {
|
||||||
this.after(delayMs, () => {
|
if (delayMs.d_ms !== "forever") {
|
||||||
|
this.after(delayMs.d_ms, () => {
|
||||||
resolve();
|
resolve();
|
||||||
});
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -91,7 +91,6 @@ import { getHistory } from "./operations/history";
|
|||||||
import { getPendingOperations } from "./operations/pending";
|
import { getPendingOperations } from "./operations/pending";
|
||||||
import { getBalances } from "./operations/balance";
|
import { getBalances } from "./operations/balance";
|
||||||
import { acceptTip, getTipStatus, processTip } from "./operations/tip";
|
import { acceptTip, getTipStatus, processTip } from "./operations/tip";
|
||||||
import { returnCoins } from "./operations/return";
|
|
||||||
import { payback } from "./operations/payback";
|
import { payback } from "./operations/payback";
|
||||||
import { TimerGroup } from "./util/timer";
|
import { TimerGroup } from "./util/timer";
|
||||||
import { AsyncCondition } from "./util/promiseUtils";
|
import { AsyncCondition } from "./util/promiseUtils";
|
||||||
@ -109,6 +108,7 @@ import {
|
|||||||
getFullRefundFees,
|
getFullRefundFees,
|
||||||
applyRefund,
|
applyRefund,
|
||||||
} from "./operations/refund";
|
} from "./operations/refund";
|
||||||
|
import { durationMin, Duration } from "./util/time";
|
||||||
|
|
||||||
|
|
||||||
const builtinCurrencies: CurrencyRecord[] = [
|
const builtinCurrencies: CurrencyRecord[] = [
|
||||||
@ -289,15 +289,15 @@ export class Wallet {
|
|||||||
numGivingLiveness++;
|
numGivingLiveness++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let dt;
|
let dt: Duration;
|
||||||
if (
|
if (
|
||||||
allPending.pendingOperations.length === 0 ||
|
allPending.pendingOperations.length === 0 ||
|
||||||
allPending.nextRetryDelay.d_ms === Number.MAX_SAFE_INTEGER
|
allPending.nextRetryDelay.d_ms === Number.MAX_SAFE_INTEGER
|
||||||
) {
|
) {
|
||||||
// Wait for 5 seconds
|
// Wait for 5 seconds
|
||||||
dt = 5000;
|
dt = { d_ms: 5000 };
|
||||||
} else {
|
} else {
|
||||||
dt = Math.min(5000, allPending.nextRetryDelay.d_ms);
|
dt = durationMin({ d_ms: 5000}, allPending.nextRetryDelay);
|
||||||
}
|
}
|
||||||
const timeout = this.timerGroup.resolveAfter(dt);
|
const timeout = this.timerGroup.resolveAfter(dt);
|
||||||
this.ws.notify({
|
this.ws.notify({
|
||||||
@ -599,7 +599,7 @@ export class Wallet {
|
|||||||
* Trigger paying coins back into the user's account.
|
* Trigger paying coins back into the user's account.
|
||||||
*/
|
*/
|
||||||
async returnCoins(req: ReturnCoinsRequest): Promise<void> {
|
async returnCoins(req: ReturnCoinsRequest): Promise<void> {
|
||||||
return returnCoins(this.ws, req);
|
throw Error("not implemented");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -708,7 +708,7 @@ export class Wallet {
|
|||||||
]).amount;
|
]).amount;
|
||||||
const totalFees = totalRefundFees;
|
const totalFees = totalRefundFees;
|
||||||
return {
|
return {
|
||||||
contractTerms: purchase.contractTerms,
|
contractTerms: purchase.contractTermsRaw,
|
||||||
hasRefund: purchase.timestampLastRefundStatus !== undefined,
|
hasRefund: purchase.timestampLastRefundStatus !== undefined,
|
||||||
totalRefundAmount: totalRefundAmount,
|
totalRefundAmount: totalRefundAmount,
|
||||||
totalRefundAndRefreshFees: totalFees,
|
totalRefundAndRefreshFees: totalFees,
|
||||||
|
@ -74,7 +74,7 @@ function TalerPayDialog({ talerPayUri }: { talerPayUri: string }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const contractTerms = payStatus.contractTerms;
|
const contractTerms = payStatus.contractTermsRaw;
|
||||||
|
|
||||||
if (!contractTerms) {
|
if (!contractTerms) {
|
||||||
return (
|
return (
|
||||||
|
@ -31,6 +31,7 @@ import * as moment from "moment";
|
|||||||
import * as i18n from "./i18n";
|
import * as i18n from "./i18n";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import ReactDOM from "react-dom";
|
import ReactDOM from "react-dom";
|
||||||
|
import { stringifyTimestamp } from "../util/time";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Render amount as HTML, which non-breaking space between
|
* Render amount as HTML, which non-breaking space between
|
||||||
@ -215,7 +216,7 @@ function FeeDetailsView(props: {
|
|||||||
<tbody>
|
<tbody>
|
||||||
{rci!.wireFees.feesForType[s].map(f => (
|
{rci!.wireFees.feesForType[s].map(f => (
|
||||||
<tr>
|
<tr>
|
||||||
<td>{moment.unix(Math.floor(f.endStamp.t_ms / 1000)).format("llll")}</td>
|
<td>{stringifyTimestamp(f.endStamp)}</td>
|
||||||
<td>{renderAmount(f.wireFee)}</td>
|
<td>{renderAmount(f.wireFee)}</td>
|
||||||
<td>{renderAmount(f.closingFee)}</td>
|
<td>{renderAmount(f.closingFee)}</td>
|
||||||
</tr>
|
</tr>
|
||||||
@ -239,9 +240,8 @@ function FeeDetailsView(props: {
|
|||||||
<p>
|
<p>
|
||||||
{i18n.str`Rounding loss:`} {overhead}
|
{i18n.str`Rounding loss:`} {overhead}
|
||||||
</p>
|
</p>
|
||||||
<p>{i18n.str`Earliest expiration (for deposit): ${moment
|
<p>{i18n.str`Earliest expiration (for deposit): ${
|
||||||
.unix(rci.earliestDepositExpiration.t_ms / 1000)
|
stringifyTimestamp(rci.earliestDepositExpiration)}`}</p>
|
||||||
.fromNow()}`}</p>
|
|
||||||
<h3>Coin Fees</h3>
|
<h3>Coin Fees</h3>
|
||||||
<div style={{ overflow: "auto" }}>
|
<div style={{ overflow: "auto" }}>
|
||||||
<table className="pure-table">
|
<table className="pure-table">
|
||||||
|
@ -25,8 +25,8 @@
|
|||||||
*/
|
*/
|
||||||
import { BrowserCryptoWorkerFactory } from "../crypto/workers/cryptoApi";
|
import { BrowserCryptoWorkerFactory } from "../crypto/workers/cryptoApi";
|
||||||
import { deleteTalerDatabase, openTalerDatabase, WALLET_DB_VERSION } from "../db";
|
import { deleteTalerDatabase, openTalerDatabase, WALLET_DB_VERSION } from "../db";
|
||||||
import { ConfirmReserveRequest, CreateReserveRequest, ReturnCoinsRequest, WalletDiagnostics } from "../types/walletTypes";
|
import { ConfirmReserveRequest, CreateReserveRequest, ReturnCoinsRequest, WalletDiagnostics, codecForCreateReserveRequest, codecForConfirmReserveRequest } from "../types/walletTypes";
|
||||||
import { AmountJson } from "../util/amounts";
|
import { AmountJson, codecForAmountJson } from "../util/amounts";
|
||||||
import { BrowserHttpLib } from "../util/http";
|
import { BrowserHttpLib } from "../util/http";
|
||||||
import { OpenedPromise, openPromise } from "../util/promiseUtils";
|
import { OpenedPromise, openPromise } from "../util/promiseUtils";
|
||||||
import { classifyTalerUri, TalerUriType } from "../util/taleruri";
|
import { classifyTalerUri, TalerUriType } from "../util/taleruri";
|
||||||
@ -91,14 +91,14 @@ async function handleMessage(
|
|||||||
exchange: detail.exchange,
|
exchange: detail.exchange,
|
||||||
senderWire: detail.senderWire,
|
senderWire: detail.senderWire,
|
||||||
};
|
};
|
||||||
const req = CreateReserveRequest.checked(d);
|
const req = codecForCreateReserveRequest().decode(d);
|
||||||
return needsWallet().createReserve(req);
|
return needsWallet().createReserve(req);
|
||||||
}
|
}
|
||||||
case "confirm-reserve": {
|
case "confirm-reserve": {
|
||||||
const d = {
|
const d = {
|
||||||
reservePub: detail.reservePub,
|
reservePub: detail.reservePub,
|
||||||
};
|
};
|
||||||
const req = ConfirmReserveRequest.checked(d);
|
const req = codecForConfirmReserveRequest().decode(d);
|
||||||
return needsWallet().confirmReserve(req);
|
return needsWallet().confirmReserve(req);
|
||||||
}
|
}
|
||||||
case "confirm-pay": {
|
case "confirm-pay": {
|
||||||
@ -117,7 +117,7 @@ async function handleMessage(
|
|||||||
if (!detail.baseUrl || typeof detail.baseUrl !== "string") {
|
if (!detail.baseUrl || typeof detail.baseUrl !== "string") {
|
||||||
return Promise.resolve({ error: "bad url" });
|
return Promise.resolve({ error: "bad url" });
|
||||||
}
|
}
|
||||||
const amount = AmountJson.checked(detail.amount);
|
const amount = codecForAmountJson().decode(detail.amount);
|
||||||
return needsWallet().getWithdrawDetailsForAmount(detail.baseUrl, amount);
|
return needsWallet().getWithdrawDetailsForAmount(detail.baseUrl, amount);
|
||||||
}
|
}
|
||||||
case "get-history": {
|
case "get-history": {
|
||||||
|
@ -56,7 +56,6 @@
|
|||||||
"src/operations/refresh.ts",
|
"src/operations/refresh.ts",
|
||||||
"src/operations/refund.ts",
|
"src/operations/refund.ts",
|
||||||
"src/operations/reserves.ts",
|
"src/operations/reserves.ts",
|
||||||
"src/operations/return.ts",
|
|
||||||
"src/operations/state.ts",
|
"src/operations/state.ts",
|
||||||
"src/operations/tip.ts",
|
"src/operations/tip.ts",
|
||||||
"src/operations/versions.ts",
|
"src/operations/versions.ts",
|
||||||
@ -75,7 +74,6 @@
|
|||||||
"src/util/amounts.ts",
|
"src/util/amounts.ts",
|
||||||
"src/util/assertUnreachable.ts",
|
"src/util/assertUnreachable.ts",
|
||||||
"src/util/asyncMemo.ts",
|
"src/util/asyncMemo.ts",
|
||||||
"src/util/checkable.ts",
|
|
||||||
"src/util/codec-test.ts",
|
"src/util/codec-test.ts",
|
||||||
"src/util/codec.ts",
|
"src/util/codec.ts",
|
||||||
"src/util/helpers-test.ts",
|
"src/util/helpers-test.ts",
|
||||||
@ -90,6 +88,7 @@
|
|||||||
"src/util/query.ts",
|
"src/util/query.ts",
|
||||||
"src/util/taleruri-test.ts",
|
"src/util/taleruri-test.ts",
|
||||||
"src/util/taleruri.ts",
|
"src/util/taleruri.ts",
|
||||||
|
"src/util/time.ts",
|
||||||
"src/util/timer.ts",
|
"src/util/timer.ts",
|
||||||
"src/util/wire.ts",
|
"src/util/wire.ts",
|
||||||
"src/wallet-test.ts",
|
"src/wallet-test.ts",
|
||||||
|
Loading…
Reference in New Issue
Block a user