wallet-core: put contract terms into separate object store

This commit is contained in:
Florian Dold 2022-10-09 02:23:06 +02:00
parent 8ac5080607
commit 19f3e6321d
No known key found for this signature in database
GPG Key ID: D2E4F00F29D02A4B
6 changed files with 183 additions and 110 deletions

View File

@ -909,6 +909,8 @@ export interface BackupPurchase {
/** /**
* Signature on the contract terms. * Signature on the contract terms.
*
* FIXME: Better name needed.
*/ */
merchant_sig?: string; merchant_sig?: string;

View File

@ -1085,18 +1085,16 @@ export enum PurchaseStatus {
Paid = OperationStatusRange.DORMANT_START + 5, Paid = OperationStatusRange.DORMANT_START + 5,
} }
/**
* Partial information about the downloaded proposal.
* Only contains data that is relevant for indexing on the
* "purchases" object stores.
*/
export interface ProposalDownload { export interface ProposalDownload {
/** contractTermsHash: string;
* The contract that was offered by the merchant. fulfillmentUrl?: string;
*/ currency: string;
contractTermsRaw: any; contractTermsMerchantSig: string;
/**
* Extracted / parsed data from the contract terms.
*
* FIXME: Do we need to store *all* that data in duplicate?
*/
contractData: WalletContractData;
} }
export interface PurchasePayInfo { export interface PurchasePayInfo {
@ -1723,6 +1721,7 @@ export interface PeerPullPaymentInitiationRecord {
* Contract terms for the other party. * Contract terms for the other party.
* *
* FIXME: Nail down type! * FIXME: Nail down type!
* FIXME: Put in contractTerms store
*/ */
contractTerms: any; contractTerms: any;
} }
@ -1819,6 +1818,18 @@ export interface CoinAvailabilityRecord {
freshCoinCount: number; freshCoinCount: number;
} }
export interface ContractTermsRecord {
/**
* Contract terms hash.
*/
h: string;
/**
* Contract terms JSON.
*/
contractTermsRaw: any;
}
/** /**
* Schema definition for the IndexedDB * Schema definition for the IndexedDB
* wallet database. * wallet database.
@ -1937,13 +1948,8 @@ export const WalletStoresV1 = {
byStatus: describeIndex("byStatus", "purchaseStatus"), byStatus: describeIndex("byStatus", "purchaseStatus"),
byFulfillmentUrl: describeIndex( byFulfillmentUrl: describeIndex(
"byFulfillmentUrl", "byFulfillmentUrl",
"download.contractData.fulfillmentUrl", "download.fulfillmentUrl",
), ),
// FIXME: Deduplicate!
byMerchantUrlAndOrderId: describeIndex("byMerchantUrlAndOrderId", [
"download.contractData.merchantBaseUrl",
"download.contractData.orderId",
]),
byUrlAndOrderId: describeIndex("byUrlAndOrderId", [ byUrlAndOrderId: describeIndex("byUrlAndOrderId", [
"merchantBaseUrl", "merchantBaseUrl",
"orderId", "orderId",
@ -2088,6 +2094,13 @@ export const WalletStoresV1 = {
}), }),
{}, {},
), ),
contractTerms: describeStore(
"contractTerms",
describeContents<ContractTermsRecord>({
keyPath: "h",
}),
{},
),
}; };
/** /**

View File

@ -88,6 +88,7 @@ export async function exportBackup(
x.exchanges, x.exchanges,
x.exchangeDetails, x.exchangeDetails,
x.coins, x.coins,
x.contractTerms,
x.denominations, x.denominations,
x.purchases, x.purchases,
x.refreshGroups, x.refreshGroups,
@ -353,7 +354,7 @@ export async function exportBackup(
const purchaseProposalIdSet = new Set<string>(); const purchaseProposalIdSet = new Set<string>();
await tx.purchases.iter().forEach((purch) => { await tx.purchases.iter().forEachAsync(async (purch) => {
const refunds: BackupRefundItem[] = []; const refunds: BackupRefundItem[] = [];
purchaseProposalIdSet.add(purch.proposalId); purchaseProposalIdSet.add(purch.proposalId);
for (const refundKey of Object.keys(purch.refunds)) { for (const refundKey of Object.keys(purch.refunds)) {
@ -418,8 +419,18 @@ export async function exportBackup(
}; };
} }
let contractTermsRaw = undefined;
if (purch.download) {
const contractTermsRecord = await tx.contractTerms.get(
purch.download.contractTermsHash,
);
if (contractTermsRecord) {
contractTermsRaw = contractTermsRecord.contractTermsRaw;
}
}
backupPurchases.push({ backupPurchases.push({
contract_terms_raw: purch.download?.contractTermsRaw, contract_terms_raw: contractTermsRaw,
auto_refund_deadline: purch.autoRefundDeadline, auto_refund_deadline: purch.autoRefundDeadline,
merchant_pay_sig: purch.merchantPaySig, merchant_pay_sig: purch.merchantPaySig,
pay_info: backupPayInfo, pay_info: backupPayInfo,
@ -428,7 +439,7 @@ export async function exportBackup(
timestamp_accepted: purch.timestampAccept, timestamp_accepted: purch.timestampAccept,
timestamp_first_successful_pay: purch.timestampFirstSuccessfulPay, timestamp_first_successful_pay: purch.timestampFirstSuccessfulPay,
nonce_priv: purch.noncePriv, nonce_priv: purch.noncePriv,
merchant_sig: purch.download?.contractData.merchantSig, merchant_sig: purch.download?.contractTermsMerchantSig,
claim_token: purch.claimToken, claim_token: purch.claimToken,
merchant_base_url: purch.merchantBaseUrl, merchant_base_url: purch.merchantBaseUrl,
order_id: purch.orderId, order_id: purch.orderId,

View File

@ -64,6 +64,7 @@ import { checkLogicInvariant } from "../../util/invariants.js";
import { GetReadOnlyAccess, GetReadWriteAccess } from "../../util/query.js"; import { GetReadOnlyAccess, GetReadWriteAccess } from "../../util/query.js";
import { makeCoinAvailable, makeEventId, TombstoneTag } from "../common.js"; import { makeCoinAvailable, makeEventId, TombstoneTag } from "../common.js";
import { getExchangeDetails } from "../exchanges.js"; import { getExchangeDetails } from "../exchanges.js";
import { extractContractData } from "../pay-merchant.js";
import { provideBackupState } from "./state.js"; import { provideBackupState } from "./state.js";
const logger = new Logger("operations/backup/import.ts"); const logger = new Logger("operations/backup/import.ts");
@ -630,49 +631,25 @@ export async function importBackup(
maxWireFee = Amounts.getZero(amount.currency); maxWireFee = Amounts.getZero(amount.currency);
} }
const download: ProposalDownload = { const download: ProposalDownload = {
contractData: { contractTermsHash,
amount, contractTermsMerchantSig: backupPurchase.merchant_sig!,
contractTermsHash: contractTermsHash, currency: amount.currency,
fulfillmentUrl: parsedContractTerms.fulfillment_url ?? "", fulfillmentUrl: backupPurchase.contract_terms_raw.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.auditor_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,
deliveryDate: parsedContractTerms.delivery_date,
deliveryLocation: parsedContractTerms.delivery_location,
},
contractTermsRaw: backupPurchase.contract_terms_raw,
}; };
const contractData = extractContractData(
backupPurchase.contract_terms_raw,
contractTermsHash,
download.contractTermsMerchantSig,
);
let payInfo: PurchasePayInfo | undefined = undefined; let payInfo: PurchasePayInfo | undefined = undefined;
if (backupPurchase.pay_info) { if (backupPurchase.pay_info) {
payInfo = { payInfo = {
coinDepositPermissions: undefined, coinDepositPermissions: undefined,
payCoinSelection: await recoverPayCoinSelection( payCoinSelection: await recoverPayCoinSelection(
tx, tx,
download.contractData, contractData,
backupPurchase.pay_info, backupPurchase.pay_info,
), ),
payCoinSelectionUid: backupPurchase.pay_info.pay_coins_uid, payCoinSelectionUid: backupPurchase.pay_info.pay_coins_uid,

View File

@ -115,6 +115,7 @@ import {
throwUnexpectedRequestError, throwUnexpectedRequestError,
} from "../util/http.js"; } from "../util/http.js";
import { checkDbInvariant, checkLogicInvariant } from "../util/invariants.js"; import { checkDbInvariant, checkLogicInvariant } from "../util/invariants.js";
import { GetReadOnlyAccess } from "../util/query.js";
import { import {
OperationAttemptResult, OperationAttemptResult,
OperationAttemptResultType, OperationAttemptResultType,
@ -256,12 +257,34 @@ function getPayRequestTimeout(purchase: PurchaseRecord): Duration {
* (Async since in the future this will query the DB.) * (Async since in the future this will query the DB.)
*/ */
export async function expectProposalDownload( export async function expectProposalDownload(
ws: InternalWalletState,
p: PurchaseRecord, p: PurchaseRecord,
): Promise<ProposalDownload> { ): Promise<{
contractData: WalletContractData;
contractTermsRaw: any;
}> {
if (!p.download) { if (!p.download) {
throw Error("expected proposal to be downloaded"); throw Error("expected proposal to be downloaded");
} }
return p.download; const download = p.download;
return await ws.db
.mktx((x) => [x.contractTerms])
.runReadOnly(async (tx) => {
const contractTerms = await tx.contractTerms.get(
download.contractTermsHash,
);
if (!contractTerms) {
throw Error("contract terms not found");
}
return {
contractData: extractContractData(
contractTerms.contractTermsRaw,
download.contractTermsHash,
download.contractTermsMerchantSig,
),
contractTermsRaw: contractTerms.contractTermsRaw,
};
});
} }
export function extractContractData( export function extractContractData(
@ -494,7 +517,7 @@ export async function processDownloadProposal(
logger.trace(`extracted contract data: ${j2s(contractData)}`); logger.trace(`extracted contract data: ${j2s(contractData)}`);
await ws.db await ws.db
.mktx((x) => [x.purchases]) .mktx((x) => [x.purchases, x.contractTerms])
.runReadWrite(async (tx) => { .runReadWrite(async (tx) => {
const p = await tx.purchases.get(proposalId); const p = await tx.purchases.get(proposalId);
if (!p) { if (!p) {
@ -504,9 +527,15 @@ export async function processDownloadProposal(
return; return;
} }
p.download = { p.download = {
contractData, contractTermsHash,
contractTermsRaw: proposalResp.contract_terms, contractTermsMerchantSig: contractData.merchantSig,
currency: contractData.amount.currency,
fulfillmentUrl: contractData.fulfillmentUrl,
}; };
await tx.contractTerms.put({
h: contractTermsHash,
contractTermsRaw: proposalResp.contract_terms,
});
if ( if (
fulfillmentUrl && fulfillmentUrl &&
(fulfillmentUrl.startsWith("http://") || (fulfillmentUrl.startsWith("http://") ||
@ -636,7 +665,7 @@ async function storeFirstPaySuccess(
): Promise<void> { ): Promise<void> {
const now = AbsoluteTime.toTimestamp(AbsoluteTime.now()); const now = AbsoluteTime.toTimestamp(AbsoluteTime.now());
await ws.db await ws.db
.mktx((x) => [x.purchases]) .mktx((x) => [x.purchases, x.contractTerms])
.runReadWrite(async (tx) => { .runReadWrite(async (tx) => {
const purchase = await tx.purchases.get(proposalId); const purchase = await tx.purchases.get(proposalId);
@ -655,7 +684,18 @@ async function storeFirstPaySuccess(
purchase.timestampFirstSuccessfulPay = now; purchase.timestampFirstSuccessfulPay = now;
purchase.lastSessionId = sessionId; purchase.lastSessionId = sessionId;
purchase.merchantPaySig = paySig; purchase.merchantPaySig = paySig;
const protoAr = purchase.download!.contractData.autoRefund; const dl = purchase.download;
checkDbInvariant(!!dl);
const contractTermsRecord = await tx.contractTerms.get(
dl.contractTermsHash,
);
checkDbInvariant(!!contractTermsRecord);
const contractData = extractContractData(
contractTermsRecord.contractTermsRaw,
dl.contractTermsHash,
dl.contractTermsMerchantSig,
);
const protoAr = contractData.autoRefund;
if (protoAr) { if (protoAr) {
const ar = Duration.fromTalerProtocolDuration(protoAr); const ar = Duration.fromTalerProtocolDuration(protoAr);
logger.info("auto_refund present"); logger.info("auto_refund present");
@ -739,7 +779,7 @@ async function handleInsufficientFunds(
throw new TalerProtocolViolationError(); throw new TalerProtocolViolationError();
} }
const { contractData } = proposal.download!; const { contractData } = await expectProposalDownload(ws, proposal);
const prevPayCoins: PreviousPayCoins = []; const prevPayCoins: PreviousPayCoins = [];
@ -1254,11 +1294,7 @@ export async function checkPaymentByProposalId(
throw Error("existing proposal is in wrong state"); throw Error("existing proposal is in wrong state");
} }
} }
const d = proposal.download; const d = await expectProposalDownload(ws, proposal);
if (!d) {
logger.error("bad proposal", proposal);
throw Error("proposal is in invalid state");
}
const contractData = d.contractData; const contractData = d.contractData;
const merchantSig = d.contractData.merchantSig; const merchantSig = d.contractData.merchantSig;
if (!merchantSig) { if (!merchantSig) {
@ -1338,7 +1374,7 @@ export async function checkPaymentByProposalId(
// FIXME: This does not surface the original error // FIXME: This does not surface the original error
throw Error("submitting pay failed"); throw Error("submitting pay failed");
} }
const download = await expectProposalDownload(purchase); const download = await expectProposalDownload(ws, purchase);
return { return {
status: PreparePayResultType.AlreadyConfirmed, status: PreparePayResultType.AlreadyConfirmed,
contractTerms: download.contractTermsRaw, contractTerms: download.contractTermsRaw,
@ -1349,7 +1385,7 @@ export async function checkPaymentByProposalId(
proposalId, proposalId,
}; };
} else if (!purchase.timestampFirstSuccessfulPay) { } else if (!purchase.timestampFirstSuccessfulPay) {
const download = await expectProposalDownload(purchase); const download = await expectProposalDownload(ws, purchase);
return { return {
status: PreparePayResultType.AlreadyConfirmed, status: PreparePayResultType.AlreadyConfirmed,
contractTerms: download.contractTermsRaw, contractTerms: download.contractTermsRaw,
@ -1364,7 +1400,7 @@ export async function checkPaymentByProposalId(
purchase.purchaseStatus === PurchaseStatus.Paid || purchase.purchaseStatus === PurchaseStatus.Paid ||
purchase.purchaseStatus === PurchaseStatus.QueryingRefund || purchase.purchaseStatus === PurchaseStatus.QueryingRefund ||
purchase.purchaseStatus === PurchaseStatus.QueryingAutoRefund; purchase.purchaseStatus === PurchaseStatus.QueryingAutoRefund;
const download = await expectProposalDownload(purchase); const download = await expectProposalDownload(ws, purchase);
return { return {
status: PreparePayResultType.AlreadyConfirmed, status: PreparePayResultType.AlreadyConfirmed,
contractTerms: download.contractTermsRaw, contractTerms: download.contractTermsRaw,
@ -1392,11 +1428,9 @@ export async function getContractTermsDetails(
throw Error(`proposal with id ${proposalId} not found`); throw Error(`proposal with id ${proposalId} not found`);
} }
if (!proposal.download || !proposal.download.contractData) { const d = await expectProposalDownload(ws, proposal);
throw Error("proposal is in invalid state");
}
return proposal.download.contractData; return d.contractData;
} }
/** /**
@ -1516,12 +1550,13 @@ export async function runPayForConfirmPay(
.runReadOnly(async (tx) => { .runReadOnly(async (tx) => {
return tx.purchases.get(proposalId); return tx.purchases.get(proposalId);
}); });
if (!purchase?.download) { if (!purchase) {
throw Error("purchase record not available anymore"); throw Error("purchase record not available anymore");
} }
const d = await expectProposalDownload(ws, purchase);
return { return {
type: ConfirmPayResultType.Done, type: ConfirmPayResultType.Done,
contractTerms: purchase.download.contractTermsRaw, contractTerms: d.contractTermsRaw,
transactionId: makeEventId(TransactionType.Payment, proposalId), transactionId: makeEventId(TransactionType.Payment, proposalId),
}; };
} }
@ -1599,7 +1634,7 @@ export async function confirmPay(
throw Error(`proposal with id ${proposalId} not found`); throw Error(`proposal with id ${proposalId} not found`);
} }
const d = proposal.download; const d = await expectProposalDownload(ws, proposal);
if (!d) { if (!d) {
throw Error("proposal is in invalid state"); throw Error("proposal is in invalid state");
} }
@ -1810,7 +1845,7 @@ export async function processPurchasePay(
const payInfo = purchase.payInfo; const payInfo = purchase.payInfo;
checkDbInvariant(!!payInfo, "payInfo"); checkDbInvariant(!!payInfo, "payInfo");
const download = await expectProposalDownload(purchase); const download = await expectProposalDownload(ws, purchase);
if (!purchase.merchantPaySig) { if (!purchase.merchantPaySig) {
const payUrl = new URL( const payUrl = new URL(
`orders/${download.contractData.orderId}/pay`, `orders/${download.contractData.orderId}/pay`,
@ -2007,7 +2042,7 @@ export async function prepareRefund(
const purchase = await ws.db const purchase = await ws.db
.mktx((x) => [x.purchases]) .mktx((x) => [x.purchases])
.runReadOnly(async (tx) => { .runReadOnly(async (tx) => {
return tx.purchases.indexes.byMerchantUrlAndOrderId.get([ return tx.purchases.indexes.byUrlAndOrderId.get([
parseResult.merchantBaseUrl, parseResult.merchantBaseUrl,
parseResult.orderId, parseResult.orderId,
]); ]);
@ -2020,10 +2055,10 @@ export async function prepareRefund(
} }
const awaiting = await queryAndSaveAwaitingRefund(ws, purchase); const awaiting = await queryAndSaveAwaitingRefund(ws, purchase);
const summary = await calculateRefundSummary(purchase); const summary = await calculateRefundSummary(ws, purchase);
const proposalId = purchase.proposalId; const proposalId = purchase.proposalId;
const { contractData: c } = await expectProposalDownload(purchase); const { contractData: c } = await expectProposalDownload(ws, purchase);
return { return {
proposalId, proposalId,
@ -2380,9 +2415,10 @@ async function acceptRefunds(
} }
async function calculateRefundSummary( async function calculateRefundSummary(
ws: InternalWalletState,
p: PurchaseRecord, p: PurchaseRecord,
): Promise<RefundSummary> { ): Promise<RefundSummary> {
const download = await expectProposalDownload(p); const download = await expectProposalDownload(ws, p);
let amountRefundGranted = Amounts.getZero( let amountRefundGranted = Amounts.getZero(
download.contractData.amount.currency, download.contractData.amount.currency,
); );
@ -2456,7 +2492,7 @@ export async function applyRefund(
const purchase = await ws.db const purchase = await ws.db
.mktx((x) => [x.purchases]) .mktx((x) => [x.purchases])
.runReadOnly(async (tx) => { .runReadOnly(async (tx) => {
return tx.purchases.indexes.byMerchantUrlAndOrderId.get([ return tx.purchases.indexes.byUrlAndOrderId.get([
parseResult.merchantBaseUrl, parseResult.merchantBaseUrl,
parseResult.orderId, parseResult.orderId,
]); ]);
@ -2513,8 +2549,8 @@ export async function applyRefundFromPurchaseId(
throw Error("purchase no longer exists"); throw Error("purchase no longer exists");
} }
const summary = await calculateRefundSummary(purchase); const summary = await calculateRefundSummary(ws, purchase);
const download = await expectProposalDownload(purchase); const download = await expectProposalDownload(ws, purchase);
return { return {
contractTermsHash: download.contractData.contractTermsHash, contractTermsHash: download.contractData.contractTermsHash,
@ -2542,7 +2578,7 @@ async function queryAndSaveAwaitingRefund(
purchase: PurchaseRecord, purchase: PurchaseRecord,
waitForAutoRefund?: boolean, waitForAutoRefund?: boolean,
): Promise<AmountJson> { ): Promise<AmountJson> {
const download = await expectProposalDownload(purchase); const download = await expectProposalDownload(ws, purchase);
const requestUrl = new URL( const requestUrl = new URL(
`orders/${download.contractData.orderId}`, `orders/${download.contractData.orderId}`,
download.contractData.merchantBaseUrl, download.contractData.merchantBaseUrl,
@ -2621,7 +2657,7 @@ export async function processPurchaseQueryRefund(
return OperationAttemptResult.finishedEmpty(); return OperationAttemptResult.finishedEmpty();
} }
const download = await expectProposalDownload(purchase); const download = await expectProposalDownload(ws, purchase);
if (purchase.timestampFirstSuccessfulPay) { if (purchase.timestampFirstSuccessfulPay) {
if ( if (

View File

@ -48,6 +48,7 @@ import {
WalletRefundItem, WalletRefundItem,
WithdrawalGroupRecord, WithdrawalGroupRecord,
WithdrawalRecordType, WithdrawalRecordType,
WalletContractData,
} from "../db.js"; } from "../db.js";
import { InternalWalletState } from "../internal-wallet-state.js"; import { InternalWalletState } from "../internal-wallet-state.js";
import { checkDbInvariant } from "../util/invariants.js"; import { checkDbInvariant } from "../util/invariants.js";
@ -55,7 +56,11 @@ import { RetryTags } from "../util/retries.js";
import { makeEventId, TombstoneTag } from "./common.js"; import { makeEventId, TombstoneTag } from "./common.js";
import { processDepositGroup } from "./deposits.js"; import { processDepositGroup } from "./deposits.js";
import { getExchangeDetails } from "./exchanges.js"; import { getExchangeDetails } from "./exchanges.js";
import { expectProposalDownload, processPurchasePay } from "./pay-merchant.js"; import {
expectProposalDownload,
extractContractData,
processPurchasePay,
} from "./pay-merchant.js";
import { processRefreshGroup } from "./refresh.js"; import { processRefreshGroup } from "./refresh.js";
import { processTip } from "./tip.js"; import { processTip } from "./tip.js";
import { import {
@ -199,7 +204,7 @@ export async function getTransactionById(
}), }),
); );
const download = await expectProposalDownload(purchase); const download = await expectProposalDownload(ws, purchase);
const cleanRefunds = filteredRefunds.filter( const cleanRefunds = filteredRefunds.filter(
(x): x is WalletRefundItem => !!x, (x): x is WalletRefundItem => !!x,
@ -214,7 +219,12 @@ export async function getTransactionById(
const payOpId = RetryTags.forPay(purchase); const payOpId = RetryTags.forPay(purchase);
const payRetryRecord = await tx.operationRetries.get(payOpId); const payRetryRecord = await tx.operationRetries.get(payOpId);
return buildTransactionForPurchase(purchase, refunds, payRetryRecord); return buildTransactionForPurchase(
purchase,
contractData,
refunds,
payRetryRecord,
);
}); });
} else if (type === TransactionType.Refresh) { } else if (type === TransactionType.Refresh) {
const refreshGroupId = rest[0]; const refreshGroupId = rest[0];
@ -268,14 +278,19 @@ export async function getTransactionById(
), ),
); );
if (t) throw Error("deleted"); if (t) throw Error("deleted");
const download = await expectProposalDownload(purchase); const download = await expectProposalDownload(ws, purchase);
const contractData = download.contractData; const contractData = download.contractData;
const refunds = mergeRefundByExecutionTime( const refunds = mergeRefundByExecutionTime(
[theRefund], [theRefund],
Amounts.getZero(contractData.amount.currency), Amounts.getZero(contractData.amount.currency),
); );
return buildTransactionForRefund(purchase, refunds[0], undefined); return buildTransactionForRefund(
purchase,
contractData,
refunds[0],
undefined,
);
}); });
} else if (type === TransactionType.PeerPullDebit) { } else if (type === TransactionType.PeerPullDebit) {
const peerPullPaymentIncomingId = rest[0]; const peerPullPaymentIncomingId = rest[0];
@ -572,12 +587,10 @@ function mergeRefundByExecutionTime(
async function buildTransactionForRefund( async function buildTransactionForRefund(
purchaseRecord: PurchaseRecord, purchaseRecord: PurchaseRecord,
contractData: WalletContractData,
refundInfo: MergedRefundInfo, refundInfo: MergedRefundInfo,
ort?: OperationRetryRecord, ort?: OperationRetryRecord,
): Promise<Transaction> { ): Promise<Transaction> {
const download = await expectProposalDownload(purchaseRecord);
const contractData = download.contractData;
const info: OrderShortInfo = { const info: OrderShortInfo = {
merchant: contractData.merchant, merchant: contractData.merchant,
orderId: contractData.orderId, orderId: contractData.orderId,
@ -617,11 +630,10 @@ async function buildTransactionForRefund(
async function buildTransactionForPurchase( async function buildTransactionForPurchase(
purchaseRecord: PurchaseRecord, purchaseRecord: PurchaseRecord,
contractData: WalletContractData,
refundsInfo: MergedRefundInfo[], refundsInfo: MergedRefundInfo[],
ort?: OperationRetryRecord, ort?: OperationRetryRecord,
): Promise<Transaction> { ): Promise<Transaction> {
const download = await expectProposalDownload(purchaseRecord);
const contractData = download.contractData;
const zero = Amounts.getZero(contractData.amount.currency); const zero = Amounts.getZero(contractData.amount.currency);
const info: OrderShortInfo = { const info: OrderShortInfo = {
@ -689,7 +701,8 @@ async function buildTransactionForPurchase(
proposalId: purchaseRecord.proposalId, proposalId: purchaseRecord.proposalId,
info, info,
frozen: frozen:
purchaseRecord.purchaseStatus === PurchaseStatus.PaymentAbortFinished ?? false, purchaseRecord.purchaseStatus === PurchaseStatus.PaymentAbortFinished ??
false,
...(ort?.lastError ? { error: ort.lastError } : {}), ...(ort?.lastError ? { error: ort.lastError } : {}),
}; };
} }
@ -715,6 +728,7 @@ export async function getTransactions(
x.peerPushPaymentInitiations, x.peerPushPaymentInitiations,
x.planchets, x.planchets,
x.purchases, x.purchases,
x.contractTerms,
x.recoupGroups, x.recoupGroups,
x.tips, x.tips,
x.tombstones, x.tombstones,
@ -814,18 +828,28 @@ export async function getTransactions(
if (!purchase.payInfo) { if (!purchase.payInfo) {
return; return;
} }
if (shouldSkipCurrency(transactionsRequest, download.currency)) {
return;
}
const contractTermsRecord = await tx.contractTerms.get(
download.contractTermsHash,
);
if (!contractTermsRecord) {
return;
}
if ( if (
shouldSkipCurrency( shouldSkipSearch(transactionsRequest, [
transactionsRequest, contractTermsRecord?.contractTermsRaw?.summary || "",
download.contractData.amount.currency, ])
)
) { ) {
return; return;
} }
const contractData = download.contractData;
if (shouldSkipSearch(transactionsRequest, [contractData.summary])) { const contractData = extractContractData(
return; contractTermsRecord?.contractTermsRaw,
} download.contractTermsHash,
download.contractTermsMerchantSig,
);
const filteredRefunds = await Promise.all( const filteredRefunds = await Promise.all(
Object.values(purchase.refunds).map(async (r) => { Object.values(purchase.refunds).map(async (r) => {
@ -847,19 +871,29 @@ export async function getTransactions(
const refunds = mergeRefundByExecutionTime( const refunds = mergeRefundByExecutionTime(
cleanRefunds, cleanRefunds,
Amounts.getZero(contractData.amount.currency), Amounts.getZero(download.currency),
); );
refunds.forEach(async (refundInfo) => { refunds.forEach(async (refundInfo) => {
transactions.push( transactions.push(
await buildTransactionForRefund(purchase, refundInfo, undefined), await buildTransactionForRefund(
purchase,
contractData,
refundInfo,
undefined,
),
); );
}); });
const payOpId = RetryTags.forPay(purchase); const payOpId = RetryTags.forPay(purchase);
const payRetryRecord = await tx.operationRetries.get(payOpId); const payRetryRecord = await tx.operationRetries.get(payOpId);
transactions.push( transactions.push(
await buildTransactionForPurchase(purchase, refunds, payRetryRecord), await buildTransactionForPurchase(
purchase,
contractData,
refunds,
payRetryRecord,
),
); );
}); });