backup import
This commit is contained in:
parent
95568395ce
commit
03810fd248
@ -60,6 +60,7 @@ import {
|
|||||||
DenomSelectionState,
|
DenomSelectionState,
|
||||||
ExchangeUpdateStatus,
|
ExchangeUpdateStatus,
|
||||||
ExchangeWireInfo,
|
ExchangeWireInfo,
|
||||||
|
PayCoinSelection,
|
||||||
ProposalDownload,
|
ProposalDownload,
|
||||||
ProposalStatus,
|
ProposalStatus,
|
||||||
RefreshSessionRecord,
|
RefreshSessionRecord,
|
||||||
@ -67,6 +68,8 @@ import {
|
|||||||
ReserveBankInfo,
|
ReserveBankInfo,
|
||||||
ReserveRecordStatus,
|
ReserveRecordStatus,
|
||||||
Stores,
|
Stores,
|
||||||
|
WalletContractData,
|
||||||
|
WalletRefundItem,
|
||||||
} from "../types/dbTypes";
|
} from "../types/dbTypes";
|
||||||
import { checkDbInvariant, checkLogicInvariant } from "../util/invariants";
|
import { checkDbInvariant, checkLogicInvariant } from "../util/invariants";
|
||||||
import { AmountJson, Amounts, codecForAmountString } from "../util/amounts";
|
import { AmountJson, Amounts, codecForAmountString } from "../util/amounts";
|
||||||
@ -77,6 +80,7 @@ import {
|
|||||||
encodeCrock,
|
encodeCrock,
|
||||||
getRandomBytes,
|
getRandomBytes,
|
||||||
hash,
|
hash,
|
||||||
|
rsaBlind,
|
||||||
stringToBytes,
|
stringToBytes,
|
||||||
} from "../crypto/talerCrypto";
|
} from "../crypto/talerCrypto";
|
||||||
import { canonicalizeBaseUrl, canonicalJson, j2s } from "../util/helpers";
|
import { canonicalizeBaseUrl, canonicalJson, j2s } from "../util/helpers";
|
||||||
@ -102,6 +106,7 @@ import { gzipSync } from "fflate";
|
|||||||
import { kdf } from "../crypto/primitives/kdf";
|
import { kdf } from "../crypto/primitives/kdf";
|
||||||
import { initRetryInfo } from "../util/retries";
|
import { initRetryInfo } from "../util/retries";
|
||||||
import { RefreshReason } from "../types/walletTypes";
|
import { RefreshReason } from "../types/walletTypes";
|
||||||
|
import { CryptoApi } from "../crypto/workers/cryptoApi";
|
||||||
|
|
||||||
interface WalletBackupConfState {
|
interface WalletBackupConfState {
|
||||||
deviceId: string;
|
deviceId: string;
|
||||||
@ -461,6 +466,8 @@ export async function exportBackup(
|
|||||||
? undefined
|
? undefined
|
||||||
: purch.abortStatus,
|
: purch.abortStatus,
|
||||||
nonce_priv: purch.noncePriv,
|
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 {
|
interface BackupCryptoPrecomputedData {
|
||||||
denomPubToHash: Record<string, string>;
|
denomPubToHash: Record<string, string>;
|
||||||
coinPrivToCompletedCoin: Record<string, CompletedCoin>;
|
coinPrivToCompletedCoin: Record<string, CompletedCoin>;
|
||||||
proposalNoncePrivToProposalPub: { [priv: string]: string };
|
proposalNoncePrivToPub: { [priv: string]: string };
|
||||||
proposalIdToContractTermsHash: { [proposalId: string]: string };
|
proposalIdToContractTermsHash: { [proposalId: string]: string };
|
||||||
reservePrivToPub: Record<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 {
|
function checkBackupInvariant(b: boolean, m?: string): asserts b {
|
||||||
if (!b) {
|
if (!b) {
|
||||||
if (m) {
|
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(
|
function getDenomSelStateFromBackup(
|
||||||
tx: TransactionHandle<typeof Stores.denominations>,
|
tx: TransactionHandle<typeof Stores.denominations>,
|
||||||
sel: BackupDenomSel,
|
sel: BackupDenomSel,
|
||||||
@ -959,9 +1114,7 @@ export async function importBackup(
|
|||||||
orderId: backupProposal.order_id,
|
orderId: backupProposal.order_id,
|
||||||
noncePriv: backupProposal.nonce_priv,
|
noncePriv: backupProposal.nonce_priv,
|
||||||
noncePub:
|
noncePub:
|
||||||
cryptoComp.proposalNoncePrivToProposalPub[
|
cryptoComp.proposalNoncePrivToPub[backupProposal.nonce_priv],
|
||||||
backupProposal.nonce_priv
|
|
||||||
],
|
|
||||||
proposalId: backupProposal.proposal_id,
|
proposalId: backupProposal.proposal_id,
|
||||||
repurchaseProposalId: backupProposal.repurchase_proposal_id,
|
repurchaseProposalId: backupProposal.repurchase_proposal_id,
|
||||||
retryInfo: initRetryInfo(false),
|
retryInfo: initRetryInfo(false),
|
||||||
@ -977,7 +1130,138 @@ export async function importBackup(
|
|||||||
backupPurchase.proposal_id,
|
backupPurchase.proposal_id,
|
||||||
);
|
);
|
||||||
if (!existingPurchase) {
|
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,
|
purchase.download.contractData.merchantBaseUrl,
|
||||||
).href;
|
).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 = {
|
const reqBody = {
|
||||||
coins: purchase.coinDepositPermissions,
|
coins: depositPermissions,
|
||||||
session_id: purchase.lastSessionId,
|
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.
|
* Add a contract to the wallet and sign coins, and send them.
|
||||||
*/
|
*/
|
||||||
@ -1248,37 +1305,11 @@ export async function confirmPay(
|
|||||||
throw Error("insufficient balance");
|
throw Error("insufficient balance");
|
||||||
}
|
}
|
||||||
|
|
||||||
const depositPermissions: CoinDepositPermission[] = [];
|
const depositPermissions = await generateDepositPermissions(
|
||||||
for (let i = 0; i < res.coinPubs.length; i++) {
|
ws,
|
||||||
const coin = await ws.db.get(Stores.coins, res.coinPubs[i]);
|
res,
|
||||||
if (!coin) {
|
d.contractData,
|
||||||
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);
|
|
||||||
}
|
|
||||||
purchase = await recordConfirmPay(
|
purchase = await recordConfirmPay(
|
||||||
ws,
|
ws,
|
||||||
proposal,
|
proposal,
|
||||||
|
@ -501,9 +501,9 @@ export async function applyRefund(
|
|||||||
const p = purchase;
|
const p = purchase;
|
||||||
|
|
||||||
let amountRefundGranted = Amounts.getZero(
|
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;
|
let pendingAtExchange = false;
|
||||||
|
|
||||||
@ -531,21 +531,21 @@ export async function applyRefund(
|
|||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
contractTermsHash: purchase.contractData.contractTermsHash,
|
contractTermsHash: purchase.download.contractData.contractTermsHash,
|
||||||
proposalId: purchase.proposalId,
|
proposalId: purchase.proposalId,
|
||||||
amountEffectivePaid: Amounts.stringify(purchase.totalPayCost),
|
amountEffectivePaid: Amounts.stringify(purchase.totalPayCost),
|
||||||
amountRefundGone: Amounts.stringify(amountRefundGone),
|
amountRefundGone: Amounts.stringify(amountRefundGone),
|
||||||
amountRefundGranted: Amounts.stringify(amountRefundGranted),
|
amountRefundGranted: Amounts.stringify(amountRefundGranted),
|
||||||
pendingAtExchange,
|
pendingAtExchange,
|
||||||
info: {
|
info: {
|
||||||
contractTermsHash: purchase.contractData.contractTermsHash,
|
contractTermsHash: purchase.download.contractData.contractTermsHash,
|
||||||
merchant: purchase.contractData.merchant,
|
merchant: purchase.download.contractData.merchant,
|
||||||
orderId: purchase.contractData.orderId,
|
orderId: purchase.download.contractData.orderId,
|
||||||
products: purchase.contractData.products,
|
products: purchase.download.contractData.products,
|
||||||
summary: purchase.contractData.summary,
|
summary: purchase.download.contractData.summary,
|
||||||
fulfillmentMessage: purchase.contractData.fulfillmentMessage,
|
fulfillmentMessage: purchase.download.contractData.fulfillmentMessage,
|
||||||
summary_i18n: purchase.contractData.summaryI18n,
|
summary_i18n: purchase.download.contractData.summaryI18n,
|
||||||
fulfillmentMessage_i18n: purchase.contractData.fulfillmentMessageI18n,
|
fulfillmentMessage_i18n: purchase.download.contractData.fulfillmentMessageI18n,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -594,14 +594,14 @@ async function processPurchaseQueryRefundImpl(
|
|||||||
|
|
||||||
if (purchase.timestampFirstSuccessfulPay) {
|
if (purchase.timestampFirstSuccessfulPay) {
|
||||||
const requestUrl = new URL(
|
const requestUrl = new URL(
|
||||||
`orders/${purchase.contractData.orderId}/refund`,
|
`orders/${purchase.download.contractData.orderId}/refund`,
|
||||||
purchase.contractData.merchantBaseUrl,
|
purchase.download.contractData.merchantBaseUrl,
|
||||||
);
|
);
|
||||||
|
|
||||||
logger.trace(`making refund request to ${requestUrl.href}`);
|
logger.trace(`making refund request to ${requestUrl.href}`);
|
||||||
|
|
||||||
const request = await ws.http.postJson(requestUrl.href, {
|
const request = await ws.http.postJson(requestUrl.href, {
|
||||||
h_contract: purchase.contractData.contractTermsHash,
|
h_contract: purchase.download.contractData.contractTermsHash,
|
||||||
});
|
});
|
||||||
|
|
||||||
logger.trace(
|
logger.trace(
|
||||||
@ -622,8 +622,8 @@ async function processPurchaseQueryRefundImpl(
|
|||||||
);
|
);
|
||||||
} else if (purchase.abortStatus === AbortStatus.AbortRefund) {
|
} else if (purchase.abortStatus === AbortStatus.AbortRefund) {
|
||||||
const requestUrl = new URL(
|
const requestUrl = new URL(
|
||||||
`orders/${purchase.contractData.orderId}/abort`,
|
`orders/${purchase.download.contractData.orderId}/abort`,
|
||||||
purchase.contractData.merchantBaseUrl,
|
purchase.download.contractData.merchantBaseUrl,
|
||||||
);
|
);
|
||||||
|
|
||||||
const abortingCoins: AbortingCoin[] = [];
|
const abortingCoins: AbortingCoin[] = [];
|
||||||
@ -641,7 +641,7 @@ async function processPurchaseQueryRefundImpl(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const abortReq: AbortRequest = {
|
const abortReq: AbortRequest = {
|
||||||
h_contract: purchase.contractData.contractTermsHash,
|
h_contract: purchase.download.contractData.contractTermsHash,
|
||||||
coins: abortingCoins,
|
coins: abortingCoins,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -669,7 +669,7 @@ async function processPurchaseQueryRefundImpl(
|
|||||||
purchase.payCoinSelection.coinContributions[i],
|
purchase.payCoinSelection.coinContributions[i],
|
||||||
),
|
),
|
||||||
rtransaction_id: 0,
|
rtransaction_id: 0,
|
||||||
execution_time: timestampAddDuration(purchase.contractData.timestamp, {
|
execution_time: timestampAddDuration(purchase.download.contractData.timestamp, {
|
||||||
d_ms: 1000,
|
d_ms: 1000,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
@ -207,12 +207,13 @@ export async function getTransactions(
|
|||||||
if (
|
if (
|
||||||
shouldSkipCurrency(
|
shouldSkipCurrency(
|
||||||
transactionsRequest,
|
transactionsRequest,
|
||||||
pr.contractData.amount.currency,
|
pr.download.contractData.amount.currency,
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (shouldSkipSearch(transactionsRequest, [pr.contractData.summary])) {
|
const contractData = pr.download.contractData;
|
||||||
|
if (shouldSkipSearch(transactionsRequest, [contractData.summary])) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const proposal = await tx.get(Stores.proposals, pr.proposalId);
|
const proposal = await tx.get(Stores.proposals, pr.proposalId);
|
||||||
@ -220,15 +221,15 @@ export async function getTransactions(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const info: OrderShortInfo = {
|
const info: OrderShortInfo = {
|
||||||
merchant: pr.contractData.merchant,
|
merchant: contractData.merchant,
|
||||||
orderId: pr.contractData.orderId,
|
orderId: contractData.orderId,
|
||||||
products: pr.contractData.products,
|
products: contractData.products,
|
||||||
summary: pr.contractData.summary,
|
summary: contractData.summary,
|
||||||
summary_i18n: pr.contractData.summaryI18n,
|
summary_i18n: contractData.summaryI18n,
|
||||||
contractTermsHash: pr.contractData.contractTermsHash,
|
contractTermsHash: contractData.contractTermsHash,
|
||||||
};
|
};
|
||||||
if (pr.contractData.fulfillmentUrl !== "") {
|
if (contractData.fulfillmentUrl !== "") {
|
||||||
info.fulfillmentUrl = pr.contractData.fulfillmentUrl;
|
info.fulfillmentUrl = contractData.fulfillmentUrl;
|
||||||
}
|
}
|
||||||
const paymentTransactionId = makeEventId(
|
const paymentTransactionId = makeEventId(
|
||||||
TransactionType.Payment,
|
TransactionType.Payment,
|
||||||
@ -237,7 +238,7 @@ export async function getTransactions(
|
|||||||
const err = pr.lastPayError ?? pr.lastRefundStatusError;
|
const err = pr.lastPayError ?? pr.lastRefundStatusError;
|
||||||
transactions.push({
|
transactions.push({
|
||||||
type: TransactionType.Payment,
|
type: TransactionType.Payment,
|
||||||
amountRaw: Amounts.stringify(pr.contractData.amount),
|
amountRaw: Amounts.stringify(contractData.amount),
|
||||||
amountEffective: Amounts.stringify(pr.totalPayCost),
|
amountEffective: Amounts.stringify(pr.totalPayCost),
|
||||||
status: pr.timestampFirstSuccessfulPay
|
status: pr.timestampFirstSuccessfulPay
|
||||||
? PaymentStatus.Paid
|
? PaymentStatus.Paid
|
||||||
@ -267,9 +268,9 @@ export async function getTransactions(
|
|||||||
groupKey,
|
groupKey,
|
||||||
);
|
);
|
||||||
let r0: WalletRefundItem | undefined;
|
let r0: WalletRefundItem | undefined;
|
||||||
let amountRaw = Amounts.getZero(pr.contractData.amount.currency);
|
let amountRaw = Amounts.getZero(contractData.amount.currency);
|
||||||
let amountEffective = Amounts.getZero(
|
let amountEffective = Amounts.getZero(
|
||||||
pr.contractData.amount.currency,
|
contractData.amount.currency,
|
||||||
);
|
);
|
||||||
for (const rk of Object.keys(pr.refunds)) {
|
for (const rk of Object.keys(pr.refunds)) {
|
||||||
const refund = pr.refunds[rk];
|
const refund = pr.refunds[rk];
|
||||||
|
@ -34,6 +34,11 @@
|
|||||||
* 6. Returning money to own bank account isn't supported/exported yet.
|
* 6. Returning money to own bank account isn't supported/exported yet.
|
||||||
* 7. Peer-to-peer payments aren't supported yet.
|
* 7. Peer-to-peer payments aren't supported yet.
|
||||||
* 8. Next update time / next refresh time isn't backed up 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:
|
* Questions:
|
||||||
* 1. What happens when two backups are merged that have
|
* 1. What happens when two backups are merged that have
|
||||||
@ -42,6 +47,10 @@
|
|||||||
* 2. Should we make more information forgettable? I.e. is
|
* 2. Should we make more information forgettable? I.e. is
|
||||||
* the coin selection still relevant for a purchase after the coins
|
* the coin selection still relevant for a purchase after the coins
|
||||||
* are legally expired?
|
* 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:
|
* General considerations / decisions:
|
||||||
* 1. Information about previously occurring errors and
|
* 1. Information about previously occurring errors and
|
||||||
@ -78,6 +87,9 @@ type DeviceIdString = string;
|
|||||||
*/
|
*/
|
||||||
type ClockValue = number;
|
type ClockValue = number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Contract terms JSON.
|
||||||
|
*/
|
||||||
type RawContractTerms = any;
|
type RawContractTerms = any;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -751,10 +763,8 @@ export interface BackupPurchase {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Signature on the contract terms.
|
* 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
|
* Private key for the nonce. Might eventually be used
|
||||||
@ -774,6 +784,19 @@ export interface BackupPurchase {
|
|||||||
contribution: BackupAmountString;
|
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
|
* Timestamp of the first time that sending a payment to the merchant
|
||||||
* for this purchase was successful.
|
* for this purchase was successful.
|
||||||
|
@ -1206,8 +1206,10 @@ export interface PurchaseRecord {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Deposit permissions, available once the user has accepted the payment.
|
* 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;
|
payCoinSelection: PayCoinSelection;
|
||||||
|
|
||||||
|
@ -398,4 +398,5 @@ export const Amounts = {
|
|||||||
fromFloat: fromFloat,
|
fromFloat: fromFloat,
|
||||||
copy: copy,
|
copy: copy,
|
||||||
fractionalBase: fractionalBase,
|
fractionalBase: fractionalBase,
|
||||||
|
divide: divide,
|
||||||
};
|
};
|
||||||
|
@ -840,7 +840,7 @@ export class Wallet {
|
|||||||
]).amount;
|
]).amount;
|
||||||
const totalFees = totalRefundFees;
|
const totalFees = totalRefundFees;
|
||||||
return {
|
return {
|
||||||
contractTerms: JSON.parse(purchase.contractTermsRaw),
|
contractTerms: JSON.parse(purchase.download.contractTermsRaw),
|
||||||
hasRefund: purchase.timestampLastRefundStatus !== undefined,
|
hasRefund: purchase.timestampLastRefundStatus !== undefined,
|
||||||
totalRefundAmount: totalRefundAmount,
|
totalRefundAmount: totalRefundAmount,
|
||||||
totalRefundAndRefreshFees: totalFees,
|
totalRefundAndRefreshFees: totalFees,
|
||||||
|
Loading…
Reference in New Issue
Block a user