backup import
This commit is contained in:
parent
95568395ce
commit
03810fd248
@ -60,6 +60,7 @@ import {
|
||||
DenomSelectionState,
|
||||
ExchangeUpdateStatus,
|
||||
ExchangeWireInfo,
|
||||
PayCoinSelection,
|
||||
ProposalDownload,
|
||||
ProposalStatus,
|
||||
RefreshSessionRecord,
|
||||
@ -67,6 +68,8 @@ import {
|
||||
ReserveBankInfo,
|
||||
ReserveRecordStatus,
|
||||
Stores,
|
||||
WalletContractData,
|
||||
WalletRefundItem,
|
||||
} from "../types/dbTypes";
|
||||
import { checkDbInvariant, checkLogicInvariant } from "../util/invariants";
|
||||
import { AmountJson, Amounts, codecForAmountString } from "../util/amounts";
|
||||
@ -77,6 +80,7 @@ import {
|
||||
encodeCrock,
|
||||
getRandomBytes,
|
||||
hash,
|
||||
rsaBlind,
|
||||
stringToBytes,
|
||||
} from "../crypto/talerCrypto";
|
||||
import { canonicalizeBaseUrl, canonicalJson, j2s } from "../util/helpers";
|
||||
@ -102,6 +106,7 @@ import { gzipSync } from "fflate";
|
||||
import { kdf } from "../crypto/primitives/kdf";
|
||||
import { initRetryInfo } from "../util/retries";
|
||||
import { RefreshReason } from "../types/walletTypes";
|
||||
import { CryptoApi } from "../crypto/workers/cryptoApi";
|
||||
|
||||
interface WalletBackupConfState {
|
||||
deviceId: string;
|
||||
@ -461,6 +466,8 @@ export async function exportBackup(
|
||||
? undefined
|
||||
: purch.abortStatus,
|
||||
nonce_priv: purch.noncePriv,
|
||||
merchant_sig: purch.download.contractData.merchantSig,
|
||||
total_pay_cost: Amounts.stringify(purch.totalPayCost),
|
||||
});
|
||||
});
|
||||
|
||||
@ -607,11 +614,77 @@ interface CompletedCoin {
|
||||
interface BackupCryptoPrecomputedData {
|
||||
denomPubToHash: Record<string, string>;
|
||||
coinPrivToCompletedCoin: Record<string, CompletedCoin>;
|
||||
proposalNoncePrivToProposalPub: { [priv: string]: string };
|
||||
proposalNoncePrivToPub: { [priv: string]: string };
|
||||
proposalIdToContractTermsHash: { [proposalId: string]: string };
|
||||
reservePrivToPub: Record<string, string>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute cryptographic values for a backup blob.
|
||||
*
|
||||
* FIXME: Take data that we already know from the DB.
|
||||
* FIXME: Move computations into crypto worker.
|
||||
*/
|
||||
async function computeBackupCryptoData(
|
||||
cryptoApi: CryptoApi,
|
||||
backupContent: WalletBackupContentV1,
|
||||
): Promise<BackupCryptoPrecomputedData> {
|
||||
const cryptoData: BackupCryptoPrecomputedData = {
|
||||
coinPrivToCompletedCoin: {},
|
||||
denomPubToHash: {},
|
||||
proposalIdToContractTermsHash: {},
|
||||
proposalNoncePrivToPub: {},
|
||||
reservePrivToPub: {},
|
||||
};
|
||||
for (const backupExchange of backupContent.exchanges) {
|
||||
for (const backupDenom of backupExchange.denominations) {
|
||||
for (const backupCoin of backupDenom.coins) {
|
||||
const coinPub = encodeCrock(
|
||||
eddsaGetPublic(decodeCrock(backupCoin.coin_priv)),
|
||||
);
|
||||
const blindedCoin = rsaBlind(
|
||||
hash(decodeCrock(backupCoin.coin_priv)),
|
||||
decodeCrock(backupCoin.blinding_key),
|
||||
decodeCrock(backupDenom.denom_pub),
|
||||
);
|
||||
cryptoData.coinPrivToCompletedCoin[backupCoin.coin_priv] = {
|
||||
coinEvHash: encodeCrock(hash(blindedCoin)),
|
||||
coinPub,
|
||||
}
|
||||
}
|
||||
cryptoData.denomPubToHash[backupDenom.denom_pub] = encodeCrock(
|
||||
hash(decodeCrock(backupDenom.denom_pub)),
|
||||
);
|
||||
}
|
||||
for (const backupReserve of backupExchange.reserves) {
|
||||
cryptoData.reservePrivToPub[backupReserve.reserve_priv] = encodeCrock(
|
||||
eddsaGetPublic(decodeCrock(backupReserve.reserve_priv)),
|
||||
);
|
||||
}
|
||||
}
|
||||
for (const prop of backupContent.proposals) {
|
||||
const contractTermsHash = await cryptoApi.hashString(
|
||||
canonicalJson(prop.contract_terms_raw),
|
||||
);
|
||||
const noncePub = encodeCrock(eddsaGetPublic(decodeCrock(prop.nonce_priv)));
|
||||
cryptoData.proposalNoncePrivToPub[prop.nonce_priv] = noncePub;
|
||||
cryptoData.proposalIdToContractTermsHash[
|
||||
prop.proposal_id
|
||||
] = contractTermsHash;
|
||||
}
|
||||
for (const purch of backupContent.purchases) {
|
||||
const contractTermsHash = await cryptoApi.hashString(
|
||||
canonicalJson(purch.contract_terms_raw),
|
||||
);
|
||||
const noncePub = encodeCrock(eddsaGetPublic(decodeCrock(purch.nonce_priv)));
|
||||
cryptoData.proposalNoncePrivToPub[purch.nonce_priv] = noncePub;
|
||||
cryptoData.proposalIdToContractTermsHash[
|
||||
purch.proposal_id
|
||||
] = contractTermsHash;
|
||||
}
|
||||
return cryptoData;
|
||||
}
|
||||
|
||||
function checkBackupInvariant(b: boolean, m?: string): asserts b {
|
||||
if (!b) {
|
||||
if (m) {
|
||||
@ -622,6 +695,88 @@ function checkBackupInvariant(b: boolean, m?: string): asserts b {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Re-compute information about the coin selection for a payment.
|
||||
*/
|
||||
async function recoverPayCoinSelection(
|
||||
tx: TransactionHandle<
|
||||
typeof Stores.exchanges | typeof Stores.coins | typeof Stores.denominations
|
||||
>,
|
||||
contractData: WalletContractData,
|
||||
backupPurchase: BackupPurchase,
|
||||
): Promise<PayCoinSelection> {
|
||||
const coinPubs: string[] = backupPurchase.pay_coins.map((x) => x.coin_pub);
|
||||
const coinContributions: AmountJson[] = backupPurchase.pay_coins.map((x) =>
|
||||
Amounts.parseOrThrow(x.contribution),
|
||||
);
|
||||
|
||||
const coveredExchanges: Set<string> = new Set();
|
||||
|
||||
let totalWireFee: AmountJson = Amounts.getZero(contractData.amount.currency);
|
||||
let totalDepositFees: AmountJson = Amounts.getZero(
|
||||
contractData.amount.currency,
|
||||
);
|
||||
|
||||
for (const coinPub of coinPubs) {
|
||||
const coinRecord = await tx.get(Stores.coins, coinPub);
|
||||
checkBackupInvariant(!!coinRecord);
|
||||
const denom = await tx.get(Stores.denominations, [
|
||||
coinRecord.exchangeBaseUrl,
|
||||
coinRecord.denomPubHash,
|
||||
]);
|
||||
checkBackupInvariant(!!denom);
|
||||
totalDepositFees = Amounts.add(totalDepositFees, denom.feeDeposit).amount;
|
||||
|
||||
if (!coveredExchanges.has(coinRecord.exchangeBaseUrl)) {
|
||||
const exchange = await tx.get(
|
||||
Stores.exchanges,
|
||||
coinRecord.exchangeBaseUrl,
|
||||
);
|
||||
checkBackupInvariant(!!exchange);
|
||||
let wireFee: AmountJson | undefined;
|
||||
const feesForType = exchange.wireInfo?.feesForType;
|
||||
checkBackupInvariant(!!feesForType);
|
||||
for (const fee of feesForType[contractData.wireMethod] || []) {
|
||||
if (
|
||||
fee.startStamp <= contractData.timestamp &&
|
||||
fee.endStamp >= contractData.timestamp
|
||||
) {
|
||||
wireFee = fee.wireFee;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (wireFee) {
|
||||
totalWireFee = Amounts.add(totalWireFee, wireFee).amount;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let customerWireFee: AmountJson;
|
||||
|
||||
const amortizedWireFee = Amounts.divide(
|
||||
totalWireFee,
|
||||
contractData.wireFeeAmortization,
|
||||
);
|
||||
if (Amounts.cmp(contractData.maxWireFee, amortizedWireFee) < 0) {
|
||||
customerWireFee = amortizedWireFee;
|
||||
} else {
|
||||
customerWireFee = Amounts.getZero(contractData.amount.currency);
|
||||
}
|
||||
|
||||
const customerDepositFees = Amounts.sub(
|
||||
totalDepositFees,
|
||||
contractData.maxDepositFee,
|
||||
).amount;
|
||||
|
||||
return {
|
||||
coinPubs,
|
||||
coinContributions,
|
||||
paymentAmount: contractData.amount,
|
||||
customerWireFees: customerWireFee,
|
||||
customerDepositFees,
|
||||
};
|
||||
}
|
||||
|
||||
function getDenomSelStateFromBackup(
|
||||
tx: TransactionHandle<typeof Stores.denominations>,
|
||||
sel: BackupDenomSel,
|
||||
@ -959,9 +1114,7 @@ export async function importBackup(
|
||||
orderId: backupProposal.order_id,
|
||||
noncePriv: backupProposal.nonce_priv,
|
||||
noncePub:
|
||||
cryptoComp.proposalNoncePrivToProposalPub[
|
||||
backupProposal.nonce_priv
|
||||
],
|
||||
cryptoComp.proposalNoncePrivToPub[backupProposal.nonce_priv],
|
||||
proposalId: backupProposal.proposal_id,
|
||||
repurchaseProposalId: backupProposal.repurchase_proposal_id,
|
||||
retryInfo: initRetryInfo(false),
|
||||
@ -977,7 +1130,138 @@ export async function importBackup(
|
||||
backupPurchase.proposal_id,
|
||||
);
|
||||
if (!existingPurchase) {
|
||||
await tx.put(Stores.purchases, {});
|
||||
const refunds: { [refundKey: string]: WalletRefundItem } = {};
|
||||
for (const backupRefund of backupPurchase.refunds) {
|
||||
const key = `${backupRefund.coin_pub}-${backupRefund.rtransaction_id}`;
|
||||
const coin = await tx.get(Stores.coins, backupRefund.coin_pub);
|
||||
checkBackupInvariant(!!coin);
|
||||
const denom = await tx.get(Stores.denominations, [
|
||||
coin.exchangeBaseUrl,
|
||||
coin.denomPubHash,
|
||||
]);
|
||||
checkBackupInvariant(!!denom);
|
||||
const common = {
|
||||
coinPub: backupRefund.coin_pub,
|
||||
executionTime: backupRefund.execution_time,
|
||||
obtainedTime: backupRefund.obtained_time,
|
||||
refundAmount: Amounts.parseOrThrow(backupRefund.refund_amount),
|
||||
refundFee: denom.feeRefund,
|
||||
rtransactionId: backupRefund.rtransaction_id,
|
||||
totalRefreshCostBound: Amounts.parseOrThrow(
|
||||
backupRefund.total_refresh_cost_bound,
|
||||
),
|
||||
};
|
||||
switch (backupRefund.type) {
|
||||
case BackupRefundState.Applied:
|
||||
refunds[key] = {
|
||||
type: RefundState.Applied,
|
||||
...common,
|
||||
};
|
||||
break;
|
||||
case BackupRefundState.Failed:
|
||||
refunds[key] = {
|
||||
type: RefundState.Failed,
|
||||
...common,
|
||||
};
|
||||
break;
|
||||
case BackupRefundState.Pending:
|
||||
refunds[key] = {
|
||||
type: RefundState.Pending,
|
||||
...common,
|
||||
};
|
||||
break;
|
||||
}
|
||||
}
|
||||
let abortStatus: AbortStatus;
|
||||
switch (backupPurchase.abort_status) {
|
||||
case "abort-finished":
|
||||
abortStatus = AbortStatus.AbortFinished;
|
||||
break;
|
||||
case "abort-refund":
|
||||
abortStatus = AbortStatus.AbortRefund;
|
||||
break;
|
||||
default:
|
||||
throw Error("not reachable");
|
||||
}
|
||||
const parsedContractTerms = codecForContractTerms().decode(
|
||||
backupPurchase.contract_terms_raw,
|
||||
);
|
||||
const amount = Amounts.parseOrThrow(parsedContractTerms.amount);
|
||||
const contractTermsHash =
|
||||
cryptoComp.proposalIdToContractTermsHash[
|
||||
backupPurchase.proposal_id
|
||||
];
|
||||
let maxWireFee: AmountJson;
|
||||
if (parsedContractTerms.max_wire_fee) {
|
||||
maxWireFee = Amounts.parseOrThrow(parsedContractTerms.max_wire_fee);
|
||||
} else {
|
||||
maxWireFee = Amounts.getZero(amount.currency);
|
||||
}
|
||||
const download: ProposalDownload = {
|
||||
contractData: {
|
||||
amount,
|
||||
contractTermsHash: contractTermsHash,
|
||||
fulfillmentUrl: parsedContractTerms.fulfillment_url ?? "",
|
||||
merchantBaseUrl: parsedContractTerms.merchant_base_url,
|
||||
merchantPub: parsedContractTerms.merchant_pub,
|
||||
merchantSig: backupPurchase.merchant_sig,
|
||||
orderId: parsedContractTerms.order_id,
|
||||
summary: parsedContractTerms.summary,
|
||||
autoRefund: parsedContractTerms.auto_refund,
|
||||
maxWireFee,
|
||||
payDeadline: parsedContractTerms.pay_deadline,
|
||||
refundDeadline: parsedContractTerms.refund_deadline,
|
||||
wireFeeAmortization:
|
||||
parsedContractTerms.wire_fee_amortization || 1,
|
||||
allowedAuditors: parsedContractTerms.auditors.map((x) => ({
|
||||
auditorBaseUrl: x.url,
|
||||
auditorPub: x.master_pub,
|
||||
})),
|
||||
allowedExchanges: parsedContractTerms.exchanges.map((x) => ({
|
||||
exchangeBaseUrl: x.url,
|
||||
exchangePub: x.master_pub,
|
||||
})),
|
||||
timestamp: parsedContractTerms.timestamp,
|
||||
wireMethod: parsedContractTerms.wire_method,
|
||||
wireInfoHash: parsedContractTerms.h_wire,
|
||||
maxDepositFee: Amounts.parseOrThrow(parsedContractTerms.max_fee),
|
||||
merchant: parsedContractTerms.merchant,
|
||||
products: parsedContractTerms.products,
|
||||
summaryI18n: parsedContractTerms.summary_i18n,
|
||||
},
|
||||
contractTermsRaw: backupPurchase.contract_terms_raw,
|
||||
};
|
||||
await tx.put(Stores.purchases, {
|
||||
proposalId: backupPurchase.proposal_id,
|
||||
noncePriv: backupPurchase.nonce_priv,
|
||||
noncePub:
|
||||
cryptoComp.proposalNoncePrivToPub[backupPurchase.nonce_priv],
|
||||
lastPayError: undefined,
|
||||
autoRefundDeadline: { t_ms: "never" },
|
||||
refundStatusRetryInfo: initRetryInfo(false),
|
||||
lastRefundStatusError: undefined,
|
||||
timestampAccept: backupPurchase.timestamp_accept,
|
||||
timestampFirstSuccessfulPay:
|
||||
backupPurchase.timestamp_first_successful_pay,
|
||||
timestampLastRefundStatus:
|
||||
backupPurchase.timestamp_last_refund_status,
|
||||
merchantPaySig: backupPurchase.merchant_pay_sig,
|
||||
lastSessionId: undefined,
|
||||
abortStatus,
|
||||
// FIXME!
|
||||
payRetryInfo: initRetryInfo(false),
|
||||
download,
|
||||
paymentSubmitPending: !backupPurchase.timestamp_first_successful_pay,
|
||||
refundQueryRequested: false,
|
||||
payCoinSelection: await recoverPayCoinSelection(
|
||||
tx,
|
||||
download.contractData,
|
||||
backupPurchase,
|
||||
),
|
||||
coinDepositPermissions: undefined,
|
||||
totalPayCost: Amounts.parseOrThrow(backupPurchase.total_pay_cost),
|
||||
refunds,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -941,8 +941,21 @@ async function submitPay(
|
||||
purchase.download.contractData.merchantBaseUrl,
|
||||
).href;
|
||||
|
||||
let depositPermissions: CoinDepositPermission[];
|
||||
|
||||
if (purchase.coinDepositPermissions) {
|
||||
depositPermissions = purchase.coinDepositPermissions;
|
||||
} else {
|
||||
// FIXME: also cache!
|
||||
depositPermissions = await generateDepositPermissions(
|
||||
ws,
|
||||
purchase.payCoinSelection,
|
||||
purchase.download.contractData,
|
||||
);
|
||||
}
|
||||
|
||||
const reqBody = {
|
||||
coins: purchase.coinDepositPermissions,
|
||||
coins: depositPermissions,
|
||||
session_id: purchase.lastSessionId,
|
||||
};
|
||||
|
||||
@ -1192,6 +1205,50 @@ export async function preparePayForUri(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate deposit permissions for a purchase.
|
||||
*
|
||||
* Accesses the database and the crypto worker.
|
||||
*/
|
||||
async function generateDepositPermissions(
|
||||
ws: InternalWalletState,
|
||||
payCoinSel: PayCoinSelection,
|
||||
contractData: WalletContractData,
|
||||
): Promise<CoinDepositPermission[]> {
|
||||
const depositPermissions: CoinDepositPermission[] = [];
|
||||
for (let i = 0; i < payCoinSel.coinPubs.length; i++) {
|
||||
const coin = await ws.db.get(Stores.coins, payCoinSel.coinPubs[i]);
|
||||
if (!coin) {
|
||||
throw Error("can't pay, allocated coin not found anymore");
|
||||
}
|
||||
const denom = await ws.db.get(Stores.denominations, [
|
||||
coin.exchangeBaseUrl,
|
||||
coin.denomPubHash,
|
||||
]);
|
||||
if (!denom) {
|
||||
throw Error(
|
||||
"can't pay, denomination of allocated coin not found anymore",
|
||||
);
|
||||
}
|
||||
const dp = await ws.cryptoApi.signDepositPermission({
|
||||
coinPriv: coin.coinPriv,
|
||||
coinPub: coin.coinPub,
|
||||
contractTermsHash: contractData.contractTermsHash,
|
||||
denomPubHash: coin.denomPubHash,
|
||||
denomSig: coin.denomSig,
|
||||
exchangeBaseUrl: coin.exchangeBaseUrl,
|
||||
feeDeposit: denom.feeDeposit,
|
||||
merchantPub: contractData.merchantPub,
|
||||
refundDeadline: contractData.refundDeadline,
|
||||
spendAmount: payCoinSel.coinContributions[i],
|
||||
timestamp: contractData.timestamp,
|
||||
wireInfoHash: contractData.wireInfoHash,
|
||||
});
|
||||
depositPermissions.push(dp);
|
||||
}
|
||||
return depositPermissions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a contract to the wallet and sign coins, and send them.
|
||||
*/
|
||||
@ -1248,37 +1305,11 @@ export async function confirmPay(
|
||||
throw Error("insufficient balance");
|
||||
}
|
||||
|
||||
const depositPermissions: CoinDepositPermission[] = [];
|
||||
for (let i = 0; i < res.coinPubs.length; i++) {
|
||||
const coin = await ws.db.get(Stores.coins, res.coinPubs[i]);
|
||||
if (!coin) {
|
||||
throw Error("can't pay, allocated coin not found anymore");
|
||||
}
|
||||
const denom = await ws.db.get(Stores.denominations, [
|
||||
coin.exchangeBaseUrl,
|
||||
coin.denomPubHash,
|
||||
]);
|
||||
if (!denom) {
|
||||
throw Error(
|
||||
"can't pay, denomination of allocated coin not found anymore",
|
||||
);
|
||||
}
|
||||
const dp = await ws.cryptoApi.signDepositPermission({
|
||||
coinPriv: coin.coinPriv,
|
||||
coinPub: coin.coinPub,
|
||||
contractTermsHash: d.contractData.contractTermsHash,
|
||||
denomPubHash: coin.denomPubHash,
|
||||
denomSig: coin.denomSig,
|
||||
exchangeBaseUrl: coin.exchangeBaseUrl,
|
||||
feeDeposit: denom.feeDeposit,
|
||||
merchantPub: d.contractData.merchantPub,
|
||||
refundDeadline: d.contractData.refundDeadline,
|
||||
spendAmount: res.coinContributions[i],
|
||||
timestamp: d.contractData.timestamp,
|
||||
wireInfoHash: d.contractData.wireInfoHash,
|
||||
});
|
||||
depositPermissions.push(dp);
|
||||
}
|
||||
const depositPermissions = await generateDepositPermissions(
|
||||
ws,
|
||||
res,
|
||||
d.contractData,
|
||||
);
|
||||
purchase = await recordConfirmPay(
|
||||
ws,
|
||||
proposal,
|
||||
|
@ -501,9 +501,9 @@ export async function applyRefund(
|
||||
const p = purchase;
|
||||
|
||||
let amountRefundGranted = Amounts.getZero(
|
||||
purchase.contractData.amount.currency,
|
||||
purchase.download.contractData.amount.currency,
|
||||
);
|
||||
let amountRefundGone = Amounts.getZero(purchase.contractData.amount.currency);
|
||||
let amountRefundGone = Amounts.getZero(purchase.download.contractData.amount.currency);
|
||||
|
||||
let pendingAtExchange = false;
|
||||
|
||||
@ -531,21 +531,21 @@ export async function applyRefund(
|
||||
});
|
||||
|
||||
return {
|
||||
contractTermsHash: purchase.contractData.contractTermsHash,
|
||||
contractTermsHash: purchase.download.contractData.contractTermsHash,
|
||||
proposalId: purchase.proposalId,
|
||||
amountEffectivePaid: Amounts.stringify(purchase.totalPayCost),
|
||||
amountRefundGone: Amounts.stringify(amountRefundGone),
|
||||
amountRefundGranted: Amounts.stringify(amountRefundGranted),
|
||||
pendingAtExchange,
|
||||
info: {
|
||||
contractTermsHash: purchase.contractData.contractTermsHash,
|
||||
merchant: purchase.contractData.merchant,
|
||||
orderId: purchase.contractData.orderId,
|
||||
products: purchase.contractData.products,
|
||||
summary: purchase.contractData.summary,
|
||||
fulfillmentMessage: purchase.contractData.fulfillmentMessage,
|
||||
summary_i18n: purchase.contractData.summaryI18n,
|
||||
fulfillmentMessage_i18n: purchase.contractData.fulfillmentMessageI18n,
|
||||
contractTermsHash: purchase.download.contractData.contractTermsHash,
|
||||
merchant: purchase.download.contractData.merchant,
|
||||
orderId: purchase.download.contractData.orderId,
|
||||
products: purchase.download.contractData.products,
|
||||
summary: purchase.download.contractData.summary,
|
||||
fulfillmentMessage: purchase.download.contractData.fulfillmentMessage,
|
||||
summary_i18n: purchase.download.contractData.summaryI18n,
|
||||
fulfillmentMessage_i18n: purchase.download.contractData.fulfillmentMessageI18n,
|
||||
},
|
||||
};
|
||||
}
|
||||
@ -594,14 +594,14 @@ async function processPurchaseQueryRefundImpl(
|
||||
|
||||
if (purchase.timestampFirstSuccessfulPay) {
|
||||
const requestUrl = new URL(
|
||||
`orders/${purchase.contractData.orderId}/refund`,
|
||||
purchase.contractData.merchantBaseUrl,
|
||||
`orders/${purchase.download.contractData.orderId}/refund`,
|
||||
purchase.download.contractData.merchantBaseUrl,
|
||||
);
|
||||
|
||||
logger.trace(`making refund request to ${requestUrl.href}`);
|
||||
|
||||
const request = await ws.http.postJson(requestUrl.href, {
|
||||
h_contract: purchase.contractData.contractTermsHash,
|
||||
h_contract: purchase.download.contractData.contractTermsHash,
|
||||
});
|
||||
|
||||
logger.trace(
|
||||
@ -622,8 +622,8 @@ async function processPurchaseQueryRefundImpl(
|
||||
);
|
||||
} else if (purchase.abortStatus === AbortStatus.AbortRefund) {
|
||||
const requestUrl = new URL(
|
||||
`orders/${purchase.contractData.orderId}/abort`,
|
||||
purchase.contractData.merchantBaseUrl,
|
||||
`orders/${purchase.download.contractData.orderId}/abort`,
|
||||
purchase.download.contractData.merchantBaseUrl,
|
||||
);
|
||||
|
||||
const abortingCoins: AbortingCoin[] = [];
|
||||
@ -641,7 +641,7 @@ async function processPurchaseQueryRefundImpl(
|
||||
}
|
||||
|
||||
const abortReq: AbortRequest = {
|
||||
h_contract: purchase.contractData.contractTermsHash,
|
||||
h_contract: purchase.download.contractData.contractTermsHash,
|
||||
coins: abortingCoins,
|
||||
};
|
||||
|
||||
@ -669,7 +669,7 @@ async function processPurchaseQueryRefundImpl(
|
||||
purchase.payCoinSelection.coinContributions[i],
|
||||
),
|
||||
rtransaction_id: 0,
|
||||
execution_time: timestampAddDuration(purchase.contractData.timestamp, {
|
||||
execution_time: timestampAddDuration(purchase.download.contractData.timestamp, {
|
||||
d_ms: 1000,
|
||||
}),
|
||||
});
|
||||
|
@ -207,12 +207,13 @@ export async function getTransactions(
|
||||
if (
|
||||
shouldSkipCurrency(
|
||||
transactionsRequest,
|
||||
pr.contractData.amount.currency,
|
||||
pr.download.contractData.amount.currency,
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
if (shouldSkipSearch(transactionsRequest, [pr.contractData.summary])) {
|
||||
const contractData = pr.download.contractData;
|
||||
if (shouldSkipSearch(transactionsRequest, [contractData.summary])) {
|
||||
return;
|
||||
}
|
||||
const proposal = await tx.get(Stores.proposals, pr.proposalId);
|
||||
@ -220,15 +221,15 @@ export async function getTransactions(
|
||||
return;
|
||||
}
|
||||
const info: OrderShortInfo = {
|
||||
merchant: pr.contractData.merchant,
|
||||
orderId: pr.contractData.orderId,
|
||||
products: pr.contractData.products,
|
||||
summary: pr.contractData.summary,
|
||||
summary_i18n: pr.contractData.summaryI18n,
|
||||
contractTermsHash: pr.contractData.contractTermsHash,
|
||||
merchant: contractData.merchant,
|
||||
orderId: contractData.orderId,
|
||||
products: contractData.products,
|
||||
summary: contractData.summary,
|
||||
summary_i18n: contractData.summaryI18n,
|
||||
contractTermsHash: contractData.contractTermsHash,
|
||||
};
|
||||
if (pr.contractData.fulfillmentUrl !== "") {
|
||||
info.fulfillmentUrl = pr.contractData.fulfillmentUrl;
|
||||
if (contractData.fulfillmentUrl !== "") {
|
||||
info.fulfillmentUrl = contractData.fulfillmentUrl;
|
||||
}
|
||||
const paymentTransactionId = makeEventId(
|
||||
TransactionType.Payment,
|
||||
@ -237,7 +238,7 @@ export async function getTransactions(
|
||||
const err = pr.lastPayError ?? pr.lastRefundStatusError;
|
||||
transactions.push({
|
||||
type: TransactionType.Payment,
|
||||
amountRaw: Amounts.stringify(pr.contractData.amount),
|
||||
amountRaw: Amounts.stringify(contractData.amount),
|
||||
amountEffective: Amounts.stringify(pr.totalPayCost),
|
||||
status: pr.timestampFirstSuccessfulPay
|
||||
? PaymentStatus.Paid
|
||||
@ -267,9 +268,9 @@ export async function getTransactions(
|
||||
groupKey,
|
||||
);
|
||||
let r0: WalletRefundItem | undefined;
|
||||
let amountRaw = Amounts.getZero(pr.contractData.amount.currency);
|
||||
let amountRaw = Amounts.getZero(contractData.amount.currency);
|
||||
let amountEffective = Amounts.getZero(
|
||||
pr.contractData.amount.currency,
|
||||
contractData.amount.currency,
|
||||
);
|
||||
for (const rk of Object.keys(pr.refunds)) {
|
||||
const refund = pr.refunds[rk];
|
||||
|
@ -34,6 +34,11 @@
|
||||
* 6. Returning money to own bank account isn't supported/exported yet.
|
||||
* 7. Peer-to-peer payments aren't supported yet.
|
||||
* 8. Next update time / next refresh time isn't backed up yet.
|
||||
* 9. Coin/denom selections should be forgettable once that information
|
||||
* becomes irrelevant.
|
||||
* 10. Re-denominated payments/refreshes are not shown properly in the total
|
||||
* payment cost.
|
||||
* 11. Failed refunds do not have any information about why they failed.
|
||||
*
|
||||
* Questions:
|
||||
* 1. What happens when two backups are merged that have
|
||||
@ -42,6 +47,10 @@
|
||||
* 2. Should we make more information forgettable? I.e. is
|
||||
* the coin selection still relevant for a purchase after the coins
|
||||
* are legally expired?
|
||||
* => Yes, still needs to be implemented
|
||||
* 3. What about re-denominations / re-selection of payment coins?
|
||||
* Is it enough to store a clock value for the selection?
|
||||
* => Coin derivation should also consider denom pub hash
|
||||
*
|
||||
* General considerations / decisions:
|
||||
* 1. Information about previously occurring errors and
|
||||
@ -78,6 +87,9 @@ type DeviceIdString = string;
|
||||
*/
|
||||
type ClockValue = number;
|
||||
|
||||
/**
|
||||
* Contract terms JSON.
|
||||
*/
|
||||
type RawContractTerms = any;
|
||||
|
||||
/**
|
||||
@ -751,10 +763,8 @@ export interface BackupPurchase {
|
||||
|
||||
/**
|
||||
* Signature on the contract terms.
|
||||
*
|
||||
* Must be present if contract_terms_raw is present.
|
||||
*/
|
||||
merchant_sig?: string;
|
||||
merchant_sig: string;
|
||||
|
||||
/**
|
||||
* Private key for the nonce. Might eventually be used
|
||||
@ -774,6 +784,19 @@ export interface BackupPurchase {
|
||||
contribution: BackupAmountString;
|
||||
}[];
|
||||
|
||||
/**
|
||||
* Total cost initially shown to the user.
|
||||
*
|
||||
* This includes the amount taken by the merchant, fees (wire/deposit) contributed
|
||||
* by the customer, refreshing fees, fees for withdraw-after-refresh and "trimmings"
|
||||
* of coins that are too small to spend.
|
||||
*
|
||||
* Note that in rare situations, this cost might not be accurate (e.g.
|
||||
* when the payment or refresh gets re-denominated).
|
||||
* We might show adjustments to this later, but currently we don't do so.
|
||||
*/
|
||||
total_pay_cost: BackupAmountString;
|
||||
|
||||
/**
|
||||
* Timestamp of the first time that sending a payment to the merchant
|
||||
* for this purchase was successful.
|
||||
|
@ -1206,8 +1206,10 @@ export interface PurchaseRecord {
|
||||
|
||||
/**
|
||||
* Deposit permissions, available once the user has accepted the payment.
|
||||
*
|
||||
* This value is cached and derived from payCoinSelection.
|
||||
*/
|
||||
coinDepositPermissions: CoinDepositPermission[];
|
||||
coinDepositPermissions: CoinDepositPermission[] | undefined;
|
||||
|
||||
payCoinSelection: PayCoinSelection;
|
||||
|
||||
|
@ -398,4 +398,5 @@ export const Amounts = {
|
||||
fromFloat: fromFloat,
|
||||
copy: copy,
|
||||
fractionalBase: fractionalBase,
|
||||
divide: divide,
|
||||
};
|
||||
|
@ -840,7 +840,7 @@ export class Wallet {
|
||||
]).amount;
|
||||
const totalFees = totalRefundFees;
|
||||
return {
|
||||
contractTerms: JSON.parse(purchase.contractTermsRaw),
|
||||
contractTerms: JSON.parse(purchase.download.contractTermsRaw),
|
||||
hasRefund: purchase.timestampLastRefundStatus !== undefined,
|
||||
totalRefundAmount: totalRefundAmount,
|
||||
totalRefundAndRefreshFees: totalFees,
|
||||
|
Loading…
Reference in New Issue
Block a user