wallet-core: fix withdrawal idempotency

This commit is contained in:
Florian Dold 2022-08-24 19:44:24 +02:00
parent d32d2895ce
commit 42c2b7508f
No known key found for this signature in database
GPG Key ID: D2E4F00F29D02A4B
6 changed files with 78 additions and 38 deletions

View File

@ -452,7 +452,6 @@ export interface BankWithdrawDetails {
suggestedExchange?: string;
confirmTransferUrl?: string;
wireTypes: string[];
extractedStatusUrl: string;
}
export interface AcceptWithdrawalResponse {

View File

@ -24,6 +24,7 @@ import {
BankApi,
BankAccessApi,
} from "@gnu-taler/taler-wallet-core";
import { j2s } from "@gnu-taler/taler-util";
/**
* Run test for basic, bank-integrated withdrawal.
@ -62,6 +63,14 @@ export async function runWithdrawalBankIntegratedTest(t: GlobalTestState) {
talerWithdrawUri: wop.taler_withdraw_uri,
},
);
// Do it twice to check idempotency
const r3 = await wallet.client.call(
WalletApiOperation.AcceptBankIntegratedWithdrawal,
{
exchangeBaseUrl: exchange.baseUrl,
talerWithdrawUri: wop.taler_withdraw_uri,
},
);
await wallet.runPending();
// Confirm it
@ -75,7 +84,8 @@ export async function runWithdrawalBankIntegratedTest(t: GlobalTestState) {
const balResp = await wallet.client.call(WalletApiOperation.GetBalances, {});
t.assertAmountEquals("TESTKUDOS:9.72", balResp.balances[0].available);
await t.shutdown();
const txn = await wallet.client.call(WalletApiOperation.GetTransactions, {});
console.log(`transactions: ${j2s(txn)}`);
}
runWithdrawalBankIntegratedTest.suites = ["wallet"];

View File

@ -112,11 +112,7 @@ export enum ReserveRecordStatus {
* with a bank-integrated withdrawal.
*/
export interface ReserveBankInfo {
/**
* Status URL that the wallet will use to query the status
* of the Taler withdrawal operation on the bank's side.
*/
statusUrl: string;
talerWithdrawUri: string;
/**
* URL that the user can be redirected to, and allows
@ -1799,6 +1795,10 @@ export const WalletStoresV1 = {
{
byReservePub: describeIndex("byReservePub", "reservePub"),
byStatus: describeIndex("byStatus", "operationStatus"),
byTalerWithdrawUri: describeIndex(
"byTalerWithdrawUri",
"bankInfo.talerWithdrawUri",
),
},
),
planchets: describeStore(

View File

@ -46,6 +46,8 @@ import {
parsePaytoUri,
AbsoluteTime,
UnblindedSignature,
BankWithdrawDetails,
parseWithdrawUri,
} from "@gnu-taler/taler-util";
import { TalerCryptoInterface } from "./crypto/cryptoImplementation.js";
import { DenominationRecord } from "./db.js";
@ -57,7 +59,12 @@ import {
isWithdrawableDenom,
readSuccessResponseJsonOrThrow,
} from "./index.browser.js";
import { BankAccessApi, BankApi, BankServiceHandle } from "./index.js";
import {
BankAccessApi,
BankApi,
BankServiceHandle,
getBankStatusUrl,
} from "./index.js";
const logger = new Logger("dbless.ts");
@ -119,7 +126,7 @@ export async function topupReserveWithDemobank(
amount,
);
const bankInfo = await getBankWithdrawalInfo(http, wopi.taler_withdraw_uri);
const bankStatusUrl = bankInfo.extractedStatusUrl;
const bankStatusUrl = getBankStatusUrl(wopi.taler_withdraw_uri);
if (!bankInfo.suggestedExchange) {
throw Error("no suggested exchange");
}

View File

@ -36,7 +36,8 @@ import {
codecForWithdrawResponse,
DenomKeyType,
Duration,
durationFromSpec, encodeCrock,
durationFromSpec,
encodeCrock,
ExchangeListItem,
ExchangeWithdrawRequest,
ForcedDenomSel,
@ -54,7 +55,8 @@ import {
VersionMatchResult,
WithdrawBatchResponse,
WithdrawResponse,
WithdrawUriInfoResponse
WithdrawUriInfoResponse,
WithdrawUriResult,
} from "@gnu-taler/taler-util";
import { EddsaKeypair } from "../crypto/cryptoImplementation.js";
import {
@ -71,12 +73,12 @@ import {
ReserveBankInfo,
ReserveRecordStatus,
WalletStoresV1,
WithdrawalGroupRecord
WithdrawalGroupRecord,
} from "../db.js";
import {
getErrorDetailFromException,
makeErrorDetail,
TalerError
TalerError,
} from "../errors.js";
import { InternalWalletState } from "../internal-wallet-state.js";
import { assertUnreachable } from "../util/assertUnreachable.js";
@ -85,24 +87,21 @@ import {
HttpRequestLibrary,
readSuccessResponseJsonOrErrorCode,
readSuccessResponseJsonOrThrow,
throwUnexpectedRequestError
throwUnexpectedRequestError,
} from "../util/http.js";
import { checkDbInvariant, checkLogicInvariant } from "../util/invariants.js";
import {
DbAccess,
GetReadOnlyAccess
} from "../util/query.js";
import { DbAccess, GetReadOnlyAccess } from "../util/query.js";
import { RetryInfo } from "../util/retries.js";
import {
WALLET_BANK_INTEGRATION_PROTOCOL_VERSION,
WALLET_EXCHANGE_PROTOCOL_VERSION
WALLET_EXCHANGE_PROTOCOL_VERSION,
} from "../versions.js";
import { guardOperationException } from "./common.js";
import {
getExchangeDetails,
getExchangePaytoUri,
getExchangeTrust,
updateExchangeFromUrl
updateExchangeFromUrl,
} from "./exchanges.js";
/**
@ -241,7 +240,7 @@ export function selectWithdrawalDenominations(
for (const d of denoms) {
let count = 0;
const cost = Amounts.add(d.value, d.feeWithdraw).amount;
for (; ;) {
for (;;) {
if (Amounts.cmp(remaining, cost) < 0) {
break;
}
@ -384,7 +383,6 @@ export async function getBankWithdrawalInfo(
return {
amount: Amounts.parseOrThrow(status.amount),
confirmTransferUrl: status.confirm_transfer_url,
extractedStatusUrl: reqUrl.href,
selectionDone: status.selection_done,
senderWire: status.sender_wire,
suggestedExchange: status.suggested_exchange,
@ -898,7 +896,8 @@ export async function updateWithdrawalDenoms(
denom.verificationStatus === DenominationVerificationStatus.Unverified
) {
logger.trace(
`Validating denomination (${current + 1}/${denominations.length
`Validating denomination (${current + 1}/${
denominations.length
}) signature of ${denom.denomPubHash}`,
);
let valid = false;
@ -1025,7 +1024,7 @@ async function queryReserve(
if (
resp.status === 404 &&
result.talerErrorResponse.code ===
TalerErrorCode.EXCHANGE_RESERVES_STATUS_UNKNOWN
TalerErrorCode.EXCHANGE_RESERVES_STATUS_UNKNOWN
) {
ws.notify({
type: NotificationType.ReserveNotYetFound,
@ -1315,7 +1314,7 @@ export async function getExchangeWithdrawalInfo(
) {
logger.warn(
`wallet's support for exchange protocol version ${WALLET_EXCHANGE_PROTOCOL_VERSION} might be outdated ` +
`(exchange has ${exchangeDetails.protocolVersion}), checking for updates`,
`(exchange has ${exchangeDetails.protocolVersion}), checking for updates`,
);
}
}
@ -1400,11 +1399,10 @@ export async function getWithdrawalDetailsForUri(
const exchangeRecords = await tx.exchanges.iter().toArray();
for (const r of exchangeRecords) {
const details = await ws.exchangeOps.getExchangeDetails(tx, r.baseUrl);
const denominations = await tx.denominations.indexes
.byExchangeBaseUrl.iter(r.baseUrl).toArray();
const denominations = await tx.denominations.indexes.byExchangeBaseUrl
.iter(r.baseUrl)
.toArray();
if (details && denominations) {
exchanges.push({
exchangeBaseUrl: details.exchangeBaseUrl,
currency: details.currency,
@ -1417,7 +1415,7 @@ export async function getWithdrawalDetailsForUri(
paytoUris: details.wireInfo.accounts.map((x) => x.payto_uri),
auditors: details.auditors,
wireInfo: details.wireInfo,
denominations: denominations
denominations: denominations,
});
}
}
@ -1502,6 +1500,18 @@ export function getReserveRequestTimeout(r: WithdrawalGroupRecord): Duration {
);
}
export function getBankStatusUrl(talerWithdrawUri: string): string {
const uriResult = parseWithdrawUri(talerWithdrawUri);
if (!uriResult) {
throw Error(`can't parse withdrawal URL ${talerWithdrawUri}`);
}
const url = new URL(
`withdrawal-operation/${uriResult.withdrawalOperationId}`,
uriResult.bankIntegrationApiBaseUrl,
);
return url.href;
}
async function registerReserveWithBank(
ws: InternalWalletState,
withdrawalGroupId: string,
@ -1524,7 +1534,7 @@ async function registerReserveWithBank(
if (!bankInfo) {
return;
}
const bankStatusUrl = bankInfo.statusUrl;
const bankStatusUrl = getBankStatusUrl(bankInfo.talerWithdrawUri);
const httpResp = await ws.http.postJson(
bankStatusUrl,
{
@ -1584,10 +1594,12 @@ async function processReserveBankStatus(
default:
return;
}
const bankStatusUrl = withdrawalGroup.bankInfo?.statusUrl;
if (!bankStatusUrl) {
if (!withdrawalGroup.bankInfo) {
return;
}
const bankStatusUrl = getBankStatusUrl(
withdrawalGroup.bankInfo.talerWithdrawUri,
);
const statusResp = await ws.http.get(bankStatusUrl, {
timeout: getReserveRequestTimeout(withdrawalGroup),
@ -1778,6 +1790,21 @@ export async function acceptWithdrawalFromUri(
restrictAge?: number;
},
): Promise<AcceptWithdrawalResponse> {
const existingWithdrawalGroup = await ws.db
.mktx((x) => ({ withdrawalGroups: x.withdrawalGroups }))
.runReadOnly(async (tx) => {
return await tx.withdrawalGroups.indexes.byTalerWithdrawUri.get(
req.talerWithdrawUri,
);
});
if (existingWithdrawalGroup) {
return {
reservePub: existingWithdrawalGroup.reservePub,
confirmTransferUrl: existingWithdrawalGroup.bankInfo?.confirmUrl,
};
}
await updateExchangeFromUrl(ws, req.selectedExchange);
const withdrawInfo = await getBankWithdrawalInfo(
ws.http,
@ -1796,7 +1823,7 @@ export async function acceptWithdrawalFromUri(
reserveStatus: ReserveRecordStatus.RegisteringBank,
bankInfo: {
exchangePaytoUri,
statusUrl: withdrawInfo.extractedStatusUrl,
talerWithdrawUri: req.talerWithdrawUri,
confirmUrl: withdrawInfo.confirmTransferUrl,
},
});

View File

@ -24,7 +24,6 @@
*/
import {
AbsoluteTime,
AcceptManualWithdrawalResult,
AmountJson,
Amounts,
BalancesResponse,
@ -98,7 +97,6 @@ import {
CoinSourceType,
exportDb,
importDb,
ReserveRecordStatus,
WalletStoresV1,
} from "./db.js";
import { getErrorDetailFromException, TalerError } from "./errors.js";
@ -187,9 +185,8 @@ import {
acceptWithdrawalFromUri,
createManualWithdrawal,
getExchangeWithdrawalInfo,
getFundingPaytoUrisTx,
getWithdrawalDetailsForUri,
processWithdrawalGroup as processWithdrawalGroup,
processWithdrawalGroup,
} from "./operations/withdraw.js";
import {
PendingOperationsResponse,