commit 062357e318d578aa024595cd5bd384fbfca520fa
parent 973a12881b05086b7a4f68ea903cebdf11ad5a0c
Author: Florian Dold <florian@dold.me>
Date: Tue, 19 Aug 2025 19:16:50 +0200
wallet-core: prepare for contracts v1 feature flag, clean up
Diffstat:
9 files changed, 390 insertions(+), 375 deletions(-)
diff --git a/packages/taler-util/src/contract-terms.ts b/packages/taler-util/src/contract-terms.ts
@@ -31,6 +31,7 @@ import {
} from "./types-taler-common.js";
import {
MerchantContractTerms,
+ MerchantContractTermsV0,
MerchantContractTokenKind,
MerchantContractVersion,
} from "./types-taler-merchant.js";
@@ -318,4 +319,59 @@ export namespace ContractTermsUtil {
maxFee,
};
}
+
+ /**
+ * Try to downgrade contract terms in the v1 format to the v0 format.
+ * Returns undefined if downgrading is not possible. This can happen
+ * when the contract only offers token payments.
+ */
+ export function downgradeContractTerms(terms: MerchantContractTerms): MerchantContractTermsV0 | undefined {
+ if (terms.version == MerchantContractVersion.V0) {
+ return terms;
+ }
+ if (terms.version !== MerchantContractVersion.V1) {
+ return undefined;
+ }
+ // Select the first choice that doesn't have
+ // and non-currency inputs.
+ let firstGood = -1;
+ for (let i = 0; i < terms.choices.length; i++) {
+ if (terms.choices[i].inputs.length == 0) {
+ firstGood = i;
+ break;
+ }
+ }
+ if (firstGood < 0) {
+ return undefined;
+ }
+ return {
+ amount: terms.choices[firstGood].amount,
+ exchanges: terms.exchanges,
+ h_wire: terms.h_wire,
+ max_fee: terms.choices[firstGood].max_fee,
+ merchant: terms.merchant,
+ merchant_base_url: terms.merchant_base_url,
+ merchant_pub: terms.merchant_pub,
+ nonce: terms.nonce,
+ order_id: terms.order_id,
+ pay_deadline: terms.pay_deadline,
+ refund_deadline: terms.refund_deadline,
+ summary: terms.summary,
+ timestamp: terms.timestamp,
+ wire_method: terms.wire_method,
+ wire_transfer_deadline: terms.wire_transfer_deadline,
+ auto_refund: terms.auto_refund,
+ delivery_date: terms.delivery_date,
+ delivery_location: terms.delivery_location,
+ extra: terms.extra,
+ fulfillment_message: terms.fulfillment_message,
+ fulfillment_message_i18n: terms.fulfillment_message_i18n,
+ fulfillment_url: terms.fulfillment_url,
+ minimum_age: terms.minimum_age,
+ products: terms.products,
+ public_reorder_url: terms.public_reorder_url,
+ summary_i18n: terms.summary_i18n,
+ version: MerchantContractVersion.V0,
+ }
+ }
}
diff --git a/packages/taler-util/src/errors.ts b/packages/taler-util/src/errors.ts
@@ -58,7 +58,7 @@ export interface DetailsMap {
};
[TalerErrorCode.WALLET_ORDER_ALREADY_PAID]: {
orderId: string;
- fulfillmentUrl: string;
+ fulfillmentUrl: string | undefined;
};
[TalerErrorCode.WALLET_CONTRACT_TERMS_MALFORMED]: empty;
[TalerErrorCode.WALLET_CONTRACT_TERMS_SIGNATURE_INVALID]: {
diff --git a/packages/taler-util/src/types-taler-wallet.ts b/packages/taler-util/src/types-taler-wallet.ts
@@ -52,6 +52,7 @@ import {
CurrencySpecification,
DurationUnitSpec,
EddsaPrivateKeyString,
+ HashCode,
TalerMerchantApi,
TemplateParams,
WithdrawalOperationStatusFlag,
@@ -445,6 +446,16 @@ export interface WalletRunConfig {
*/
features: {
allowHttp: boolean;
+
+ /**
+ * If set to true, enable V1 contracts. Otherwise, emulate v0 contracts
+ * to wallet-core clients.
+ *
+ * Will become enabled by default in the future.
+ *
+ * Added 2025-08-19.
+ */
+ enableV1Contracts: boolean;
};
/**
@@ -1024,6 +1035,9 @@ export type PreparePayResult =
/**
* Payment is possible.
+ *
+ * This response is only returned for v0 contracts
+ * or when v1 are not enabled yet.
*/
export interface PreparePayResultPaymentPossible {
status: PreparePayResultType.PaymentPossible;
@@ -1078,7 +1092,7 @@ export interface PreparePayResultAlreadyConfirmed {
transactionId: TransactionIdStr;
- contractTerms: MerchantContractTermsV0;
+ contractTerms: MerchantContractTerms;
paid: boolean;
@@ -2424,7 +2438,7 @@ export type GetChoicesForPaymentResult = {
* Data extracted from the contract terms that
* is relevant for payment processing in the wallet.
*/
- contractData: WalletContractData;
+ contractTerms: MerchantContractTerms;
};
export interface SharePaymentRequest {
@@ -3611,22 +3625,11 @@ export interface AllowedExchangeInfo {
* Data extracted from the contract terms that is relevant for payment
* processing in the wallet.
*/
-export type WalletContractData = MerchantContractTerms & {
- /**
- * Fulfillment URL, or the empty string if the order has no fulfillment URL.
- *
- * Stored as a non-nullable string as we use this field for IndexedDB indexing.
- */
- fulfillmentUrl: string;
- contractTermsHash: string;
- merchantSig: string;
-
- /**
- * Amounts are undefined for unfinished v1 orders.
- */
- amountRaw?: AmountString;
- amountEffective?: AmountString;
-};
+export interface DownloadedContractData {
+ contractTermsRaw: any;
+ contractTerms: MerchantContractTerms;
+ contractTermsHash: HashCode;
+}
export type PayWalletData = {
choice_index?: number;
diff --git a/packages/taler-wallet-core/src/deposits.ts b/packages/taler-wallet-core/src/deposits.ts
@@ -42,6 +42,7 @@ import {
HttpStatusCode,
KycAuthTransferInfo,
Logger,
+ MerchantContractTerms,
MerchantContractTermsV0,
MerchantContractVersion,
NotificationType,
@@ -62,7 +63,7 @@ import {
TransactionState,
TransactionType,
URL,
- WalletContractData,
+ DownloadedContractData,
WalletNotification,
assertUnreachable,
canonicalJson,
@@ -70,6 +71,7 @@ import {
checkLogicInvariant,
codecForBatchDepositSuccess,
codecForLegitimizationNeededResponse,
+ codecForMerchantContractTerms,
codecForTackTransactionAccepted,
codecForTackTransactionWired,
encodeCrock,
@@ -131,7 +133,6 @@ import {
runKycCheckAlgo,
} from "./kyc.js";
import {
- extractContractData,
generateDepositPermissions,
getTotalPaymentCost,
} from "./pay-merchant.js";
@@ -1548,7 +1549,7 @@ async function processDepositGroupTrack(
async function doCoinSelection(
ctx: DepositTransactionContext,
depositGroup: DepositGroupRecord,
- contractData: WalletContractData,
+ contractData: MerchantContractTerms,
): Promise<TaskRunResult> {
const wex = ctx.wex;
const depositGroupId = ctx.depositGroupId;
@@ -1643,7 +1644,7 @@ async function doCoinSelection(
interface SubmitBatchArgs {
exchangeBaseUrl: string;
- contractTerms: MerchantContractTermsV0;
+ contractTerms: MerchantContractTerms;
depositPermissions: CoinDepositPermission[];
depositGroup: DepositGroupRecord;
merchantSigResp: SignContractTermsHashResponse;
@@ -1779,12 +1780,8 @@ async function processDepositGroupPendingDeposit(
if (!contractTermsRec) {
throw Error("contract terms for deposit not found in database");
}
- const contractTerms: MerchantContractTermsV0 =
- contractTermsRec.contractTermsRaw;
- const contractData = extractContractData(
+ const contractTerms = codecForMerchantContractTerms().decode(
contractTermsRec.contractTermsRaw,
- depositGroup.contractTermsHash,
- "",
);
const ctx = new DepositTransactionContext(wex, depositGroupId);
@@ -1794,7 +1791,7 @@ async function processDepositGroupPendingDeposit(
if (!depositGroup.payCoinSelection) {
logger.info("missing coin selection for deposit group, selecting now");
- return await doCoinSelection(ctx, depositGroup, contractData);
+ return await doCoinSelection(ctx, depositGroup, contractTerms);
}
await wex.db.runReadWriteTx({ storeNames: ["depositGroups"] }, async (tx) => {
@@ -1813,7 +1810,8 @@ async function processDepositGroupPendingDeposit(
const depositPermissions = await generateDepositPermissions(
wex,
depositGroup.payCoinSelection,
- contractData,
+ contractTerms,
+ contractTermsRec.h,
);
// Exchanges involved in the deposit
@@ -2268,17 +2266,17 @@ export async function createDepositGroup(
str: canonicalJson(contractTerms),
});
- const contractData = extractContractData(
- contractTerms,
+ const contractData: DownloadedContractData = {
+ contractTerms: contractTerms,
+ contractTermsRaw: contractTerms,
contractTermsHash,
- "",
- );
+ }
if (
- contractData.version !== undefined &&
- contractData.version !== MerchantContractVersion.V0
+ contractData.contractTerms.version !== undefined &&
+ contractData.contractTerms.version !== MerchantContractVersion.V0
) {
- throw Error(`unsupported contract version ${contractData.version}`);
+ throw Error(`unsupported contract version ${contractData.contractTerms.version}`);
}
const totalDepositCost = await getTotalPaymentCost(wex, currency, coins);
@@ -2322,7 +2320,7 @@ export async function createDepositGroup(
contractTermsHash,
depositGroupId,
currency: Amounts.currencyOf(totalDepositCost),
- amount: contractData.amount,
+ amount: contractData.contractTerms.amount,
noncePriv: noncePair.priv,
noncePub: noncePair.pub,
timestampCreated: timestampPreciseToDb(
diff --git a/packages/taler-wallet-core/src/pay-merchant.ts b/packages/taler-wallet-core/src/pay-merchant.ts
@@ -51,11 +51,13 @@ import {
ConfirmPayResultType,
ContractTermsUtil,
DenomKeyType,
+ DownloadedContractData,
Duration,
encodeCrock,
ForcedCoinSel,
GetChoicesForPaymentResult,
getRandomBytes,
+ HashCodeString,
hashPayWalletData,
HttpStatusCode,
j2s,
@@ -110,7 +112,6 @@ import {
TransactionState,
TransactionType,
URL,
- WalletContractData,
WalletNotification,
} from "@gnu-taler/taler-util";
import {
@@ -253,7 +254,7 @@ export class PayMerchantTransactionContext implements TransactionContext {
tx,
purchaseRec,
);
- const contractData = download.contractData;
+ const contractData = download.contractTerms;
const payOpId = TaskIdentifiers.forPay(purchaseRec);
const payRetryRec = await tx.operationRetries.get(payOpId);
@@ -289,11 +290,11 @@ export class PayMerchantTransactionContext implements TransactionContext {
orderId: contractData.order_id,
summary: contractData.summary,
summary_i18n: contractData.summary_i18n,
- contractTermsHash: contractData.contractTermsHash,
+ contractTermsHash: download.contractTermsHash,
};
- if (contractData.fulfillmentUrl !== "") {
- info.fulfillmentUrl = contractData.fulfillmentUrl;
+ if (contractData.fulfillment_url !== "") {
+ info.fulfillmentUrl = contractData.fulfillment_url;
}
const refunds: RefundInfoShort[] = refundsInfo.map((r) => ({
@@ -540,7 +541,7 @@ export class PayMerchantTransactionContext implements TransactionContext {
async abortTransaction(reason?: TalerErrorDetail): Promise<void> {
const { wex, proposalId, transactionId } = this;
- const transitionInfo = await wex.db.runReadWriteTx(
+ await wex.db.runReadWriteTx(
{
storeNames: [
"coinAvailability",
@@ -624,7 +625,7 @@ export class PayMerchantTransactionContext implements TransactionContext {
async resumeTransaction(): Promise<void> {
const { wex, proposalId, transactionId } = this;
- const transitionInfo = await wex.db.runReadWriteTx(
+ await wex.db.runReadWriteTx(
{ storeNames: ["purchases", "transactionsMeta"] },
async (tx) => {
const purchase = await tx.purchases.get(proposalId);
@@ -651,7 +652,7 @@ export class PayMerchantTransactionContext implements TransactionContext {
async failTransaction(reason?: TalerErrorDetail): Promise<void> {
const { wex, proposalId, transactionId } = this;
- const transitionInfo = await wex.db.runReadWriteTx(
+ await wex.db.runReadWriteTx(
{
storeNames: [
"purchases",
@@ -746,9 +747,9 @@ export class RefundTransactionContext implements TransactionContext {
if (maybeContractData) {
paymentInfo = {
- merchant: maybeContractData.merchant,
- summary: maybeContractData.summary,
- summary_i18n: maybeContractData.summary_i18n,
+ merchant: maybeContractData.contractTerms.merchant,
+ summary: maybeContractData.contractTerms.summary,
+ summary_i18n: maybeContractData.contractTerms.summary_i18n,
};
}
const purchaseRecord = await tx.purchases.get(refundRecord.proposalId);
@@ -782,7 +783,7 @@ export class RefundTransactionContext implements TransactionContext {
}
async deleteTransaction(): Promise<void> {
- const { wex, refundGroupId, transactionId } = this;
+ const { wex } = this;
const res = await wex.db.runReadWriteTx(
{
@@ -846,8 +847,8 @@ export class RefundTransactionContext implements TransactionContext {
async function lookupMaybeContractData(
tx: WalletDbReadOnlyTransaction<["purchases", "contractTerms"]>,
proposalId: string,
-): Promise<WalletContractData | undefined> {
- let contractData: WalletContractData | undefined = undefined;
+): Promise<DownloadedContractData | undefined> {
+ let contractData: DownloadedContractData | undefined = undefined;
const purchaseTx = await tx.purchases.get(proposalId);
if (purchaseTx && purchaseTx.download) {
const download = purchaseTx.download;
@@ -857,11 +858,13 @@ async function lookupMaybeContractData(
if (!contractTermsRecord) {
return;
}
- contractData = extractContractData(
- contractTermsRecord?.contractTermsRaw,
- download.contractTermsHash,
- download.contractTermsMerchantSig,
- );
+ contractData = {
+ contractTerms: contractTermsRecord.contractTermsRaw,
+ contractTermsHash: contractTermsRecord.h,
+ contractTermsRaw: codecForMerchantContractTerms().decode(
+ contractTermsRecord.contractTermsRaw,
+ ),
+ };
}
return contractData;
@@ -957,10 +960,7 @@ export async function expectProposalDownloadByIdInTx(
wex: WalletExecutionContext,
tx: WalletDbReadOnlyTransaction<["contractTerms", "purchases"]>,
proposalId: string,
-): Promise<{
- contractData: WalletContractData;
- contractTermsRaw: any;
-}> {
+): Promise<DownloadedContractData> {
const rec = await tx.purchases.get(proposalId);
if (!rec) {
throw Error("purchase record not found");
@@ -972,27 +972,25 @@ export async function expectProposalDownloadInTx(
wex: WalletExecutionContext,
tx: WalletDbReadOnlyTransaction<["contractTerms"]>,
p: PurchaseRecord,
-): Promise<{
- contractData: WalletContractData;
- contractTermsRaw: any;
-}> {
+): Promise<DownloadedContractData> {
if (!p.download) {
throw Error("expected proposal to be downloaded");
}
const download = p.download;
- const contractTerms = await tx.contractTerms.get(download.contractTermsHash);
- if (!contractTerms) {
+ const contractTermsRec = await tx.contractTerms.get(
+ download.contractTermsHash,
+ );
+ if (!contractTermsRec) {
throw Error("contract terms not found");
}
return {
- contractData: extractContractData(
- contractTerms.contractTermsRaw,
- download.contractTermsHash,
- download.contractTermsMerchantSig,
+ contractTerms: codecForMerchantContractTerms().decode(
+ contractTermsRec.contractTermsRaw,
),
- contractTermsRaw: contractTerms.contractTermsRaw,
+ contractTermsRaw: contractTermsRec.contractTermsRaw,
+ contractTermsHash: contractTermsRec.h,
};
}
@@ -1002,10 +1000,7 @@ export async function expectProposalDownloadInTx(
async function expectProposalDownload(
wex: WalletExecutionContext,
p: PurchaseRecord,
-): Promise<{
- contractData: WalletContractData;
- contractTermsRaw: any;
-}> {
+): Promise<DownloadedContractData> {
return await wex.db.runReadOnlyTx(
{ storeNames: ["contractTerms"] },
async (tx) => {
@@ -1014,19 +1009,6 @@ async function expectProposalDownload(
);
}
-export function extractContractData(
- parsedContractTerms: MerchantContractTerms,
- contractTermsHash: string,
- merchantSig: string,
-): WalletContractData {
- return {
- fulfillmentUrl: parsedContractTerms.fulfillment_url ?? "",
- contractTermsHash,
- merchantSig,
- ...parsedContractTerms,
- };
-}
-
async function processDownloadProposal(
wex: WalletExecutionContext,
proposalId: string,
@@ -1214,11 +1196,13 @@ async function processDownloadProposal(
);
}
- const contractData = extractContractData(
- parsedContractTerms,
- contractTermsHash,
- proposalResp.sig,
- );
+ const contractData: DownloadedContractData = {
+ contractTerms: codecForMerchantContractTerms().decode(
+ proposalResp.contract_terms,
+ ),
+ contractTermsHash: contractTermsHash,
+ contractTermsRaw: proposalResp.contract_terms,
+ };
logger.trace(`extracted contract data: ${j2s(contractData)}`);
@@ -1240,19 +1224,23 @@ async function processDownloadProposal(
// v1: currency is resolved after choice selection
let currency: string = "UNKNOWN";
if (
- contractData.version === undefined ||
- contractData.version === MerchantContractVersion.V0
+ contractData.contractTerms.version === undefined ||
+ contractData.contractTerms.version === MerchantContractVersion.V0
+ ) {
+ currency = Amounts.currencyOf(contractData.contractTerms.amount);
+ } else if (
+ contractData.contractTerms.version === MerchantContractVersion.V1
) {
- currency = Amounts.currencyOf(contractData.amount);
- } else if (contractData.version === MerchantContractVersion.V1) {
// if there is only one choice, or all choices have the same currency
- if (contractData.choices.length === 1) {
- currency = Amounts.currencyOf(contractData.choices[0].amount);
- } else if (contractData.choices.length > 1) {
+ if (contractData.contractTerms.choices.length === 1) {
+ currency = Amounts.currencyOf(
+ contractData.contractTerms.choices[0].amount,
+ );
+ } else if (contractData.contractTerms.choices.length > 1) {
const firstCurrency = Amounts.currencyOf(
- contractData.choices[0].amount,
+ contractData.contractTerms.choices[0].amount,
);
- const allSame = contractData.choices.every(
+ const allSame = contractData.contractTerms.choices.every(
(c) => Amounts.currencyOf(c.amount) === firstCurrency,
);
if (allSame) {
@@ -1263,9 +1251,9 @@ async function processDownloadProposal(
p.download = {
contractTermsHash,
- contractTermsMerchantSig: contractData.merchantSig,
+ contractTermsMerchantSig: proposalResp.sig,
currency,
- fulfillmentUrl: contractData.fulfillmentUrl,
+ fulfillmentUrl: contractData.contractTerms.fulfillment_url,
};
await tx.contractTerms.put({
h: contractTermsHash,
@@ -1456,11 +1444,7 @@ async function createOrReusePurchase(
);
if (oldProposal.shared || oldProposal.createdFromShared) {
const download = await expectProposalDownload(wex, oldProposal);
- const paid = await checkIfOrderIsAlreadyPaid(
- wex,
- download.contractData,
- false,
- );
+ const paid = await checkIfOrderIsAlreadyPaid(wex, download, false);
logger.info(`old proposal paid: ${paid}`);
// if this transaction was shared and the order is paid then it
// means that another wallet already paid the proposal
@@ -1597,12 +1581,10 @@ async function storeFirstPaySuccess(
!!contractTermsRecord,
`no contract terms found for purchase ${purchase.orderId}`,
);
- const contractData = extractContractData(
+ const contractTerms = codecForMerchantContractTerms().decode(
contractTermsRecord.contractTermsRaw,
- dl.contractTermsHash,
- dl.contractTermsMerchantSig,
);
- const protoAr = contractData.auto_refund;
+ const protoAr = contractTerms.auto_refund;
if (protoAr) {
const ar = Duration.fromTalerProtocolDuration(protoAr);
logger.info("auto_refund present");
@@ -1663,14 +1645,14 @@ async function reselectCoinsTx(
return;
}
- const { contractData } = await expectProposalDownloadByIdInTx(
+ const contractData = await expectProposalDownloadByIdInTx(
ctx.wex,
tx,
ctx.proposalId,
);
const { available, amountRaw, maxFee } = ContractTermsUtil.extractAmounts(
- contractData,
+ contractData.contractTerms,
p.choiceIndex,
);
if (!available) {
@@ -1696,16 +1678,16 @@ async function reselectCoinsTx(
const res = await selectPayCoinsInTx(ctx.wex, tx, {
restrictExchanges: {
auditors: [],
- exchanges: contractData.exchanges.map((ex) => ({
+ exchanges: contractData.contractTerms.exchanges.map((ex) => ({
exchangeBaseUrl: ex.url,
exchangePub: ex.master_pub,
})),
},
- restrictWireMethod: contractData.wire_method,
+ restrictWireMethod: contractData.contractTerms.wire_method,
contractTermsAmount: Amounts.parseOrThrow(amountRaw),
depositFeeLimit: Amounts.parseOrThrow(maxFee),
prevPayCoins,
- requiredMinimumAge: contractData.minimum_age,
+ requiredMinimumAge: contractData.contractTerms.minimum_age,
});
switch (res.type) {
@@ -1727,13 +1709,13 @@ async function reselectCoinsTx(
if (p.choiceIndex === undefined) throw Error("assertion failed");
- if (contractData.version !== MerchantContractVersion.V1)
+ if (contractData.contractTerms.version !== MerchantContractVersion.V1)
throw Error("assertion failed");
const res = await selectPayTokensInTx(tx, {
proposalId: p.proposalId,
choiceIndex: p.choiceIndex,
- contractTerms: contractData,
+ contractTerms: contractData.contractTerms,
});
switch (res.type) {
@@ -1903,13 +1885,8 @@ async function checkPaymentByProposalId(
}
}
}
- const d = await expectProposalDownload(wex, proposal);
- const contractData = d.contractData;
-
- const merchantSig = d.contractData.merchantSig;
- if (!merchantSig) {
- throw Error("BUG: proposal is in invalid state");
- }
+ const contractDl = await expectProposalDownload(wex, proposal);
+ const contractTerms = contractDl.contractTerms;
proposalId = proposal.proposalId;
@@ -1938,36 +1915,36 @@ async function checkPaymentByProposalId(
purchase.purchaseStatus === PurchaseStatus.DialogProposed ||
purchase.purchaseStatus === PurchaseStatus.DialogShared
) {
- if (contractData.version === MerchantContractVersion.V1) {
+ if (contractTerms.version === MerchantContractVersion.V1) {
return {
status: PreparePayResultType.ChoiceSelection,
transactionId,
- contractTerms: d.contractTermsRaw,
- contractTermsHash: contractData.contractTermsHash,
+ contractTerms: contractTerms,
+ contractTermsHash: contractDl.contractTermsHash,
talerUri,
};
}
- const instructedAmount = Amounts.parseOrThrow(contractData.amount);
+ const instructedAmount = Amounts.parseOrThrow(contractTerms.amount);
// If not already paid, check if we could pay for it.
const res = await selectPayCoins(wex, {
restrictExchanges: {
auditors: [],
- exchanges: contractData.exchanges.map((ex) => ({
+ exchanges: contractTerms.exchanges.map((ex) => ({
exchangeBaseUrl: ex.url,
exchangePub: ex.master_pub,
})),
},
contractTermsAmount: instructedAmount,
- depositFeeLimit: Amounts.parseOrThrow(contractData.max_fee),
+ depositFeeLimit: Amounts.parseOrThrow(contractTerms.max_fee),
prevPayCoins: [],
- requiredMinimumAge: contractData.minimum_age,
- restrictWireMethod: contractData.wire_method,
+ requiredMinimumAge: contractTerms.minimum_age,
+ restrictWireMethod: contractTerms.wire_method,
});
let coins: SelectedProspectiveCoin[] | undefined = undefined;
- const allowedExchangeUrls = contractData.exchanges.map((x) => x.url);
+ const allowedExchangeUrls = contractTerms.exchanges.map((x) => x.url);
switch (res.type) {
case "failure": {
@@ -1982,9 +1959,9 @@ async function checkPaymentByProposalId(
});
return {
status: PreparePayResultType.InsufficientBalance,
- contractTerms: d.contractTermsRaw,
+ contractTerms,
transactionId,
- amountRaw: Amounts.stringify(contractData.amount),
+ amountRaw: Amounts.stringify(contractTerms.amount),
scopes,
talerUri,
balanceDetails: res.insufficientBalanceDetails,
@@ -2000,7 +1977,7 @@ async function checkPaymentByProposalId(
assertUnreachable(res);
}
- const currency = Amounts.currencyOf(contractData.amount);
+ const currency = Amounts.currencyOf(contractTerms.amount);
const totalCost = await getTotalPaymentCost(wex, currency, coins);
logger.trace("costInfo", totalCost);
logger.trace("coinsForPayment", res);
@@ -2013,18 +1990,18 @@ async function checkPaymentByProposalId(
return {
status: PreparePayResultType.PaymentPossible,
- contractTerms: d.contractTermsRaw,
+ contractTerms,
transactionId,
amountEffective: Amounts.stringify(totalCost),
amountRaw: Amounts.stringify(instructedAmount),
scopes,
- contractTermsHash: d.contractData.contractTermsHash,
+ contractTermsHash: contractDl.contractTermsHash,
talerUri,
};
}
const scopes = await wex.db.runAllStoresReadOnlyTx({}, async (tx) => {
- let exchangeUrls = contractData.exchanges.map((x) => x.url);
+ let exchangeUrls = contractTerms.exchanges.map((x) => x.url);
return await getScopeForAllExchanges(tx, exchangeUrls);
});
@@ -2049,10 +2026,8 @@ async function checkPaymentByProposalId(
await waitPaymentResult(wex, proposalId, sessionId);
const download = await expectProposalDownload(wex, purchase);
- const contractData = download.contractData;
-
const { available, amountRaw } = ContractTermsUtil.extractAmounts(
- contractData,
+ download.contractTerms,
purchase.choiceIndex,
);
if (!available) {
@@ -2061,8 +2036,8 @@ async function checkPaymentByProposalId(
return {
status: PreparePayResultType.AlreadyConfirmed,
- contractTerms: download.contractTermsRaw,
- contractTermsHash: download.contractData.contractTermsHash,
+ contractTerms,
+ contractTermsHash: download.contractTermsHash,
paid: true,
amountRaw: Amounts.stringify(amountRaw),
amountEffective: purchase.payInfo
@@ -2076,7 +2051,7 @@ async function checkPaymentByProposalId(
const download = await expectProposalDownload(wex, purchase);
const { available, amountRaw } = ContractTermsUtil.extractAmounts(
- contractData,
+ contractTerms,
purchase.choiceIndex,
);
if (!available) {
@@ -2085,8 +2060,8 @@ async function checkPaymentByProposalId(
return {
status: PreparePayResultType.AlreadyConfirmed,
- contractTerms: download.contractTermsRaw,
- contractTermsHash: download.contractData.contractTermsHash,
+ contractTerms,
+ contractTermsHash: download.contractTermsHash,
paid: purchase.purchaseStatus === PurchaseStatus.FailedPaidByOther,
amountRaw: Amounts.stringify(amountRaw),
amountEffective: purchase.payInfo
@@ -2101,7 +2076,7 @@ async function checkPaymentByProposalId(
const download = await expectProposalDownload(wex, purchase);
const { available, amountRaw } = ContractTermsUtil.extractAmounts(
- contractData,
+ contractTerms,
purchase.choiceIndex,
);
if (!available) {
@@ -2110,14 +2085,14 @@ async function checkPaymentByProposalId(
return {
status: PreparePayResultType.AlreadyConfirmed,
- contractTerms: download.contractTermsRaw,
- contractTermsHash: download.contractData.contractTermsHash,
+ contractTerms: contractTerms,
+ contractTermsHash: download.contractTermsHash,
paid,
amountRaw: Amounts.stringify(amountRaw),
amountEffective: purchase.payInfo
? Amounts.stringify(purchase.payInfo.totalPayCost)
: undefined,
- ...(paid ? { nextUrl: contractData.order_id } : {}),
+ ...(paid ? { nextUrl: contractTerms.order_id } : {}),
scopes,
transactionId,
talerUri,
@@ -2137,7 +2112,7 @@ function isPurchasePaid(purchase: PurchaseRecord): boolean {
export async function getContractTermsDetails(
wex: WalletExecutionContext,
proposalId: string,
-): Promise<WalletContractData> {
+): Promise<DownloadedContractData> {
const proposal = await wex.db.runReadOnlyTx(
{ storeNames: ["purchases"] },
async (tx) => {
@@ -2149,9 +2124,7 @@ export async function getContractTermsDetails(
throw Error(`proposal with id ${proposalId} not found`);
}
- const d = await expectProposalDownload(wex, proposal);
-
- return d.contractData;
+ return await expectProposalDownload(wex, proposal);
}
/**
@@ -2366,7 +2339,8 @@ export async function preparePayForTemplate(
export async function generateDepositPermissions(
wex: WalletExecutionContext,
payCoinSel: DbCoinSelection,
- contractData: WalletContractData,
+ contractData: MerchantContractTerms,
+ contractTermsHash: HashCodeString,
walletData?: PayWalletData,
): Promise<CoinDepositPermission[]> {
const depositPermissions: CoinDepositPermission[] = [];
@@ -2403,7 +2377,7 @@ export async function generateDepositPermissions(
const dp = await wex.cryptoApi.signDepositPermission({
coinPriv: coin.coinPriv,
coinPub: coin.coinPub,
- contractTermsHash: contractData.contractTermsHash,
+ contractTermsHash,
denomPubHash: coin.denomPubHash,
denomKeyType: denom.denomPub.cipher,
denomSig: coin.denomSig,
@@ -2530,7 +2504,7 @@ export async function getChoicesForPayment(
const choices: ChoiceSelectionDetail[] = [];
await wex.db.runAllStoresReadOnlyTx({}, async (tx) => {
const tokenSels: SelectPayTokensResult[] = [];
- const contractTerms: MerchantContractTerms = d.contractData;
+ const contractTerms: MerchantContractTerms = d.contractTerms;
switch (contractTerms.version) {
case undefined:
case MerchantContractVersion.V0:
@@ -2652,8 +2626,8 @@ export async function getChoicesForPayment(
return {
choices,
- contractData: d.contractData,
- ...(await calculateDefaultChoice(wex, choices, d.contractData)),
+ contractTerms: d.contractTerms,
+ ...(await calculateDefaultChoice(wex, choices, d.contractTerms)),
};
}
@@ -2828,9 +2802,9 @@ export async function confirmPay(
logger.trace("confirmPay: purchase record does not exist yet");
- const contractData = d.contractData;
+ const contractTerms = d.contractTerms;
const { available, amountRaw, maxFee } = ContractTermsUtil.extractAmounts(
- contractData,
+ contractTerms,
choiceIndex,
);
if (!available) {
@@ -2850,155 +2824,147 @@ export async function confirmPay(
`recording payment on ${proposal.orderId} with session ID ${sessionId}`,
);
- const transitionInfo = await wex.db.runAllStoresReadWriteTx(
- {},
- async (tx) => {
- const p = await tx.purchases.get(proposal.proposalId);
- if (!p) {
- return;
- }
+ await wex.db.runAllStoresReadWriteTx({}, async (tx) => {
+ const p = await tx.purchases.get(proposal.proposalId);
+ if (!p) {
+ return;
+ }
- let selectTokensResult: SelectPayTokensResult | undefined;
+ let selectTokensResult: SelectPayTokensResult | undefined;
- if (contractData.version === MerchantContractVersion.V1) {
- selectTokensResult = await selectPayTokensInTx(tx, {
- proposalId,
- choiceIndex: choiceIndex!,
- contractTerms: contractData,
- });
+ if (contractTerms.version === MerchantContractVersion.V1) {
+ selectTokensResult = await selectPayTokensInTx(tx, {
+ proposalId,
+ choiceIndex: choiceIndex!,
+ contractTerms,
+ });
- switch (selectTokensResult.type) {
- case "failure": {
- logger.warn("not confirming payment, insufficient tokens");
- throw Error("insufficient tokens");
- }
+ switch (selectTokensResult.type) {
+ case "failure": {
+ logger.warn("not confirming payment, insufficient tokens");
+ throw Error("insufficient tokens");
}
-
- logger.trace("token selection result", selectTokensResult);
}
- const selectCoinsResult = await selectPayCoinsInTx(wex, tx, {
- restrictExchanges: {
- auditors: [],
- exchanges: contractData.exchanges.map((ex) => ({
- exchangeBaseUrl: ex.url,
- exchangePub: ex.master_pub,
- })),
- },
- restrictWireMethod: contractData.wire_method,
- contractTermsAmount: Amounts.parseOrThrow(amountRaw),
- depositFeeLimit: Amounts.parseOrThrow(maxFee),
- prevPayCoins: [],
- requiredMinimumAge: contractData.minimum_age,
- forcedSelection: forcedCoinSel,
- });
+ logger.trace("token selection result", selectTokensResult);
+ }
- let coins: SelectedProspectiveCoin[] | undefined = undefined;
+ const selectCoinsResult = await selectPayCoinsInTx(wex, tx, {
+ restrictExchanges: {
+ auditors: [],
+ exchanges: contractTerms.exchanges.map((ex) => ({
+ exchangeBaseUrl: ex.url,
+ exchangePub: ex.master_pub,
+ })),
+ },
+ restrictWireMethod: contractTerms.wire_method,
+ contractTermsAmount: Amounts.parseOrThrow(amountRaw),
+ depositFeeLimit: Amounts.parseOrThrow(maxFee),
+ prevPayCoins: [],
+ requiredMinimumAge: contractTerms.minimum_age,
+ forcedSelection: forcedCoinSel,
+ });
- switch (selectCoinsResult.type) {
- case "failure": {
- // Should not happen, since checkPay should be called first
- // FIXME: Actually, this should be handled gracefully,
- // and the status should be stored in the DB.
- logger.warn("not confirming payment, insufficient coins");
- throw Error("insufficient balance");
- }
- case "prospective": {
- coins = selectCoinsResult.result.prospectiveCoins;
- break;
- }
- case "success":
- coins = selectCoinsResult.coinSel.coins;
- break;
- default:
- assertUnreachable(selectCoinsResult);
+ let coins: SelectedProspectiveCoin[] | undefined = undefined;
+
+ switch (selectCoinsResult.type) {
+ case "failure": {
+ // Should not happen, since checkPay should be called first
+ // FIXME: Actually, this should be handled gracefully,
+ // and the status should be stored in the DB.
+ logger.warn("not confirming payment, insufficient coins");
+ throw Error("insufficient balance");
+ }
+ case "prospective": {
+ coins = selectCoinsResult.result.prospectiveCoins;
+ break;
}
+ case "success":
+ coins = selectCoinsResult.coinSel.coins;
+ break;
+ default:
+ assertUnreachable(selectCoinsResult);
+ }
- logger.trace("coin selection result", selectCoinsResult);
+ logger.trace("coin selection result", selectCoinsResult);
- const payCostInfo = await getTotalPaymentCostInTx(
- wex,
- tx,
- currency,
- coins,
- );
+ const payCostInfo = await getTotalPaymentCostInTx(wex, tx, currency, coins);
- p.choiceIndex = choiceIndex;
- if (
- p.download &&
- choiceIndex !== undefined &&
- contractData.version === MerchantContractVersion.V1
- ) {
- const amount = contractData.choices[choiceIndex].amount;
- p.download.currency = Amounts.currencyOf(amount);
- }
+ p.choiceIndex = choiceIndex;
+ if (
+ p.download &&
+ choiceIndex !== undefined &&
+ contractTerms.version === MerchantContractVersion.V1
+ ) {
+ const amount = contractTerms.choices[choiceIndex].amount;
+ p.download.currency = Amounts.currencyOf(amount);
+ }
- const oldTxState = computePayMerchantTransactionState(p);
- switch (p.purchaseStatus) {
- case PurchaseStatus.DialogShared:
- case PurchaseStatus.DialogProposed:
- p.payInfo = {
- totalPayCost: Amounts.stringify(payCostInfo),
+ const oldTxState = computePayMerchantTransactionState(p);
+ switch (p.purchaseStatus) {
+ case PurchaseStatus.DialogShared:
+ case PurchaseStatus.DialogProposed:
+ p.payInfo = {
+ totalPayCost: Amounts.stringify(payCostInfo),
+ };
+ if (selectTokensResult?.type === "success") {
+ const tokens = selectTokensResult.tokens;
+ p.payInfo.payTokenSelection = {
+ tokenPubs: tokens.map((t) => t.tokenUsePub),
};
- if (selectTokensResult?.type === "success") {
- const tokens = selectTokensResult.tokens;
- p.payInfo.payTokenSelection = {
- tokenPubs: tokens.map((t) => t.tokenUsePub),
- };
- }
- if (selectCoinsResult.type === "success") {
- setCoinSel(p, selectCoinsResult.coinSel);
- }
- p.lastSessionId = sessionId;
- p.timestampAccept = timestampPreciseToDb(TalerPreciseTimestamp.now());
- p.purchaseStatus = PurchaseStatus.PendingPaying;
- await tx.purchases.put(p);
- await ctx.updateTransactionMeta(tx);
- if (p.payInfo.payTokenSelection) {
- await spendTokens(tx, {
- tokenPubs: p.payInfo.payTokenSelection.tokenPubs,
- transactionId: ctx.transactionId,
- });
- }
- if (p.payInfo.payCoinSelection) {
- const sel = p.payInfo.payCoinSelection;
- await spendCoins(wex, tx, {
- transactionId: transactionId as TransactionIdStr,
- coinPubs: sel.coinPubs,
- contributions: sel.coinContributions.map((x) =>
- Amounts.parseOrThrow(x),
- ),
- refreshReason: RefreshReason.PayMerchant,
- });
- }
- break;
- case PurchaseStatus.Done:
- case PurchaseStatus.PendingPaying:
- default:
- break;
- }
- const newTxState = computePayMerchantTransactionState(p);
- applyNotifyTransition(tx.notify, transactionId, {
- oldTxState,
- newTxState,
- balanceEffect: BalanceEffect.Any,
- });
- },
- );
+ }
+ if (selectCoinsResult.type === "success") {
+ setCoinSel(p, selectCoinsResult.coinSel);
+ }
+ p.lastSessionId = sessionId;
+ p.timestampAccept = timestampPreciseToDb(TalerPreciseTimestamp.now());
+ p.purchaseStatus = PurchaseStatus.PendingPaying;
+ await tx.purchases.put(p);
+ await ctx.updateTransactionMeta(tx);
+ if (p.payInfo.payTokenSelection) {
+ await spendTokens(tx, {
+ tokenPubs: p.payInfo.payTokenSelection.tokenPubs,
+ transactionId: ctx.transactionId,
+ });
+ }
+ if (p.payInfo.payCoinSelection) {
+ const sel = p.payInfo.payCoinSelection;
+ await spendCoins(wex, tx, {
+ transactionId: transactionId as TransactionIdStr,
+ coinPubs: sel.coinPubs,
+ contributions: sel.coinContributions.map((x) =>
+ Amounts.parseOrThrow(x),
+ ),
+ refreshReason: RefreshReason.PayMerchant,
+ });
+ }
+ break;
+ case PurchaseStatus.Done:
+ case PurchaseStatus.PendingPaying:
+ default:
+ break;
+ }
+ const newTxState = computePayMerchantTransactionState(p);
+ applyNotifyTransition(tx.notify, transactionId, {
+ oldTxState,
+ newTxState,
+ balanceEffect: BalanceEffect.Any,
+ });
+ });
// TODO: pre-generate slates based on choice priority!
if (
- choiceIndex !== undefined &&
- contractData.version === MerchantContractVersion.V1
+ choiceIndex != null &&
+ contractTerms.version === MerchantContractVersion.V1
) {
- const choice = contractData.choices[choiceIndex];
+ const choice = contractTerms.choices[choiceIndex];
for (let j = 0; j < choice.outputs.length; j++) {
await generateSlate(
wex,
proposal,
- contractData,
+ contractTerms,
d.contractTermsRaw,
- choiceIndex!,
+ choiceIndex,
j,
);
}
@@ -3131,11 +3097,7 @@ async function processPurchasePay(
const download = await expectProposalDownload(wex, purchase);
if (purchase.shared) {
- const paid = await checkIfOrderIsAlreadyPaid(
- wex,
- download.contractData,
- false,
- );
+ const paid = await checkIfOrderIsAlreadyPaid(wex, download, false);
if (paid) {
await ctx.transition(async (p) => {
@@ -3147,16 +3109,15 @@ async function processPurchasePay(
type: TaskRunResultType.Error,
errorDetail: makeErrorDetail(TalerErrorCode.WALLET_ORDER_ALREADY_PAID, {
orderId: purchase.orderId,
- fulfillmentUrl: download.contractData.fulfillmentUrl,
+ fulfillmentUrl: download.contractTerms.fulfillment_url,
}),
};
}
}
- const contractData = download.contractData;
const choiceIndex = purchase.choiceIndex;
const { available, amountRaw, maxFee } = ContractTermsUtil.extractAmounts(
- contractData,
+ download.contractTerms,
choiceIndex,
);
if (!available) {
@@ -3169,16 +3130,16 @@ async function processPurchasePay(
const selectCoinsResult = await selectPayCoins(wex, {
restrictExchanges: {
auditors: [],
- exchanges: contractData.exchanges.map((ex) => ({
+ exchanges: download.contractTerms.exchanges.map((ex) => ({
exchangeBaseUrl: ex.url,
exchangePub: ex.master_pub,
})),
},
- restrictWireMethod: contractData.wire_method,
+ restrictWireMethod: download.contractTerms.wire_method,
contractTermsAmount: Amounts.parseOrThrow(amountRaw),
depositFeeLimit: Amounts.parseOrThrow(maxFee),
prevPayCoins: [],
- requiredMinimumAge: contractData.minimum_age,
+ requiredMinimumAge: download.contractTerms.minimum_age,
});
switch (selectCoinsResult.type) {
case "failure": {
@@ -3270,14 +3231,14 @@ async function processPurchasePay(
if (!purchase.merchantPaySig) {
const payUrl = new URL(
- `orders/${download.contractData.order_id}/pay`,
- download.contractData.merchant_base_url,
+ `orders/${download.contractTerms.order_id}/pay`,
+ download.contractTerms.merchant_base_url,
);
let slates: SlateRecord[] | undefined = undefined;
let wallet_data: PayWalletData | undefined = undefined;
if (
- contractData.version === MerchantContractVersion.V1 &&
+ download.contractTerms.version === MerchantContractVersion.V1 &&
purchase.choiceIndex !== undefined
) {
const index = purchase.choiceIndex;
@@ -3300,9 +3261,11 @@ async function processPurchasePay(
},
);
- if (slates.length !== contractData.choices[index].outputs.length) {
+ if (
+ slates.length !== download.contractTerms.choices[index].outputs.length
+ ) {
throw Error(
- `number of slates ${slates.length} doesn't match number of outputs ${contractData.choices[index].outputs.length}`,
+ `number of slates ${slates.length} doesn't match number of outputs ${download.contractTerms.choices[index].outputs.length}`,
);
}
}
@@ -3312,7 +3275,8 @@ async function processPurchasePay(
depositPermissions = await generateDepositPermissions(
wex,
payInfo.payCoinSelection,
- download.contractData,
+ download.contractTerms,
+ download.contractTermsHash,
wallet_data,
);
@@ -3326,7 +3290,7 @@ async function processPurchasePay(
reqBody.tokens = await generateTokenSigs(
wex,
proposalId,
- contractData.contractTermsHash,
+ download.contractTermsHash,
encodeCrock(hashPayWalletData(wallet_data)),
payInfo.payTokenSelection.tokenPubs,
);
@@ -3425,9 +3389,9 @@ async function processPurchasePay(
logger.trace("got success from pay URL", merchantResp);
- const merchantPub = download.contractData.merchant_pub;
+ const merchantPub = download.contractTerms.merchant_pub;
const { valid } = await wex.cryptoApi.isValidPaymentSignature({
- contractHash: download.contractData.contractTermsHash,
+ contractHash: download.contractTermsHash,
merchantPub,
sig: merchantResp.sig,
});
@@ -3486,12 +3450,12 @@ async function processPurchasePay(
await storeFirstPaySuccess(wex, proposalId, sessionId, merchantResp);
} else {
const payAgainUrl = new URL(
- `orders/${download.contractData.order_id}/paid`,
- download.contractData.merchant_base_url,
+ `orders/${download.contractTerms.order_id}/paid`,
+ download.contractTerms.merchant_base_url,
);
const reqBody = {
sig: purchase.merchantPaySig,
- h_contract: download.contractData.contractTermsHash,
+ h_contract: download.contractTermsHash,
session_id: sessionId ?? "",
};
logger.trace(`/paid request body: ${j2s(reqBody)}`);
@@ -4051,12 +4015,12 @@ export async function sharePayment(
async function checkIfOrderIsAlreadyPaid(
wex: WalletExecutionContext,
- contract: WalletContractData,
+ contract: DownloadedContractData,
doLongPolling: boolean,
) {
const requestUrl = new URL(
- `orders/${contract.order_id}`,
- contract.merchant_base_url,
+ `orders/${contract.contractTerms.order_id}`,
+ contract.contractTerms.merchant_base_url,
);
requestUrl.searchParams.set("h_contract", contract.contractTermsHash);
@@ -4094,11 +4058,7 @@ async function processPurchaseDialogShared(
const ctx = new PayMerchantTransactionContext(wex, proposalId);
- const paid = await checkIfOrderIsAlreadyPaid(
- wex,
- download.contractData,
- true,
- );
+ const paid = await checkIfOrderIsAlreadyPaid(wex, download, true);
if (paid) {
await ctx.transition(async (p) => {
@@ -4121,9 +4081,8 @@ async function processPurchaseAutoRefund(
logger.trace(`processing auto-refund for proposal ${proposalId}`);
const download = await expectProposalDownload(wex, purchase);
- const contractData = download.contractData;
const { available, amountRaw } = ContractTermsUtil.extractAmounts(
- contractData,
+ download.contractTerms,
purchase.choiceIndex,
);
if (!available) {
@@ -4179,13 +4138,10 @@ async function processPurchaseAutoRefund(
}
const requestUrl = new URL(
- `orders/${download.contractData.order_id}`,
- download.contractData.merchant_base_url,
- );
- requestUrl.searchParams.set(
- "h_contract",
- download.contractData.contractTermsHash,
+ `orders/${download.contractTerms.order_id}`,
+ download.contractTerms.merchant_base_url,
);
+ requestUrl.searchParams.set("h_contract", download.contractTermsHash);
requestUrl.searchParams.set("refund", Amounts.stringify(totalKnownRefund));
@@ -4225,8 +4181,8 @@ async function processPurchaseAbortingRefund(
logger.trace(`processing aborting-refund for proposal ${proposalId}`);
const requestUrl = new URL(
- `orders/${download.contractData.order_id}/abort`,
- download.contractData.merchant_base_url,
+ `orders/${download.contractTerms.order_id}/abort`,
+ download.contractTerms.merchant_base_url,
);
const abortingCoins: AbortingCoin[] = [];
@@ -4250,7 +4206,7 @@ async function processPurchaseAbortingRefund(
});
const abortReq: AbortRequest = {
- h_contract: download.contractData.contractTermsHash,
+ h_contract: download.contractTermsHash,
coins: abortingCoins,
};
@@ -4308,7 +4264,7 @@ async function processPurchaseAbortingRefund(
rtransaction_id: 0,
execution_time: AbsoluteTime.toProtocolTimestamp(
AbsoluteTime.addDuration(
- AbsoluteTime.fromProtocolTimestamp(download.contractData.timestamp),
+ AbsoluteTime.fromProtocolTimestamp(download.contractTerms.timestamp),
Duration.fromSpec({ seconds: 1 }),
),
),
@@ -4327,13 +4283,10 @@ async function processPurchaseQueryRefund(
const download = await expectProposalDownload(wex, purchase);
const requestUrl = new URL(
- `orders/${download.contractData.order_id}`,
- download.contractData.merchant_base_url,
- );
- requestUrl.searchParams.set(
- "h_contract",
- download.contractData.contractTermsHash,
+ `orders/${download.contractTerms.order_id}`,
+ download.contractTerms.merchant_base_url,
);
+ requestUrl.searchParams.set("h_contract", download.contractTermsHash);
const resp = await cancelableFetch(wex, requestUrl);
const orderStatus = await readSuccessResponseJsonOrThrow(
@@ -4377,8 +4330,8 @@ async function processPurchaseAcceptRefund(
const download = await expectProposalDownload(wex, purchase);
const requestUrl = new URL(
- `orders/${download.contractData.order_id}/refund`,
- download.contractData.merchant_base_url,
+ `orders/${download.contractTerms.order_id}/refund`,
+ download.contractTerms.merchant_base_url,
);
logger.trace(`making refund request to ${requestUrl.href}`);
@@ -4386,7 +4339,7 @@ async function processPurchaseAcceptRefund(
const request = await cancelableFetch(wex, requestUrl, {
method: "POST",
body: {
- h_contract: download.contractData.contractTermsHash,
+ h_contract: download.contractTermsHash,
},
});
@@ -4536,9 +4489,8 @@ async function storeRefunds(
const now = TalerPreciseTimestamp.now();
const download = await expectProposalDownload(wex, purchase);
- const contractData = download.contractData;
const { available, amountRaw } = ContractTermsUtil.extractAmounts(
- contractData,
+ download.contractTerms,
purchase.choiceIndex,
);
if (!available) {
diff --git a/packages/taler-wallet-core/src/wallet-api-types.ts b/packages/taler-wallet-core/src/wallet-api-types.ts
@@ -170,7 +170,7 @@ import {
UserAttentionsResponse,
ValidateIbanRequest,
ValidateIbanResponse,
- WalletContractData,
+ DownloadedContractData,
WalletCoreVersion,
WithdrawTestBalanceRequest,
WithdrawUriInfoResponse,
@@ -242,18 +242,10 @@ export enum WalletApiOperation {
DumpCoins = "dumpCoins",
SetCoinSuspended = "setCoinSuspended",
ForceRefresh = "forceRefresh",
- ExportBackup = "exportBackup",
- AddBackupProvider = "addBackupProvider",
- RemoveBackupProvider = "removeBackupProvider",
- RunBackupCycle = "runBackupCycle",
- ExportBackupRecovery = "exportBackupRecovery",
- ImportBackupRecovery = "importBackupRecovery",
- GetBackupInfo = "getBackupInfo",
CheckDeposit = "checkDeposit",
GetVersion = "getVersion",
GenerateDepositGroupTxId = "generateDepositGroupTxId",
CreateDepositGroup = "createDepositGroup",
- SetWalletDeviceId = "setWalletDeviceId",
ImportDb = "importDb",
ExportDb = "exportDb",
ExportDbToFile = "exportDbToFile",
@@ -272,10 +264,6 @@ export enum WalletApiOperation {
ApplyDevExperiment = "applyDevExperiment",
ValidateIban = "validateIban",
GetCurrencySpecification = "getCurrencySpecification",
- ListStoredBackups = "listStoredBackups",
- CreateStoredBackup = "createStoredBackup",
- DeleteStoredBackup = "deleteStoredBackup",
- RecoverStoredBackup = "recoverStoredBackup",
UpdateExchangeEntry = "updateExchangeEntry",
PrepareWithdrawExchange = "prepareWithdrawExchange",
GetExchangeResources = "getExchangeResources",
@@ -288,12 +276,33 @@ export enum WalletApiOperation {
RemoveGlobalCurrencyAuditor = "removeGlobalCurrencyAuditor",
ListAssociatedRefreshes = "listAssociatedRefreshes",
Shutdown = "shutdown",
- HintNetworkAvailability = "hintNetworkAvailability",
CanonicalizeBaseUrl = "canonicalizeBaseUrl",
GetDepositWireTypes = "getDepositWireTypes",
GetDepositWireTypesForCurrency = "getDepositWireTypesForCurrency",
GetQrCodesForPayto = "getQrCodesForPayto",
+ StartExchangeWalletKyc = "startExchangeWalletKyc",
GetBankingChoicesForPayto = "getBankingChoicesForPayto",
+
+ // Stored backups
+
+ ListStoredBackups = "listStoredBackups",
+ CreateStoredBackup = "createStoredBackup",
+ DeleteStoredBackup = "deleteStoredBackup",
+ RecoverStoredBackup = "recoverStoredBackup",
+
+ // legacy backup functionality
+
+ SetWalletDeviceId = "setWalletDeviceId",
+ ExportBackup = "exportBackup",
+ AddBackupProvider = "addBackupProvider",
+ RemoveBackupProvider = "removeBackupProvider",
+ RunBackupCycle = "runBackupCycle",
+ ExportBackupRecovery = "exportBackupRecovery",
+ ImportBackupRecovery = "importBackupRecovery",
+ GetBackupInfo = "getBackupInfo",
+
+ // Testing
+
TestingWaitTransactionsFinal = "testingWaitTransactionsFinal",
TestingWaitRefreshesFinal = "testingWaitRefreshesFinal",
TestingWaitTransactionState = "testingWaitTransactionState",
@@ -305,9 +314,12 @@ export enum WalletApiOperation {
TestingPing = "testingPing",
TestingGetReserveHistory = "testingGetReserveHistory",
TestingResetAllRetries = "testingResetAllRetries",
- StartExchangeWalletKyc = "startExchangeWalletKyc",
TestingWaitExchangeWalletKyc = "testingWaitWalletKyc",
TestingPlanMigrateExchangeBaseUrl = "testingPlanMigrateExchangeBaseUrl",
+
+ // Hints
+
+ HintNetworkAvailability = "hintNetworkAvailability",
HintApplicationResumed = "hintApplicationResumed",
/**
@@ -634,7 +646,7 @@ export type PreparePayForTemplateOp = {
export type GetContractTermsDetailsOp = {
op: WalletApiOperation.GetContractTermsDetails;
request: GetContractTermsDetailsRequest;
- response: WalletContractData;
+ response: DownloadedContractData;
};
/**
diff --git a/packages/taler-wallet-core/src/wallet.ts b/packages/taler-wallet-core/src/wallet.ts
@@ -133,7 +133,7 @@ import {
ValidateIbanRequest,
ValidateIbanResponse,
WalletBankAccountInfo,
- WalletContractData,
+ DownloadedContractData,
WalletCoreVersion,
WalletNotification,
WalletRunConfig,
@@ -1158,7 +1158,7 @@ async function handleGetExchangeTos(
async function handleGetContractTermsDetails(
wex: WalletExecutionContext,
req: GetContractTermsDetailsRequest,
-): Promise<WalletContractData> {
+): Promise<DownloadedContractData> {
const parsedTx = parseTransactionIdentifier(req.transactionId);
if (parsedTx?.tag !== TransactionType.Payment) {
throw Error("transactionId is not a payment transaction");
@@ -2574,6 +2574,7 @@ function applyRunConfigDefaults(wcp?: PartialWalletRunConfig): WalletRunConfig {
},
features: {
allowHttp: true,
+ enableV1Contracts: false,
},
testing: {
devModeActive: wcp?.testing?.devModeActive ?? false,
diff --git a/packages/taler-wallet-webextension/src/hooks/useSettings.ts b/packages/taler-wallet-webextension/src/hooks/useSettings.ts
@@ -14,22 +14,13 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-import { buildStorageKey, useLocalStorage } from "@gnu-taler/web-util/browser";
-import { Settings, defaultSettings } from "../platform/api.js";
import {
Codec,
buildCodecForObject,
codecForBoolean,
} from "@gnu-taler/taler-util";
-
-function parse_json_or_undefined<T>(str: string | undefined): T | undefined {
- if (str === undefined) return undefined;
- try {
- return JSON.parse(str);
- } catch {
- return undefined;
- }
-}
+import { buildStorageKey, useLocalStorage } from "@gnu-taler/web-util/browser";
+import { Settings, defaultSettings } from "../platform/api.js";
export const codecForSettings = (): Codec<Settings> =>
buildCodecForObject<Settings>()
@@ -46,6 +37,7 @@ export const codecForSettings = (): Codec<Settings> =>
.property("showExchangeManagement", codecForBoolean())
.property("selectTosFormat", codecForBoolean())
.property("showWalletActivity", codecForBoolean())
+ .property("walletEnableV1Contracts", codecForBoolean())
.build("Settings");
const SETTINGS_KEY = buildStorageKey("wallet-settings", codecForSettings());
diff --git a/packages/taler-wallet-webextension/src/platform/api.ts b/packages/taler-wallet-webextension/src/platform/api.ts
@@ -142,6 +142,7 @@ export const defaultSettings: Settings = {
walletAllowHttp: false,
selectTosFormat: false,
showWalletActivity: false,
+ walletEnableV1Contracts: true,
};
/**