aboutsummaryrefslogtreecommitdiff
path: root/packages/taler-wallet-core/src
diff options
context:
space:
mode:
Diffstat (limited to 'packages/taler-wallet-core/src')
-rw-r--r--packages/taler-wallet-core/src/db.ts11
-rw-r--r--packages/taler-wallet-core/src/operations/backup/export.ts4
-rw-r--r--packages/taler-wallet-core/src/operations/backup/import.ts10
-rw-r--r--packages/taler-wallet-core/src/operations/pay-merchant.ts262
-rw-r--r--packages/taler-wallet-core/src/wallet-api-types.ts10
-rw-r--r--packages/taler-wallet-core/src/wallet.ts7
6 files changed, 285 insertions, 19 deletions
diff --git a/packages/taler-wallet-core/src/db.ts b/packages/taler-wallet-core/src/db.ts
index c12d0f2f7..6a7a26f2f 100644
--- a/packages/taler-wallet-core/src/db.ts
+++ b/packages/taler-wallet-core/src/db.ts
@@ -1145,6 +1145,11 @@ export enum PurchaseStatus {
* Proposal downloaded, but the user needs to accept/reject it.
*/
DialogProposed = 30,
+ /**
+ * Proposal shared to other wallet or read from other wallet
+ * the user needs to accept/reject it.
+ */
+ DialogShared = 31,
/**
* The user has rejected the proposal.
@@ -1271,6 +1276,12 @@ export interface PurchaseRecord {
posConfirmation: string | undefined;
/**
+ * This purchase was created by sharing nonce or
+ * did the wallet made the nonce public
+ */
+ shared: boolean;
+
+ /**
* When was the purchase record created?
*/
timestamp: TalerPreciseTimestamp;
diff --git a/packages/taler-wallet-core/src/operations/backup/export.ts b/packages/taler-wallet-core/src/operations/backup/export.ts
index 23c6e787a..21ba5dc37 100644
--- a/packages/taler-wallet-core/src/operations/backup/export.ts
+++ b/packages/taler-wallet-core/src/operations/backup/export.ts
@@ -422,6 +422,9 @@ export async function exportBackup(
case PurchaseStatus.PendingPaying:
propStatus = BackupProposalStatus.Proposed;
break;
+ case PurchaseStatus.DialogShared:
+ propStatus = BackupProposalStatus.Shared;
+ break;
case PurchaseStatus.FailedClaim:
case PurchaseStatus.AbortedIncompletePayment:
propStatus = BackupProposalStatus.PermanentlyFailed;
@@ -483,6 +486,7 @@ export async function exportBackup(
repurchase_proposal_id: purch.repurchaseProposalId,
download_session_id: purch.downloadSessionId,
timestamp_proposed: purch.timestamp,
+ shared: purch.shared,
});
});
diff --git a/packages/taler-wallet-core/src/operations/backup/import.ts b/packages/taler-wallet-core/src/operations/backup/import.ts
index 7f73a14b0..b161aa8f2 100644
--- a/packages/taler-wallet-core/src/operations/backup/import.ts
+++ b/packages/taler-wallet-core/src/operations/backup/import.ts
@@ -62,7 +62,11 @@ import { InternalWalletState } from "../../internal-wallet-state.js";
import { assertUnreachable } from "../../util/assertUnreachable.js";
import { checkLogicInvariant } from "../../util/invariants.js";
import { GetReadOnlyAccess, GetReadWriteAccess } from "../../util/query.js";
-import { constructTombstone, makeCoinAvailable, TombstoneTag } from "../common.js";
+import {
+ constructTombstone,
+ makeCoinAvailable,
+ TombstoneTag,
+} from "../common.js";
import { getExchangeDetails } from "../exchanges.js";
import { extractContractData } from "../pay-merchant.js";
import { provideBackupState } from "./state.js";
@@ -576,6 +580,9 @@ export async function importBackup(
case BackupProposalStatus.Paid:
proposalStatus = PurchaseStatus.Done;
break;
+ case BackupProposalStatus.Shared:
+ proposalStatus = PurchaseStatus.DialogShared;
+ break;
case BackupProposalStatus.Proposed:
proposalStatus = PurchaseStatus.DialogProposed;
break;
@@ -702,6 +709,7 @@ export async function importBackup(
repurchaseProposalId: backupPurchase.repurchase_proposal_id,
purchaseStatus: proposalStatus,
timestamp: backupPurchase.timestamp_proposed,
+ shared: backupPurchase.shared,
});
}
}
diff --git a/packages/taler-wallet-core/src/operations/pay-merchant.ts b/packages/taler-wallet-core/src/operations/pay-merchant.ts
index c74fcedcf..d53ee1b43 100644
--- a/packages/taler-wallet-core/src/operations/pay-merchant.ts
+++ b/packages/taler-wallet-core/src/operations/pay-merchant.ts
@@ -61,7 +61,10 @@ import {
PreparePayResultType,
randomBytes,
RefreshReason,
+ SharePaymentResult,
StartRefundQueryForUriResponse,
+ stringifyPaytoUri,
+ stringifyPayUri,
stringifyTalerUri,
TalerError,
TalerErrorCode,
@@ -542,7 +545,9 @@ async function processDownloadProposal(
p.repurchaseProposalId = otherPurchase.proposalId;
await tx.purchases.put(p);
} else {
- p.purchaseStatus = PurchaseStatus.DialogProposed;
+ p.purchaseStatus = p.shared
+ ? PurchaseStatus.DialogShared
+ : PurchaseStatus.DialogProposed;
await tx.purchases.put(p);
}
const newTxState = computePayMerchantTransactionState(p);
@@ -570,15 +575,22 @@ async function createPurchase(
claimToken: string | undefined,
noncePriv: string | undefined,
): Promise<string> {
- const oldProposal = await ws.db
+ const oldProposals = await ws.db
.mktx((x) => [x.purchases])
.runReadOnly(async (tx) => {
- return tx.purchases.indexes.byUrlAndOrderId.get([
+ return tx.purchases.indexes.byUrlAndOrderId.getAll([
merchantBaseUrl,
orderId,
]);
});
+ const oldProposal = oldProposals.find((p) => {
+ return (
+ p.downloadSessionId === sessionId &&
+ (!noncePriv || p.noncePriv === noncePriv) &&
+ p.claimToken === claimToken
+ );
+ });
/* If we have already claimed this proposal with the same sessionId
* nonce and claim token, reuse it. */
if (
@@ -589,11 +601,42 @@ async function createPurchase(
) {
// FIXME: This lacks proper error handling
await processDownloadProposal(ws, oldProposal.proposalId);
+
+ if (oldProposal.purchaseStatus === PurchaseStatus.DialogShared) {
+ const download = await expectProposalDownload(ws, oldProposal);
+ const paid = await checkIfOrderIsAlreadyPaid(ws, download.contractData);
+ if (paid) {
+ //if this transaction was shared and the order is paid then it
+ //means that another wallet already paid the proposal
+ const transitionInfo = await ws.db
+ .mktx((x) => [x.purchases])
+ .runReadWrite(async (tx) => {
+ const p = await tx.purchases.get(oldProposal.proposalId);
+ if (!p) {
+ logger.warn("purchase does not exist anymore");
+ return;
+ }
+ const oldTxState = computePayMerchantTransactionState(p);
+ p.purchaseStatus = PurchaseStatus.FailedClaim;
+ const newTxState = computePayMerchantTransactionState(p);
+ await tx.purchases.put(p);
+ return { oldTxState, newTxState };
+ });
+
+ const transactionId = constructTransactionIdentifier({
+ tag: TransactionType.Payment,
+ proposalId: oldProposal.proposalId,
+ });
+ notifyTransition(ws, transactionId, transitionInfo);
+ }
+ }
return oldProposal.proposalId;
}
let noncePair: EddsaKeypair;
+ let shared = false;
if (noncePriv) {
+ shared = true;
noncePair = {
priv: noncePriv,
pub: (await ws.cryptoApi.eddsaGetPublic({ priv: noncePriv })).pub,
@@ -627,19 +670,12 @@ async function createPurchase(
timestampLastRefundStatus: undefined,
pendingRemovedCoinPubs: undefined,
posConfirmation: undefined,
+ shared: shared,
};
const transitionInfo = await ws.db
.mktx((x) => [x.purchases])
.runReadWrite(async (tx) => {
- const existingRecord = await tx.purchases.indexes.byUrlAndOrderId.get([
- merchantBaseUrl,
- orderId,
- ]);
- if (existingRecord) {
- // Created concurrently
- return undefined;
- }
await tx.purchases.put(proposalRecord);
const oldTxState: TransactionState = {
major: TransactionMajorState.None,
@@ -983,7 +1019,11 @@ export async function checkPaymentByProposalId(
return tx.purchases.get(proposalId);
});
- if (!purchase || purchase.purchaseStatus === PurchaseStatus.DialogProposed) {
+ if (
+ !purchase ||
+ purchase.purchaseStatus === PurchaseStatus.DialogProposed ||
+ purchase.purchaseStatus === PurchaseStatus.DialogShared
+ ) {
// If not already paid, check if we could pay for it.
const res = await selectPayCoinsNew(ws, {
auditors: [],
@@ -1007,7 +1047,6 @@ export async function checkPaymentByProposalId(
contractTerms: d.contractTermsRaw,
proposalId: proposal.proposalId,
transactionId,
- noncePriv: proposal.noncePriv,
amountRaw: Amounts.stringify(d.contractData.amount),
talerUri,
balanceDetails: res.insufficientBalanceDetails,
@@ -1023,7 +1062,6 @@ export async function checkPaymentByProposalId(
contractTerms: d.contractTermsRaw,
transactionId,
proposalId: proposal.proposalId,
- noncePriv: proposal.noncePriv,
amountEffective: Amounts.stringify(totalCost),
amountRaw: Amounts.stringify(res.coinSel.paymentAmount),
contractTermsHash: d.contractData.contractTermsHash,
@@ -1067,7 +1105,9 @@ export async function checkPaymentByProposalId(
contractTermsHash: download.contractData.contractTermsHash,
paid: true,
amountRaw: Amounts.stringify(download.contractData.amount),
- amountEffective: Amounts.stringify(purchase.payInfo?.totalPayCost!),
+ amountEffective: purchase.payInfo
+ ? Amounts.stringify(purchase.payInfo.totalPayCost)
+ : undefined,
transactionId,
proposalId,
talerUri,
@@ -1080,7 +1120,9 @@ export async function checkPaymentByProposalId(
contractTermsHash: download.contractData.contractTermsHash,
paid: false,
amountRaw: Amounts.stringify(download.contractData.amount),
- amountEffective: Amounts.stringify(purchase.payInfo?.totalPayCost!),
+ amountEffective: purchase.payInfo
+ ? Amounts.stringify(purchase.payInfo.totalPayCost)
+ : undefined,
transactionId,
proposalId,
talerUri,
@@ -1097,7 +1139,9 @@ export async function checkPaymentByProposalId(
contractTermsHash: download.contractData.contractTermsHash,
paid,
amountRaw: Amounts.stringify(download.contractData.amount),
- amountEffective: Amounts.stringify(purchase.payInfo?.totalPayCost!),
+ amountEffective: purchase.payInfo
+ ? Amounts.stringify(purchase.payInfo.totalPayCost)
+ : undefined,
...(paid ? { nextUrl: download.contractData.orderId } : {}),
transactionId,
proposalId,
@@ -1406,6 +1450,7 @@ export async function confirmPay(
}
const oldTxState = computePayMerchantTransactionState(p);
switch (p.purchaseStatus) {
+ case PurchaseStatus.DialogShared:
case PurchaseStatus.DialogProposed:
p.payInfo = {
payCoinSelection: coinSelection,
@@ -1480,6 +1525,8 @@ export async function processPurchase(
return processPurchaseAbortingRefund(ws, purchase);
case PurchaseStatus.PendingAcceptRefund:
return processPurchaseAcceptRefund(ws, purchase);
+ case PurchaseStatus.DialogShared:
+ return processPurchaseDialogShared(ws, purchase);
case PurchaseStatus.FailedClaim:
case PurchaseStatus.Done:
case PurchaseStatus.RepurchaseDetected:
@@ -1540,6 +1587,41 @@ export async function processPurchasePay(
checkDbInvariant(!!payInfo, "payInfo");
const download = await expectProposalDownload(ws, purchase);
+
+ if (purchase.shared) {
+ const paid = await checkIfOrderIsAlreadyPaid(ws, download.contractData);
+
+ if (paid) {
+ const transitionInfo = await ws.db
+ .mktx((x) => [x.purchases])
+ .runReadWrite(async (tx) => {
+ const p = await tx.purchases.get(purchase.proposalId);
+ if (!p) {
+ logger.warn("purchase does not exist anymore");
+ return;
+ }
+ const oldTxState = computePayMerchantTransactionState(p);
+ p.purchaseStatus = PurchaseStatus.FailedClaim;
+ const newTxState = computePayMerchantTransactionState(p);
+ await tx.purchases.put(p);
+ return { oldTxState, newTxState };
+ });
+ const transactionId = constructTransactionIdentifier({
+ tag: TransactionType.Payment,
+ proposalId,
+ });
+
+ notifyTransition(ws, transactionId, transitionInfo);
+
+ return {
+ type: TaskRunResultType.Error,
+ errorDetail: makeErrorDetail(TalerErrorCode.WALLET_ORDER_ALREADY_PAID, {
+ orderId: purchase.orderId,
+ }),
+ };
+ }
+ }
+
if (!purchase.merchantPaySig) {
const payUrl = new URL(
`orders/${download.contractData.orderId}/pay`,
@@ -1681,7 +1763,10 @@ export async function refuseProposal(
logger.trace(`proposal ${proposalId} not found, won't refuse proposal`);
return undefined;
}
- if (proposal.purchaseStatus !== PurchaseStatus.DialogProposed) {
+ if (
+ proposal.purchaseStatus !== PurchaseStatus.DialogProposed &&
+ proposal.purchaseStatus !== PurchaseStatus.DialogShared
+ ) {
return undefined;
}
const oldTxState = computePayMerchantTransactionState(proposal);
@@ -1996,6 +2081,11 @@ export function computePayMerchantTransactionState(
major: TransactionMajorState.Dialog,
minor: TransactionMinorState.MerchantOrderProposed,
};
+ case PurchaseStatus.DialogShared:
+ return {
+ major: TransactionMajorState.Dialog,
+ minor: TransactionMinorState.MerchantOrderProposed,
+ };
// Final States
case PurchaseStatus.AbortedProposalRefused:
return {
@@ -2078,6 +2168,8 @@ export function computePayMerchantTransactionActions(
// Dialog States
case PurchaseStatus.DialogProposed:
return [];
+ case PurchaseStatus.DialogShared:
+ return [];
// Final States
case PurchaseStatus.AbortedProposalRefused:
return [TransactionAction.Delete];
@@ -2096,6 +2188,140 @@ export function computePayMerchantTransactionActions(
}
}
+export async function sharePayment(
+ ws: InternalWalletState,
+ merchantBaseUrl: string,
+ orderId: string,
+): Promise<SharePaymentResult> {
+ const result = await ws.db
+ .mktx((x) => [x.purchases])
+ .runReadWrite(async (tx) => {
+ const p = await tx.purchases.indexes.byUrlAndOrderId.get([
+ merchantBaseUrl,
+ orderId,
+ ]);
+ if (!p) {
+ logger.warn("purchase does not exist anymore");
+ return undefined;
+ }
+ if (
+ p.purchaseStatus !== PurchaseStatus.DialogProposed &&
+ p.purchaseStatus !== PurchaseStatus.DialogShared
+ ) {
+ //FIXME: purchase can be shared before being paid
+ return undefined;
+ }
+ if (p.purchaseStatus === PurchaseStatus.DialogProposed) {
+ p.purchaseStatus = PurchaseStatus.DialogShared;
+ p.shared = true;
+ tx.purchases.put(p);
+ }
+
+ return {
+ nonce: p.noncePriv,
+ session: p.lastSessionId,
+ token: p.claimToken,
+ };
+ });
+
+ if (result === undefined) {
+ throw Error("This purchase can't be shared");
+ }
+ const privatePayUri = stringifyPayUri({
+ merchantBaseUrl,
+ orderId,
+ sessionId: result.session ?? "",
+ noncePriv: result.nonce,
+ claimToken: result.token,
+ });
+ return { privatePayUri };
+}
+
+async function checkIfOrderIsAlreadyPaid(
+ ws: InternalWalletState,
+ contract: WalletContractData,
+) {
+ const requestUrl = new URL(
+ `orders/${contract.orderId}`,
+ contract.merchantBaseUrl,
+ );
+ requestUrl.searchParams.set("h_contract", contract.contractTermsHash);
+
+ requestUrl.searchParams.set("timeout_ms", "1000");
+
+ const resp = await ws.http.fetch(requestUrl.href);
+ if (
+ resp.status === HttpStatusCode.Ok ||
+ resp.status === HttpStatusCode.Accepted ||
+ resp.status === HttpStatusCode.Found
+ ) {
+ return true;
+ } else if (resp.status === HttpStatusCode.PaymentRequired) {
+ return false;
+ }
+ //forbidden, not found, not acceptable
+ throw Error(`this order cant be paid: ${resp.status}`);
+}
+
+async function processPurchaseDialogShared(
+ ws: InternalWalletState,
+ purchase: PurchaseRecord,
+): Promise<TaskRunResult> {
+ const proposalId = purchase.proposalId;
+ logger.trace(`processing dialog-shared for proposal ${proposalId}`);
+
+ const taskId = constructTaskIdentifier({
+ tag: PendingTaskType.Purchase,
+ proposalId,
+ });
+
+ // FIXME: Put this logic into runLongpollAsync?
+ if (ws.activeLongpoll[taskId]) {
+ return TaskRunResult.longpoll();
+ }
+ const download = await expectProposalDownload(ws, purchase);
+
+ if (purchase.purchaseStatus !== PurchaseStatus.DialogShared) {
+ return TaskRunResult.finished();
+ }
+
+ runLongpollAsync(ws, taskId, async (ct) => {
+ const paid = await checkIfOrderIsAlreadyPaid(ws, download.contractData);
+ if (paid) {
+ const transitionInfo = await ws.db
+ .mktx((x) => [x.purchases])
+ .runReadWrite(async (tx) => {
+ const p = await tx.purchases.get(purchase.proposalId);
+ if (!p) {
+ logger.warn("purchase does not exist anymore");
+ return;
+ }
+ const oldTxState = computePayMerchantTransactionState(p);
+ p.purchaseStatus = PurchaseStatus.FailedClaim;
+ const newTxState = computePayMerchantTransactionState(p);
+ await tx.purchases.put(p);
+ return { oldTxState, newTxState };
+ });
+ const transactionId = constructTransactionIdentifier({
+ tag: TransactionType.Payment,
+ proposalId,
+ });
+
+ notifyTransition(ws, transactionId, transitionInfo);
+
+ return {
+ ready: true,
+ };
+ }
+
+ return {
+ ready: false,
+ };
+ });
+
+ return TaskRunResult.longpoll();
+}
+
async function processPurchaseAutoRefund(
ws: InternalWalletState,
purchase: PurchaseRecord,
diff --git a/packages/taler-wallet-core/src/wallet-api-types.ts b/packages/taler-wallet-core/src/wallet-api-types.ts
index cea548db6..e395237cf 100644
--- a/packages/taler-wallet-core/src/wallet-api-types.ts
+++ b/packages/taler-wallet-core/src/wallet-api-types.ts
@@ -112,6 +112,8 @@ import {
WithdrawFakebankRequest,
WithdrawTestBalanceRequest,
WithdrawUriInfoResponse,
+ SharePaymentRequest,
+ SharePaymentResult,
} from "@gnu-taler/taler-util";
import { AuditorTrustRecord, WalletContractData } from "./db.js";
import {
@@ -129,6 +131,7 @@ export enum WalletApiOperation {
WithdrawTestkudos = "withdrawTestkudos",
WithdrawTestBalance = "withdrawTestBalance",
PreparePayForUri = "preparePayForUri",
+ SharePayment = "sharePayment",
PreparePayForTemplate = "preparePayForTemplate",
GetContractTermsDetails = "getContractTermsDetails",
RunIntegrationTest = "runIntegrationTest",
@@ -458,6 +461,12 @@ export type PreparePayForUriOp = {
response: PreparePayResult;
};
+export type SharePaymentOp = {
+ op: WalletApiOperation.SharePayment;
+ request: SharePaymentRequest;
+ response: SharePaymentResult;
+};
+
/**
* Prepare to make a payment based on a taler://pay-template/ URI.
*/
@@ -984,6 +993,7 @@ export type WalletOperations = {
[WalletApiOperation.GetVersion]: GetVersionOp;
[WalletApiOperation.WithdrawFakebank]: WithdrawFakebankOp;
[WalletApiOperation.PreparePayForUri]: PreparePayForUriOp;
+ [WalletApiOperation.SharePayment]: SharePaymentOp;
[WalletApiOperation.PreparePayForTemplate]: PreparePayForTemplateOp;
[WalletApiOperation.GetContractTermsDetails]: GetContractTermsDetailsOp;
[WalletApiOperation.WithdrawTestkudos]: WithdrawTestkudosOp;
diff --git a/packages/taler-wallet-core/src/wallet.ts b/packages/taler-wallet-core/src/wallet.ts
index 11030af2b..ca86cbb14 100644
--- a/packages/taler-wallet-core/src/wallet.ts
+++ b/packages/taler-wallet-core/src/wallet.ts
@@ -117,6 +117,7 @@ import {
parsePaytoUri,
sampleWalletCoreTransactions,
validateIban,
+ codecForSharePaymentRequest,
} from "@gnu-taler/taler-util";
import {
HttpRequestLibrary,
@@ -203,6 +204,7 @@ import {
getContractTermsDetails,
preparePayForUri,
processPurchase,
+ sharePayment,
startQueryRefund,
startRefundQueryForUri,
} from "./operations/pay-merchant.js";
@@ -1207,6 +1209,11 @@ async function dispatchRequestInternal<Op extends WalletApiOperation>(
await runPending(ws);
return {};
}
+ case WalletApiOperation.SharePayment: {
+ const req = codecForSharePaymentRequest().decode(payload);
+ return await sharePayment(ws, req.merchantBaseUrl, req.orderId);
+ }
+
case WalletApiOperation.PreparePayForUri: {
const req = codecForPreparePayRequest().decode(payload);
return await preparePayForUri(ws, req.talerPayUri);