#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
This commit is contained in:
parent
f47b5bd783
commit
5d76573ac0
@ -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.
|
||||
@ -1270,6 +1275,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?
|
||||
*/
|
||||
|
@ -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,
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
|
Loading…
Reference in New Issue
Block a user