diff options
author | Florian Dold <florian@dold.me> | 2023-11-22 15:20:10 +0100 |
---|---|---|
committer | Florian Dold <florian@dold.me> | 2023-11-22 17:08:15 +0100 |
commit | bd37a0b04123d734e1e3fae105f0d9c24279629f (patch) | |
tree | 31f03c2acd3c1f123f8762391456b1aa22df9803 /packages/taler-wallet-core | |
parent | 32182fb1b912e1136ba933c4a4f204e6e2f33de2 (diff) | |
download | wallet-core-bd37a0b04123d734e1e3fae105f0d9c24279629f.tar.gz wallet-core-bd37a0b04123d734e1e3fae105f0d9c24279629f.tar.bz2 wallet-core-bd37a0b04123d734e1e3fae105f0d9c24279629f.zip |
wallet-core: implement and test currency conversion withdrawals
Diffstat (limited to 'packages/taler-wallet-core')
5 files changed, 121 insertions, 19 deletions
diff --git a/packages/taler-wallet-core/src/crypto/cryptoImplementation.ts b/packages/taler-wallet-core/src/crypto/cryptoImplementation.ts index 56392f090..36ca128ae 100644 --- a/packages/taler-wallet-core/src/crypto/cryptoImplementation.ts +++ b/packages/taler-wallet-core/src/crypto/cryptoImplementation.ts @@ -164,7 +164,7 @@ export interface TalerCryptoInterface { req: ContractTermsValidationRequest, ): Promise<ValidationResult>; - createEddsaKeypair(req: unknown): Promise<EddsaKeypair>; + createEddsaKeypair(req: {}): Promise<EddsaKeypair>; eddsaGetPublic(req: EddsaGetPublicRequest): Promise<EddsaGetPublicResponse>; diff --git a/packages/taler-wallet-core/src/db.ts b/packages/taler-wallet-core/src/db.ts index 3e8452bcd..0cafae2a1 100644 --- a/packages/taler-wallet-core/src/db.ts +++ b/packages/taler-wallet-core/src/db.ts @@ -52,10 +52,10 @@ import { TalerPreciseTimestamp, TalerProtocolDuration, TalerProtocolTimestamp, - //TalerProtocolTimestamp, TransactionIdStr, UnblindedSignature, WireInfo, + WithdrawalAccountInfo, codecForAny, } from "@gnu-taler/taler-util"; import { DbRetryInfo, TaskIdentifiers } from "./operations/common.js"; @@ -1373,6 +1373,11 @@ export interface WgInfoBankIntegrated { export interface WgInfoBankManual { withdrawalType: WithdrawalRecordType.BankManual; + + /** + * Info about withdrawal accounts, possibly including currency conversion. + */ + exchangeCreditAccounts?: WithdrawalAccountInfo[]; } export interface WgInfoBankPeerPull { diff --git a/packages/taler-wallet-core/src/operations/transactions.ts b/packages/taler-wallet-core/src/operations/transactions.ts index 7ee6275b0..b294c163e 100644 --- a/packages/taler-wallet-core/src/operations/transactions.ts +++ b/packages/taler-wallet-core/src/operations/transactions.ts @@ -720,6 +720,7 @@ function buildTransactionForManualWithdraw( type: WithdrawalType.ManualTransfer, reservePub: withdrawalGroup.reservePub, exchangePaytoUris, + exchangeCreditAccounts: withdrawalGroup.wgInfo.exchangeCreditAccounts, reserveIsReady: withdrawalGroup.status === WithdrawalGroupStatus.Done || withdrawalGroup.status === WithdrawalGroupStatus.PendingReady, diff --git a/packages/taler-wallet-core/src/operations/withdraw.ts b/packages/taler-wallet-core/src/operations/withdraw.ts index 275d0aaf0..a5a6bded8 100644 --- a/packages/taler-wallet-core/src/operations/withdraw.ts +++ b/packages/taler-wallet-core/src/operations/withdraw.ts @@ -24,6 +24,7 @@ import { AgeRestriction, AmountJson, AmountLike, + AmountString, Amounts, BankWithdrawDetails, CancellationToken, @@ -42,6 +43,7 @@ import { LibtoolVersion, Logger, NotificationType, + PaytoUri, TalerError, TalerErrorCode, TalerErrorDetail, @@ -54,21 +56,25 @@ import { TransactionType, URL, UnblindedSignature, + WireAccountDetails, WithdrawUriInfoResponse, + WithdrawalAccountInfo, addPaytoQueryParams, canonicalizeBaseUrl, codecForBankWithdrawalOperationPostResponse, + codecForCashinConversionResponse, codecForExchangeWithdrawBatchResponse, codecForIntegrationBankConfig, codecForReserveStatus, codecForWalletKycUuid, codecForWithdrawOperationStatusResponse, + createEddsaKeyPair, encodeCrock, getErrorDetailFromException, getRandomBytes, j2s, makeErrorDetail, - parseWithdrawUri + parseWithdrawUri, } from "@gnu-taler/taler-util"; import { HttpRequestLibrary, @@ -95,9 +101,8 @@ import { import { ExchangeDetailsRecord, ExchangeEntryDbRecordStatus, - PendingTaskType, isWithdrawableDenom, - timestampPreciseToDb + timestampPreciseToDb, } from "../index.js"; import { InternalWalletState } from "../internal-wallet-state.js"; import { @@ -110,6 +115,7 @@ import { makeExchangeListItem, runLongpollAsync, } from "../operations/common.js"; +import { PendingTaskType } from "../pending-types.js"; import { assertUnreachable } from "../util/assertUnreachable.js"; import { selectForcedWithdrawalDenominations, @@ -1751,8 +1757,23 @@ export async function getExchangeWithdrawalInfo( logger.trace("updating exchange"); const { exchange, exchangeDetails } = await ws.exchangeOps.updateExchangeFromUrl(ws, exchangeBaseUrl); + + if (exchangeDetails.currency != instructedAmount.currency) { + // Specifiying the amount in the conversion input currency is not yet supported. + // We might add support for it later. + throw new Error( + `withdrawal only supported when specifying target currency ${exchangeDetails.currency}`, + ); + } + + const withdrawalAccountList = await fetchWithdrawalAccountInfo(ws, { + exchangeDetails, + instructedAmount, + }); + logger.trace("updating withdrawal denoms"); await updateWithdrawalDenoms(ws, exchangeBaseUrl); + logger.trace("getting candidate denoms"); const denoms = await getCandidateWithdrawalDenoms(ws, exchangeBaseUrl); logger.trace("selecting withdrawal denoms"); @@ -1773,8 +1794,15 @@ export async function getExchangeWithdrawalInfo( } const exchangeWireAccounts: string[] = []; + for (const account of exchangeDetails.wireInfo.accounts) { - exchangeWireAccounts.push(account.payto_uri); + const details: WireAccountDetails = { + paytoUri: account.payto_uri, + }; + if (account.credit_restrictions) { + details.creditRestrictions = account.credit_restrictions; + } + exchangeWireAccounts.push(details.paytoUri); } let hasDenomWithAgeRestriction = false; @@ -1854,6 +1882,7 @@ export async function getExchangeWithdrawalInfo( earliestDepositExpiration, exchangePaytoUris: paytoUris, exchangeWireAccounts, + withdrawalAccountList, exchangeVersion: exchangeDetails.protocolVersionRange || "unknown", numOfferedDenoms: possibleDenoms.length, selectedDenoms, @@ -1946,15 +1975,6 @@ export async function getWithdrawalDetailsForUri( }; } -export async function getFundingPaytoUrisTx( - ws: InternalWalletState, - withdrawalGroupId: string, -): Promise<string[]> { - return await ws.db - .mktx((x) => [x.exchanges, x.exchangeDetails, x.withdrawalGroups]) - .runReadWrite((tx) => getFundingPaytoUris(tx, withdrawalGroupId)); -} - export function augmentPaytoUrisForWithdrawal( plainPaytoUris: string[], reservePub: string, @@ -2361,10 +2381,6 @@ export async function internalPrepareCreateWithdrawalGroup( const exchangeInfo = await updateExchangeFromUrl(ws, canonExchange); const exchangeDetails = exchangeInfo.exchangeDetails; - if (!exchangeDetails) { - logger.trace(exchangeDetails); - throw Error("exchange not updated"); - } const transactionId = constructTransactionIdentifier({ tag: TransactionType.Withdrawal, withdrawalGroupId: withdrawalGroup.withdrawalGroupId, @@ -2566,6 +2582,60 @@ export async function acceptWithdrawalFromUri( } /** + * Gather information about bank accounts that can be used for + * withdrawals. This includes accounts that are in a different + * currency and require conversion. + */ +async function fetchWithdrawalAccountInfo( + ws: InternalWalletState, + req: { + exchangeDetails: ExchangeDetailsRecord; + instructedAmount: AmountJson; + reservePub?: string; + }, +): Promise<WithdrawalAccountInfo[]> { + const { exchangeDetails, instructedAmount } = req; + const withdrawalAccounts: WithdrawalAccountInfo[] = []; + for (let acct of exchangeDetails.wireInfo.accounts) { + let paytoUri: string; + let transferAmount: AmountString; + if (acct.conversion_url != null) { + const reqUrl = new URL("cashin-rate", acct.conversion_url); + reqUrl.searchParams.set( + "amount_credit", + Amounts.stringify(instructedAmount), + ); + const httpResp = await ws.http.fetch(reqUrl.href); + const resp = await readSuccessResponseJsonOrThrow( + httpResp, + codecForCashinConversionResponse(), + ); + paytoUri = acct.payto_uri; + transferAmount = resp.amount_debit; + if (req.reservePub) { + } + } else { + paytoUri = acct.payto_uri; + transferAmount = Amounts.stringify(instructedAmount); + } + paytoUri = addPaytoQueryParams(paytoUri, { + amount: Amounts.stringify(transferAmount), + }); + if (req.reservePub != null) { + paytoUri = addPaytoQueryParams(paytoUri, { + message: `Taler Withdrawal ${req.reservePub}`, + }); + } + const acctInfo: WithdrawalAccountInfo = { + paytoUri, + transferAmount, + }; + withdrawalAccounts.push(acctInfo); + } + return withdrawalAccounts; +} + +/** * Create a manual withdrawal operation. * * Adds the corresponding exchange as a trusted exchange if it is neither @@ -2582,15 +2652,39 @@ export async function createManualWithdrawal( forcedDenomSel?: ForcedDenomSel; }, ): Promise<AcceptManualWithdrawalResult> { + const { exchangeBaseUrl } = req; + const amount = Amounts.parseOrThrow(req.amount); + const { exchangeDetails } = await ws.exchangeOps.updateExchangeFromUrl( + ws, + exchangeBaseUrl, + ); + + if (exchangeDetails.currency != amount.currency) { + throw Error( + "manual withdrawal with conversion from foreign currency is not yet supported", + ); + } + const reserveKeyPair: EddsaKeypair = await ws.cryptoApi.createEddsaKeypair( + {}, + ); + + const withdrawalAccountList = await fetchWithdrawalAccountInfo(ws, { + exchangeDetails, + instructedAmount: amount, + reservePub: reserveKeyPair.pub, + }); + const withdrawalGroup = await internalCreateWithdrawalGroup(ws, { amount: Amounts.jsonifyAmount(req.amount), wgInfo: { withdrawalType: WithdrawalRecordType.BankManual, + exchangeCreditAccounts: withdrawalAccountList, }, exchangeBaseUrl: req.exchangeBaseUrl, forcedDenomSel: req.forcedDenomSel, restrictAge: req.restrictAge, reserveStatus: WithdrawalGroupStatus.PendingQueryingStatus, + reserveKeyPair, }); const withdrawalGroupId = withdrawalGroup.withdrawalGroupId; @@ -2610,6 +2704,7 @@ export async function createManualWithdrawal( return { reservePub: withdrawalGroup.reservePub, exchangePaytoUris: exchangePaytoUris, + withdrawalAccountsList: withdrawalAccountList, transactionId, }; } diff --git a/packages/taler-wallet-core/src/wallet.ts b/packages/taler-wallet-core/src/wallet.ts index 0694aef8a..4472bdbad 100644 --- a/packages/taler-wallet-core/src/wallet.ts +++ b/packages/taler-wallet-core/src/wallet.ts @@ -1215,6 +1215,7 @@ async function dispatchRequestInternal<Op extends WalletApiOperation>( paytoUris: wi.exchangePaytoUris, tosAccepted: wi.termsOfServiceAccepted, ageRestrictionOptions: wi.ageRestrictionOptions, + withdrawalAccountList: wi.withdrawalAccountList, numCoins, }; return resp; |