wallet-core: fix withdrawal idempotency
This commit is contained in:
parent
d32d2895ce
commit
42c2b7508f
@ -452,7 +452,6 @@ export interface BankWithdrawDetails {
|
|||||||
suggestedExchange?: string;
|
suggestedExchange?: string;
|
||||||
confirmTransferUrl?: string;
|
confirmTransferUrl?: string;
|
||||||
wireTypes: string[];
|
wireTypes: string[];
|
||||||
extractedStatusUrl: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AcceptWithdrawalResponse {
|
export interface AcceptWithdrawalResponse {
|
||||||
|
@ -24,6 +24,7 @@ import {
|
|||||||
BankApi,
|
BankApi,
|
||||||
BankAccessApi,
|
BankAccessApi,
|
||||||
} from "@gnu-taler/taler-wallet-core";
|
} from "@gnu-taler/taler-wallet-core";
|
||||||
|
import { j2s } from "@gnu-taler/taler-util";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Run test for basic, bank-integrated withdrawal.
|
* Run test for basic, bank-integrated withdrawal.
|
||||||
@ -62,6 +63,14 @@ export async function runWithdrawalBankIntegratedTest(t: GlobalTestState) {
|
|||||||
talerWithdrawUri: wop.taler_withdraw_uri,
|
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();
|
await wallet.runPending();
|
||||||
|
|
||||||
// Confirm it
|
// Confirm it
|
||||||
@ -75,7 +84,8 @@ export async function runWithdrawalBankIntegratedTest(t: GlobalTestState) {
|
|||||||
const balResp = await wallet.client.call(WalletApiOperation.GetBalances, {});
|
const balResp = await wallet.client.call(WalletApiOperation.GetBalances, {});
|
||||||
t.assertAmountEquals("TESTKUDOS:9.72", balResp.balances[0].available);
|
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"];
|
runWithdrawalBankIntegratedTest.suites = ["wallet"];
|
||||||
|
@ -112,11 +112,7 @@ export enum ReserveRecordStatus {
|
|||||||
* with a bank-integrated withdrawal.
|
* with a bank-integrated withdrawal.
|
||||||
*/
|
*/
|
||||||
export interface ReserveBankInfo {
|
export interface ReserveBankInfo {
|
||||||
/**
|
talerWithdrawUri: string;
|
||||||
* Status URL that the wallet will use to query the status
|
|
||||||
* of the Taler withdrawal operation on the bank's side.
|
|
||||||
*/
|
|
||||||
statusUrl: string;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* URL that the user can be redirected to, and allows
|
* URL that the user can be redirected to, and allows
|
||||||
@ -1799,6 +1795,10 @@ export const WalletStoresV1 = {
|
|||||||
{
|
{
|
||||||
byReservePub: describeIndex("byReservePub", "reservePub"),
|
byReservePub: describeIndex("byReservePub", "reservePub"),
|
||||||
byStatus: describeIndex("byStatus", "operationStatus"),
|
byStatus: describeIndex("byStatus", "operationStatus"),
|
||||||
|
byTalerWithdrawUri: describeIndex(
|
||||||
|
"byTalerWithdrawUri",
|
||||||
|
"bankInfo.talerWithdrawUri",
|
||||||
|
),
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
planchets: describeStore(
|
planchets: describeStore(
|
||||||
|
@ -46,6 +46,8 @@ import {
|
|||||||
parsePaytoUri,
|
parsePaytoUri,
|
||||||
AbsoluteTime,
|
AbsoluteTime,
|
||||||
UnblindedSignature,
|
UnblindedSignature,
|
||||||
|
BankWithdrawDetails,
|
||||||
|
parseWithdrawUri,
|
||||||
} from "@gnu-taler/taler-util";
|
} from "@gnu-taler/taler-util";
|
||||||
import { TalerCryptoInterface } from "./crypto/cryptoImplementation.js";
|
import { TalerCryptoInterface } from "./crypto/cryptoImplementation.js";
|
||||||
import { DenominationRecord } from "./db.js";
|
import { DenominationRecord } from "./db.js";
|
||||||
@ -57,7 +59,12 @@ import {
|
|||||||
isWithdrawableDenom,
|
isWithdrawableDenom,
|
||||||
readSuccessResponseJsonOrThrow,
|
readSuccessResponseJsonOrThrow,
|
||||||
} from "./index.browser.js";
|
} 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");
|
const logger = new Logger("dbless.ts");
|
||||||
|
|
||||||
@ -119,7 +126,7 @@ export async function topupReserveWithDemobank(
|
|||||||
amount,
|
amount,
|
||||||
);
|
);
|
||||||
const bankInfo = await getBankWithdrawalInfo(http, wopi.taler_withdraw_uri);
|
const bankInfo = await getBankWithdrawalInfo(http, wopi.taler_withdraw_uri);
|
||||||
const bankStatusUrl = bankInfo.extractedStatusUrl;
|
const bankStatusUrl = getBankStatusUrl(wopi.taler_withdraw_uri);
|
||||||
if (!bankInfo.suggestedExchange) {
|
if (!bankInfo.suggestedExchange) {
|
||||||
throw Error("no suggested exchange");
|
throw Error("no suggested exchange");
|
||||||
}
|
}
|
||||||
|
@ -36,7 +36,8 @@ import {
|
|||||||
codecForWithdrawResponse,
|
codecForWithdrawResponse,
|
||||||
DenomKeyType,
|
DenomKeyType,
|
||||||
Duration,
|
Duration,
|
||||||
durationFromSpec, encodeCrock,
|
durationFromSpec,
|
||||||
|
encodeCrock,
|
||||||
ExchangeListItem,
|
ExchangeListItem,
|
||||||
ExchangeWithdrawRequest,
|
ExchangeWithdrawRequest,
|
||||||
ForcedDenomSel,
|
ForcedDenomSel,
|
||||||
@ -54,7 +55,8 @@ import {
|
|||||||
VersionMatchResult,
|
VersionMatchResult,
|
||||||
WithdrawBatchResponse,
|
WithdrawBatchResponse,
|
||||||
WithdrawResponse,
|
WithdrawResponse,
|
||||||
WithdrawUriInfoResponse
|
WithdrawUriInfoResponse,
|
||||||
|
WithdrawUriResult,
|
||||||
} from "@gnu-taler/taler-util";
|
} from "@gnu-taler/taler-util";
|
||||||
import { EddsaKeypair } from "../crypto/cryptoImplementation.js";
|
import { EddsaKeypair } from "../crypto/cryptoImplementation.js";
|
||||||
import {
|
import {
|
||||||
@ -71,12 +73,12 @@ import {
|
|||||||
ReserveBankInfo,
|
ReserveBankInfo,
|
||||||
ReserveRecordStatus,
|
ReserveRecordStatus,
|
||||||
WalletStoresV1,
|
WalletStoresV1,
|
||||||
WithdrawalGroupRecord
|
WithdrawalGroupRecord,
|
||||||
} from "../db.js";
|
} from "../db.js";
|
||||||
import {
|
import {
|
||||||
getErrorDetailFromException,
|
getErrorDetailFromException,
|
||||||
makeErrorDetail,
|
makeErrorDetail,
|
||||||
TalerError
|
TalerError,
|
||||||
} from "../errors.js";
|
} from "../errors.js";
|
||||||
import { InternalWalletState } from "../internal-wallet-state.js";
|
import { InternalWalletState } from "../internal-wallet-state.js";
|
||||||
import { assertUnreachable } from "../util/assertUnreachable.js";
|
import { assertUnreachable } from "../util/assertUnreachable.js";
|
||||||
@ -85,24 +87,21 @@ import {
|
|||||||
HttpRequestLibrary,
|
HttpRequestLibrary,
|
||||||
readSuccessResponseJsonOrErrorCode,
|
readSuccessResponseJsonOrErrorCode,
|
||||||
readSuccessResponseJsonOrThrow,
|
readSuccessResponseJsonOrThrow,
|
||||||
throwUnexpectedRequestError
|
throwUnexpectedRequestError,
|
||||||
} from "../util/http.js";
|
} from "../util/http.js";
|
||||||
import { checkDbInvariant, checkLogicInvariant } from "../util/invariants.js";
|
import { checkDbInvariant, checkLogicInvariant } from "../util/invariants.js";
|
||||||
import {
|
import { DbAccess, GetReadOnlyAccess } from "../util/query.js";
|
||||||
DbAccess,
|
|
||||||
GetReadOnlyAccess
|
|
||||||
} from "../util/query.js";
|
|
||||||
import { RetryInfo } from "../util/retries.js";
|
import { RetryInfo } from "../util/retries.js";
|
||||||
import {
|
import {
|
||||||
WALLET_BANK_INTEGRATION_PROTOCOL_VERSION,
|
WALLET_BANK_INTEGRATION_PROTOCOL_VERSION,
|
||||||
WALLET_EXCHANGE_PROTOCOL_VERSION
|
WALLET_EXCHANGE_PROTOCOL_VERSION,
|
||||||
} from "../versions.js";
|
} from "../versions.js";
|
||||||
import { guardOperationException } from "./common.js";
|
import { guardOperationException } from "./common.js";
|
||||||
import {
|
import {
|
||||||
getExchangeDetails,
|
getExchangeDetails,
|
||||||
getExchangePaytoUri,
|
getExchangePaytoUri,
|
||||||
getExchangeTrust,
|
getExchangeTrust,
|
||||||
updateExchangeFromUrl
|
updateExchangeFromUrl,
|
||||||
} from "./exchanges.js";
|
} from "./exchanges.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -241,7 +240,7 @@ export function selectWithdrawalDenominations(
|
|||||||
for (const d of denoms) {
|
for (const d of denoms) {
|
||||||
let count = 0;
|
let count = 0;
|
||||||
const cost = Amounts.add(d.value, d.feeWithdraw).amount;
|
const cost = Amounts.add(d.value, d.feeWithdraw).amount;
|
||||||
for (; ;) {
|
for (;;) {
|
||||||
if (Amounts.cmp(remaining, cost) < 0) {
|
if (Amounts.cmp(remaining, cost) < 0) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@ -384,7 +383,6 @@ export async function getBankWithdrawalInfo(
|
|||||||
return {
|
return {
|
||||||
amount: Amounts.parseOrThrow(status.amount),
|
amount: Amounts.parseOrThrow(status.amount),
|
||||||
confirmTransferUrl: status.confirm_transfer_url,
|
confirmTransferUrl: status.confirm_transfer_url,
|
||||||
extractedStatusUrl: reqUrl.href,
|
|
||||||
selectionDone: status.selection_done,
|
selectionDone: status.selection_done,
|
||||||
senderWire: status.sender_wire,
|
senderWire: status.sender_wire,
|
||||||
suggestedExchange: status.suggested_exchange,
|
suggestedExchange: status.suggested_exchange,
|
||||||
@ -898,7 +896,8 @@ export async function updateWithdrawalDenoms(
|
|||||||
denom.verificationStatus === DenominationVerificationStatus.Unverified
|
denom.verificationStatus === DenominationVerificationStatus.Unverified
|
||||||
) {
|
) {
|
||||||
logger.trace(
|
logger.trace(
|
||||||
`Validating denomination (${current + 1}/${denominations.length
|
`Validating denomination (${current + 1}/${
|
||||||
|
denominations.length
|
||||||
}) signature of ${denom.denomPubHash}`,
|
}) signature of ${denom.denomPubHash}`,
|
||||||
);
|
);
|
||||||
let valid = false;
|
let valid = false;
|
||||||
@ -1025,7 +1024,7 @@ async function queryReserve(
|
|||||||
if (
|
if (
|
||||||
resp.status === 404 &&
|
resp.status === 404 &&
|
||||||
result.talerErrorResponse.code ===
|
result.talerErrorResponse.code ===
|
||||||
TalerErrorCode.EXCHANGE_RESERVES_STATUS_UNKNOWN
|
TalerErrorCode.EXCHANGE_RESERVES_STATUS_UNKNOWN
|
||||||
) {
|
) {
|
||||||
ws.notify({
|
ws.notify({
|
||||||
type: NotificationType.ReserveNotYetFound,
|
type: NotificationType.ReserveNotYetFound,
|
||||||
@ -1315,7 +1314,7 @@ export async function getExchangeWithdrawalInfo(
|
|||||||
) {
|
) {
|
||||||
logger.warn(
|
logger.warn(
|
||||||
`wallet's support for exchange protocol version ${WALLET_EXCHANGE_PROTOCOL_VERSION} might be outdated ` +
|
`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();
|
const exchangeRecords = await tx.exchanges.iter().toArray();
|
||||||
for (const r of exchangeRecords) {
|
for (const r of exchangeRecords) {
|
||||||
const details = await ws.exchangeOps.getExchangeDetails(tx, r.baseUrl);
|
const details = await ws.exchangeOps.getExchangeDetails(tx, r.baseUrl);
|
||||||
const denominations = await tx.denominations.indexes
|
const denominations = await tx.denominations.indexes.byExchangeBaseUrl
|
||||||
.byExchangeBaseUrl.iter(r.baseUrl).toArray();
|
.iter(r.baseUrl)
|
||||||
|
.toArray();
|
||||||
if (details && denominations) {
|
if (details && denominations) {
|
||||||
|
|
||||||
|
|
||||||
exchanges.push({
|
exchanges.push({
|
||||||
exchangeBaseUrl: details.exchangeBaseUrl,
|
exchangeBaseUrl: details.exchangeBaseUrl,
|
||||||
currency: details.currency,
|
currency: details.currency,
|
||||||
@ -1417,7 +1415,7 @@ export async function getWithdrawalDetailsForUri(
|
|||||||
paytoUris: details.wireInfo.accounts.map((x) => x.payto_uri),
|
paytoUris: details.wireInfo.accounts.map((x) => x.payto_uri),
|
||||||
auditors: details.auditors,
|
auditors: details.auditors,
|
||||||
wireInfo: details.wireInfo,
|
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(
|
async function registerReserveWithBank(
|
||||||
ws: InternalWalletState,
|
ws: InternalWalletState,
|
||||||
withdrawalGroupId: string,
|
withdrawalGroupId: string,
|
||||||
@ -1524,7 +1534,7 @@ async function registerReserveWithBank(
|
|||||||
if (!bankInfo) {
|
if (!bankInfo) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const bankStatusUrl = bankInfo.statusUrl;
|
const bankStatusUrl = getBankStatusUrl(bankInfo.talerWithdrawUri);
|
||||||
const httpResp = await ws.http.postJson(
|
const httpResp = await ws.http.postJson(
|
||||||
bankStatusUrl,
|
bankStatusUrl,
|
||||||
{
|
{
|
||||||
@ -1584,10 +1594,12 @@ async function processReserveBankStatus(
|
|||||||
default:
|
default:
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const bankStatusUrl = withdrawalGroup.bankInfo?.statusUrl;
|
if (!withdrawalGroup.bankInfo) {
|
||||||
if (!bankStatusUrl) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const bankStatusUrl = getBankStatusUrl(
|
||||||
|
withdrawalGroup.bankInfo.talerWithdrawUri,
|
||||||
|
);
|
||||||
|
|
||||||
const statusResp = await ws.http.get(bankStatusUrl, {
|
const statusResp = await ws.http.get(bankStatusUrl, {
|
||||||
timeout: getReserveRequestTimeout(withdrawalGroup),
|
timeout: getReserveRequestTimeout(withdrawalGroup),
|
||||||
@ -1778,6 +1790,21 @@ export async function acceptWithdrawalFromUri(
|
|||||||
restrictAge?: number;
|
restrictAge?: number;
|
||||||
},
|
},
|
||||||
): Promise<AcceptWithdrawalResponse> {
|
): 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);
|
await updateExchangeFromUrl(ws, req.selectedExchange);
|
||||||
const withdrawInfo = await getBankWithdrawalInfo(
|
const withdrawInfo = await getBankWithdrawalInfo(
|
||||||
ws.http,
|
ws.http,
|
||||||
@ -1796,7 +1823,7 @@ export async function acceptWithdrawalFromUri(
|
|||||||
reserveStatus: ReserveRecordStatus.RegisteringBank,
|
reserveStatus: ReserveRecordStatus.RegisteringBank,
|
||||||
bankInfo: {
|
bankInfo: {
|
||||||
exchangePaytoUri,
|
exchangePaytoUri,
|
||||||
statusUrl: withdrawInfo.extractedStatusUrl,
|
talerWithdrawUri: req.talerWithdrawUri,
|
||||||
confirmUrl: withdrawInfo.confirmTransferUrl,
|
confirmUrl: withdrawInfo.confirmTransferUrl,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@ -24,7 +24,6 @@
|
|||||||
*/
|
*/
|
||||||
import {
|
import {
|
||||||
AbsoluteTime,
|
AbsoluteTime,
|
||||||
AcceptManualWithdrawalResult,
|
|
||||||
AmountJson,
|
AmountJson,
|
||||||
Amounts,
|
Amounts,
|
||||||
BalancesResponse,
|
BalancesResponse,
|
||||||
@ -98,7 +97,6 @@ import {
|
|||||||
CoinSourceType,
|
CoinSourceType,
|
||||||
exportDb,
|
exportDb,
|
||||||
importDb,
|
importDb,
|
||||||
ReserveRecordStatus,
|
|
||||||
WalletStoresV1,
|
WalletStoresV1,
|
||||||
} from "./db.js";
|
} from "./db.js";
|
||||||
import { getErrorDetailFromException, TalerError } from "./errors.js";
|
import { getErrorDetailFromException, TalerError } from "./errors.js";
|
||||||
@ -187,9 +185,8 @@ import {
|
|||||||
acceptWithdrawalFromUri,
|
acceptWithdrawalFromUri,
|
||||||
createManualWithdrawal,
|
createManualWithdrawal,
|
||||||
getExchangeWithdrawalInfo,
|
getExchangeWithdrawalInfo,
|
||||||
getFundingPaytoUrisTx,
|
|
||||||
getWithdrawalDetailsForUri,
|
getWithdrawalDetailsForUri,
|
||||||
processWithdrawalGroup as processWithdrawalGroup,
|
processWithdrawalGroup,
|
||||||
} from "./operations/withdraw.js";
|
} from "./operations/withdraw.js";
|
||||||
import {
|
import {
|
||||||
PendingOperationsResponse,
|
PendingOperationsResponse,
|
||||||
|
Loading…
Reference in New Issue
Block a user