diff options
Diffstat (limited to 'packages/taler-wallet-core/src/withdraw.ts')
-rw-r--r-- | packages/taler-wallet-core/src/withdraw.ts | 301 |
1 files changed, 173 insertions, 128 deletions
diff --git a/packages/taler-wallet-core/src/withdraw.ts b/packages/taler-wallet-core/src/withdraw.ts index 24e2861e1..0e7ed144c 100644 --- a/packages/taler-wallet-core/src/withdraw.ts +++ b/packages/taler-wallet-core/src/withdraw.ts @@ -344,9 +344,11 @@ export class WithdrawTransactionContext implements TransactionContext { "exchanges" as const, "exchangeDetails" as const, ]; - let stores = opts.extraStores + const stores = opts.extraStores ? [...baseStores, ...opts.extraStores] : baseStores; + + let errorThrown: Error | undefined; const transitionInfo = await this.wex.db.runReadWriteTx( { storeNames: stores }, async (tx) => { @@ -359,7 +361,17 @@ export class WithdrawTransactionContext implements TransactionContext { major: TransactionMajorState.None, }; } - const res = await f(wgRec, tx); + let res: TransitionResult<WithdrawalGroupRecord> | undefined; + try { + res = await f(wgRec, tx); + } catch (error) { + if (error instanceof Error) { + errorThrown = error; + } + return undefined; + } + + // const res = await f(wgRec, tx); switch (res.type) { case TransitionResultType.Transition: { await tx.withdrawalGroups.put(res.rec); @@ -384,6 +396,9 @@ export class WithdrawTransactionContext implements TransactionContext { } }, ); + if (errorThrown) { + throw errorThrown; + } notifyTransition(this.wex, this.transactionId, transitionInfo); return transitionInfo; } @@ -929,6 +944,10 @@ async function processPlanchetGenerate( withdrawalGroup.denomsSel !== undefined, "can't process uninitialized exchange", ); + checkDbInvariant( + withdrawalGroup.exchangeBaseUrl !== undefined, + "can't get funding uri from uninitialized wg", + ); const exchangeBaseUrl = withdrawalGroup.exchangeBaseUrl; let planchet = await wex.db.runReadOnlyTx( { storeNames: ["planchets"] }, @@ -1133,6 +1152,10 @@ async function processPlanchetExchangeBatchRequest( logger.info( `processing planchet exchange batch request ${withdrawalGroup.withdrawalGroupId}, start=${args.coinStartIndex}, len=${args.batchSize}`, ); + checkDbInvariant( + withdrawalGroup.exchangeBaseUrl !== undefined, + "can't get funding uri from uninitialized wg", + ); const exchangeBaseUrl = withdrawalGroup.exchangeBaseUrl; const batchReq: ExchangeBatchWithdrawRequest = { planchets: [] }; @@ -1268,6 +1291,10 @@ async function processPlanchetVerifyAndStoreCoin( resp: ExchangeWithdrawResponse, ): Promise<void> { const withdrawalGroup = wgContext.wgRecord; + checkDbInvariant( + withdrawalGroup.exchangeBaseUrl !== undefined, + "can't get funding uri from uninitialized wg", + ); const exchangeBaseUrl = withdrawalGroup.exchangeBaseUrl; logger.trace(`checking and storing planchet idx=${coinIdx}`); @@ -1517,6 +1544,10 @@ async function processQueryReserve( return TaskRunResult.backoff(); } checkDbInvariant( + withdrawalGroup.exchangeBaseUrl !== undefined, + "can't get funding uri from uninitialized wg", + ); + checkDbInvariant( withdrawalGroup.denomsSel !== undefined, "can't process uninitialized exchange", ); @@ -1752,6 +1783,10 @@ async function redenominateWithdrawal( return; } checkDbInvariant( + wg.exchangeBaseUrl !== undefined, + "can't get funding uri from uninitialized wg", + ); + checkDbInvariant( wg.denomsSel !== undefined, "can't process uninitialized exchange", ); @@ -1894,6 +1929,10 @@ async function processWithdrawalGroupPendingReady( withdrawalGroup.denomsSel !== undefined, "can't process uninitialized exchange", ); + checkDbInvariant( + withdrawalGroup.exchangeBaseUrl !== undefined, + "can't get funding uri from uninitialized wg", + ); const exchangeBaseUrl = withdrawalGroup.exchangeBaseUrl; await fetchFreshExchange(wex, withdrawalGroup.exchangeBaseUrl); @@ -2321,6 +2360,10 @@ export async function getFundingPaytoUris( const withdrawalGroup = await tx.withdrawalGroups.get(withdrawalGroupId); checkDbInvariant(!!withdrawalGroup, `no withdrawal for ${withdrawalGroupId}`); checkDbInvariant( + withdrawalGroup.exchangeBaseUrl !== undefined, + "can't get funding uri from uninitialized wg", + ); + checkDbInvariant( withdrawalGroup.instructedAmount !== undefined, "can't get funding uri from uninitialized wg", ); @@ -2675,7 +2718,7 @@ export async function internalPrepareCreateWithdrawalGroup( args: { reserveStatus: WithdrawalGroupStatus; amount?: AmountJson; - exchangeBaseUrl: string; + exchangeBaseUrl: string | undefined; forcedWithdrawalGroupId?: string; forcedDenomSel?: ForcedDenomSel; reserveKeyPair?: EddsaKeypair; @@ -2716,7 +2759,7 @@ export async function internalPrepareCreateWithdrawalGroup( let initialDenomSel: DenomSelectionState | undefined; const denomSelUid = encodeCrock(getRandomBytes(16)); - if (amount !== undefined) { + if (amount !== undefined && exchangeBaseUrl !== undefined) { initialDenomSel = await getInitialDenomsSelection( wex, exchangeBaseUrl, @@ -2747,7 +2790,9 @@ export async function internalPrepareCreateWithdrawalGroup( wgInfo: args.wgInfo, }; - await fetchFreshExchange(wex, exchangeBaseUrl); + if (exchangeBaseUrl !== undefined) { + await fetchFreshExchange(wex, exchangeBaseUrl); + } const transactionId = constructTransactionIdentifier({ tag: TransactionType.Withdrawal, @@ -2757,12 +2802,13 @@ export async function internalPrepareCreateWithdrawalGroup( return { withdrawalGroup, transactionId, - creationInfo: !amount - ? undefined - : { - amount, - canonExchange: exchangeBaseUrl, - }, + creationInfo: + !amount || !exchangeBaseUrl + ? undefined + : { + amount, + canonExchange: exchangeBaseUrl, + }, }; } @@ -2792,8 +2838,8 @@ export async function internalPerformCreateWithdrawalGroup( if (existingWg) { return { withdrawalGroup: existingWg, - exchangeNotif: undefined, transitionInfo: undefined, + exchangeNotif: undefined, }; } await tx.withdrawalGroups.add(withdrawalGroup); @@ -2809,7 +2855,21 @@ export async function internalPerformCreateWithdrawalGroup( exchangeNotif: undefined, }; } - const exchange = await tx.exchanges.get(prep.creationInfo.canonExchange); + return internalPerformExchangeWasUsed( + wex, + tx, + prep.creationInfo.canonExchange, + withdrawalGroup, + ); +} + +export async function internalPerformExchangeWasUsed( + wex: WalletExecutionContext, + tx: WalletDbReadWriteTransaction<["exchanges"]>, + canonExchange: string, + withdrawalGroup: WithdrawalGroupRecord, +): Promise<PerformCreateWithdrawalGroupResult> { + const exchange = await tx.exchanges.get(canonExchange); if (exchange) { exchange.lastWithdrawal = timestampPreciseToDb(TalerPreciseTimestamp.now()); await tx.exchanges.put(exchange); @@ -2825,11 +2885,7 @@ export async function internalPerformCreateWithdrawalGroup( newTxState, }; - const exchangeUsedRes = await markExchangeUsed( - wex, - tx, - prep.creationInfo.canonExchange, - ); + const exchangeUsedRes = await markExchangeUsed(wex, tx, canonExchange); const ctx = new WithdrawTransactionContext( wex, @@ -2857,7 +2913,7 @@ export async function internalCreateWithdrawalGroup( wex: WalletExecutionContext, args: { reserveStatus: WithdrawalGroupStatus; - exchangeBaseUrl: string; + exchangeBaseUrl: string | undefined; amount?: AmountJson; forcedWithdrawalGroupId?: string; forcedDenomSel?: ForcedDenomSel; @@ -2903,7 +2959,6 @@ export async function prepareBankIntegratedWithdrawal( wex: WalletExecutionContext, req: { talerWithdrawUri: string; - selectedExchange?: string; }, ): Promise<PrepareBankIntegratedWithdrawalResponse> { const existingWithdrawalGroup = await wex.db.runReadOnlyTx( @@ -2932,12 +2987,6 @@ export async function prepareBankIntegratedWithdrawal( const info = await getWithdrawalDetailsForUri(wex, req.talerWithdrawUri); - const exchangeBaseUrl = - req.selectedExchange ?? withdrawInfo.suggestedExchange; - if (!exchangeBaseUrl) { - return { info }; - } - /** * Withdrawal group without exchange and amount * this is an special case when the user haven't yet @@ -2946,7 +2995,7 @@ export async function prepareBankIntegratedWithdrawal( * same URI */ const withdrawalGroup = await internalCreateWithdrawalGroup(wex, { - exchangeBaseUrl, + exchangeBaseUrl: undefined, wgInfo: { withdrawalType: WithdrawalRecordType.BankIntegrated, bankInfo: { @@ -2955,6 +3004,7 @@ export async function prepareBankIntegratedWithdrawal( timestampBankConfirmed: undefined, timestampReserveInfoPosted: undefined, wireTypes: withdrawInfo.wireTypes, + currency: withdrawInfo.currency, }, }, reserveStatus: WithdrawalGroupStatus.DialogProposed, @@ -2977,6 +3027,9 @@ export async function confirmWithdrawal( req: ConfirmWithdrawalRequest, ): Promise<void> { const parsedTx = parseTransactionIdentifier(req.transactionId); + const selectedExchange = req.exchangeBaseUrl; + const instructedAmount = Amounts.parseOrThrow(req.amount); + if (parsedTx?.tag !== TransactionType.Withdrawal) { throw Error("invalid withdrawal transaction ID"); } @@ -2998,7 +3051,6 @@ export async function confirmWithdrawal( throw Error("not a bank integrated withdrawal"); } - const selectedExchange = req.exchangeBaseUrl; const exchange = await fetchFreshExchange(wex, selectedExchange); const talerWithdrawUri = withdrawalGroup.wgInfo.bankInfo.talerWithdrawUri; @@ -3006,30 +3058,36 @@ export async function confirmWithdrawal( /** * The only reason this could be undefined is because it is an old wallet - * database before adding the wireType field was added + * database before adding the prepareWithdrawal feature */ - let wtypes: string[]; - if (withdrawalGroup.wgInfo.bankInfo.wireTypes === undefined) { + let bankWireTypes: string[]; + let bankCurrency: string; + if ( + withdrawalGroup.wgInfo.bankInfo.wireTypes === undefined || + withdrawalGroup.wgInfo.bankInfo.currency === undefined + ) { const withdrawInfo = await getBankWithdrawalInfo( wex.http, talerWithdrawUri, ); - wtypes = withdrawInfo.wireTypes; + bankWireTypes = withdrawInfo.wireTypes; + bankCurrency = withdrawInfo.currency; } else { - wtypes = withdrawalGroup.wgInfo.bankInfo.wireTypes; + bankWireTypes = withdrawalGroup.wgInfo.bankInfo.wireTypes; + bankCurrency = withdrawalGroup.wgInfo.bankInfo.currency; } const exchangePaytoUri = await getExchangePaytoUri( wex, selectedExchange, - wtypes, + bankWireTypes, ); const withdrawalAccountList = await fetchWithdrawalAccountInfo( wex, { exchange, - instructedAmount: Amounts.parseOrThrow(req.amount), + instructedAmount, }, wex.cancellationToken, ); @@ -3040,23 +3098,34 @@ export async function confirmWithdrawal( ); const initalDenoms = await getInitialDenomsSelection( wex, - req.exchangeBaseUrl, - Amounts.parseOrThrow(req.amount), + exchange.exchangeBaseUrl, + instructedAmount, req.forcedDenomSel, ); + let pending = false; await ctx.transition({}, async (rec) => { if (!rec) { return TransitionResult.stay(); } switch (rec.status) { + case WithdrawalGroupStatus.PendingWaitConfirmBank: { + pending = true; + return TransitionResult.stay(); + } + case WithdrawalGroupStatus.AbortedOtherWallet: { + throw TalerError.fromDetail( + TalerErrorCode.WALLET_WITHDRAWAL_OPERATION_ABORTED_BY_BANK, + {}, + ); + } case WithdrawalGroupStatus.DialogProposed: { - rec.exchangeBaseUrl = req.exchangeBaseUrl; + rec.exchangeBaseUrl = exchange.exchangeBaseUrl; rec.instructedAmount = req.amount; + rec.restrictAge = req.restrictAge; rec.denomsSel = initalDenoms; rec.rawWithdrawalAmount = initalDenoms.totalWithdrawCost; rec.effectiveWithdrawalAmount = initalDenoms.totalCoinValue; - rec.restrictAge = req.restrictAge; rec.wgInfo = { withdrawalType: WithdrawalRecordType.BankIntegrated, @@ -3067,19 +3136,50 @@ export async function confirmWithdrawal( confirmUrl: confirmUrl, timestampBankConfirmed: undefined, timestampReserveInfoPosted: undefined, - wireTypes: wtypes, + wireTypes: bankWireTypes, + currency: bankCurrency, }, }; - + pending = true; rec.status = WithdrawalGroupStatus.PendingRegisteringBank; return TransitionResult.transition(rec); } - default: - throw Error("unable to confirm withdrawal in current state"); + default: { + throw Error( + `unable to confirm withdrawal in current state: ${rec.status}`, + ); + } } }); await wex.taskScheduler.resetTaskRetries(ctx.taskId); + + wex.ws.notify({ + type: NotificationType.BalanceChange, + hintTransactionId: ctx.transactionId, + }); + + const res = await wex.db.runReadWriteTx( + { + storeNames: ["exchanges"], + }, + async (tx) => { + const r = await internalPerformExchangeWasUsed( + wex, + tx, + exchange.exchangeBaseUrl, + withdrawalGroup, + ); + return r; + }, + ); + if (res.exchangeNotif) { + wex.ws.notify(res.exchangeNotif); + } + + if (pending) { + await waitWithdrawalRegistered(wex, ctx); + } } /** @@ -3104,112 +3204,57 @@ export async function acceptWithdrawalFromUri( ): Promise<AcceptWithdrawalResponse> { const selectedExchange = req.selectedExchange; logger.info( - `accepting withdrawal via ${req.talerWithdrawUri}, canonicalized selected exchange ${selectedExchange}`, - ); - const existingWithdrawalGroup = await wex.db.runReadOnlyTx( - { storeNames: ["withdrawalGroups"] }, - async (tx) => { - return await tx.withdrawalGroups.indexes.byTalerWithdrawUri.get( - req.talerWithdrawUri, - ); - }, + `preparing withdrawal via ${req.talerWithdrawUri}, canonicalized selected exchange ${selectedExchange}`, ); - if (existingWithdrawalGroup) { - let url: string | undefined; - if ( - existingWithdrawalGroup.wgInfo.withdrawalType === - WithdrawalRecordType.BankIntegrated - ) { - url = existingWithdrawalGroup.wgInfo.bankInfo.confirmUrl; - } - return { - reservePub: existingWithdrawalGroup.reservePub, - confirmTransferUrl: url, - transactionId: constructTransactionIdentifier({ - tag: TransactionType.Withdrawal, - withdrawalGroupId: existingWithdrawalGroup.withdrawalGroupId, - }), - }; - } - - const exchange = await fetchFreshExchange(wex, selectedExchange); - const withdrawInfo = await getBankWithdrawalInfo( - wex.http, - req.talerWithdrawUri, - ); - const exchangePaytoUri = await getExchangePaytoUri( - wex, - selectedExchange, - withdrawInfo.wireTypes, - ); + const p = await prepareBankIntegratedWithdrawal(wex, { + talerWithdrawUri: req.talerWithdrawUri, + }); - let amount: AmountJson; - if (withdrawInfo.amount == null) { + let amount: AmountString; + if (p.info.amount == null) { if (req.amount == null) { throw Error( "amount required, as withdrawal operation has flexible amount", ); } - amount = Amounts.parseOrThrow(req.amount); + amount = req.amount as AmountString; } else { - if ( - req.amount != null && - Amounts.cmp(req.amount, withdrawInfo.amount) != 0 - ) { + if (req.amount != null && Amounts.cmp(req.amount, p.info.amount) != 0) { throw Error( "mismatched amount, amount is fixed by bank but client provided different amount", ); } - amount = withdrawInfo.amount; + amount = p.info.amount; } - const withdrawalAccountList = await fetchWithdrawalAccountInfo( - wex, - { - exchange, - instructedAmount: amount, - }, - CancellationToken.CONTINUE, - ); - - const withdrawalGroup = await internalCreateWithdrawalGroup(wex, { - amount, - exchangeBaseUrl: req.selectedExchange, - wgInfo: { - withdrawalType: WithdrawalRecordType.BankIntegrated, - exchangeCreditAccounts: withdrawalAccountList, - bankInfo: { - exchangePaytoUri, - talerWithdrawUri: req.talerWithdrawUri, - confirmUrl: withdrawInfo.confirmTransferUrl, - timestampBankConfirmed: undefined, - timestampReserveInfoPosted: undefined, - wireTypes: withdrawInfo.wireTypes, - }, - }, + logger.info(`confirming withdrawal with tx ${p.transactionId}`); + await confirmWithdrawal(wex, { + amount: Amounts.stringify(amount), + exchangeBaseUrl: selectedExchange, + transactionId: p.transactionId, restrictAge: req.restrictAge, forcedDenomSel: req.forcedDenomSel, - reserveStatus: WithdrawalGroupStatus.PendingRegisteringBank, - }); - - const withdrawalGroupId = withdrawalGroup.withdrawalGroupId; - - const ctx = new WithdrawTransactionContext(wex, withdrawalGroupId); - - wex.ws.notify({ - type: NotificationType.BalanceChange, - hintTransactionId: ctx.transactionId, }); - wex.taskScheduler.startShepherdTask(ctx.taskId); + const newWithdrawralGroup = await wex.db.runReadOnlyTx( + { storeNames: ["withdrawalGroups"] }, + async (tx) => { + return await tx.withdrawalGroups.indexes.byTalerWithdrawUri.get( + req.talerWithdrawUri, + ); + }, + ); - await waitWithdrawalRegistered(wex, ctx); + checkDbInvariant( + newWithdrawralGroup !== undefined, + "withdrawal don't exist after confirm", + ); return { - reservePub: withdrawalGroup.reservePub, - confirmTransferUrl: withdrawInfo.confirmTransferUrl, - transactionId: ctx.transactionId, + reservePub: newWithdrawralGroup.reservePub, + confirmTransferUrl: p.info.confirmTransferUrl, + transactionId: p.transactionId, }; } @@ -3434,7 +3479,7 @@ export async function createManualWithdrawal( ); const withdrawalGroup = await internalCreateWithdrawalGroup(wex, { - amount: Amounts.jsonifyAmount(req.amount), + amount: amount, wgInfo: { withdrawalType: WithdrawalRecordType.BankManual, exchangeCreditAccounts: withdrawalAccountsList, |