summaryrefslogtreecommitdiff
path: root/packages
diff options
context:
space:
mode:
authorSebastian <sebasjm@gmail.com>2023-07-03 12:42:44 -0300
committerSebastian <sebasjm@gmail.com>2023-07-03 12:42:44 -0300
commit5d76573ac054c4204e95a26dc286eb0af1f2d10d (patch)
tree602e90b51b1f5989776bc0b727b7956a3432c676 /packages
parentf47b5bd783def7481233f94275e333365d50b08b (diff)
downloadwallet-core-5d76573ac054c4204e95a26dc286eb0af1f2d10d.tar.gz
wallet-core-5d76573ac054c4204e95a26dc286eb0af1f2d10d.tar.bz2
wallet-core-5d76573ac054c4204e95a26dc286eb0af1f2d10d.zip
#7741 share payment
save shared state in backup if purchase is shared check before making the payment of before claim the order already confirmed order can return without effective if coin selection was not made sharePayment operation
Diffstat (limited to 'packages')
-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);