diff options
author | Florian Dold <florian@dold.me> | 2024-09-09 21:18:51 +0200 |
---|---|---|
committer | Florian Dold <florian@dold.me> | 2024-09-09 21:18:54 +0200 |
commit | 049b369f745edb5f8a2d7ec57d5af98ca81b05fa (patch) | |
tree | 285eb6d4de04712f52c9473b1e9cc15854dc5db7 | |
parent | a5e24b8df44d28f5868f1f4279d54a34001c553f (diff) |
wallet-core: move getMaxDepositAmount to coinSelection.ts
The function is now using the normal wallet-core coin selection
infrastructure, leading to automatic support for credit/debit
restrictions and less code duplication.
-rw-r--r-- | packages/taler-util/src/types-taler-merchant.ts | 46 | ||||
-rw-r--r-- | packages/taler-util/src/types-taler-wallet.ts | 2 | ||||
-rw-r--r-- | packages/taler-util/src/types.test.ts | 12 | ||||
-rw-r--r-- | packages/taler-wallet-core/src/coinSelection.test.ts | 168 | ||||
-rw-r--r-- | packages/taler-wallet-core/src/coinSelection.ts | 121 | ||||
-rw-r--r-- | packages/taler-wallet-core/src/deposits.ts | 15 | ||||
-rw-r--r-- | packages/taler-wallet-core/src/instructedAmountConversion.test.ts | 143 | ||||
-rw-r--r-- | packages/taler-wallet-core/src/instructedAmountConversion.ts | 42 | ||||
-rw-r--r-- | packages/taler-wallet-core/src/wallet.ts | 10 |
9 files changed, 295 insertions, 264 deletions
diff --git a/packages/taler-util/src/types-taler-merchant.ts b/packages/taler-util/src/types-taler-merchant.ts index de3664b67..2d24e2d72 100644 --- a/packages/taler-util/src/types-taler-merchant.ts +++ b/packages/taler-util/src/types-taler-merchant.ts @@ -335,18 +335,6 @@ export interface MerchantAbortPayRefundSuccessStatus { exchange_pub: string; } -/** - * Information about an exchange as stored inside a - * merchant's contract terms. - */ -export interface ExchangeHandle { - // The exchange's base URL. - url: string; - - // Master public key of the exchange. - master_pub: EddsaPublicKeyString; -} - export interface AuditorHandle { /** * Official name of the auditor. @@ -531,7 +519,7 @@ export interface MerchantContractTerms { delivery_location?: Location; // Exchanges that the merchant accepts even if it does not accept any auditors that audit them. - exchanges: ExchangeHandle[]; + exchanges: Exchange[]; // List of products that are part of the purchase (see Product). products?: Product[]; @@ -667,12 +655,6 @@ export interface MerchantBlindSigWrapperV1 { blind_sig: string; } -export const codecForExchangeHandle = (): Codec<ExchangeHandle> => - buildCodecForObject<ExchangeHandle>() - .property("master_pub", codecForString()) - .property("url", codecForString()) - .build("ExchangeHandle"); - export const codecForAuditorHandle = (): Codec<AuditorHandle> => buildCodecForObject<AuditorHandle>() .property("name", codecForString()) @@ -727,7 +709,7 @@ export const codecForMerchantContractTerms = (): Codec<MerchantContractTerms> => .property("max_fee", codecForAmountString()) .property("merchant", codecForMerchantInfo()) .property("merchant_pub", codecForString()) - .property("exchanges", codecForList(codecForExchangeHandle())) + .property("exchanges", codecForList(codecForExchange())) .property("products", codecOptional(codecForList(codecForProduct()))) .property("extra", codecForAny()) .property("minimum_age", codecOptional(codecForNumber())) @@ -815,16 +797,13 @@ export interface PaymentResponse { pos_confirmation?: string; } export interface PaymentDeniedLegallyResponse { - // Base URL of the exchanges that denied the payment. // The wallet should refresh the coins from these // exchanges, but may try to pay with coins from // other exchanges. exchange_base_urls: string[]; - } - export interface PaymentStatusRequestParams { // Hash of the order’s contract terms (this is used to // authenticate the wallet/customer in case @@ -1541,7 +1520,6 @@ export interface CategoryProductList { export interface ProductSummary { // Product ID to use. product_id: string; - } export interface CategoryCreateRequest { @@ -2698,6 +2676,7 @@ export interface Tax { // Amount paid in tax. tax: AmountString; } + export interface Merchant { // The merchant's legal name of business. name: string; @@ -2718,6 +2697,7 @@ export interface Merchant { // Some of the typical fields for a location (such as a street address) may be absent. jurisdiction?: Location; } + // Delivery location, loosely modeled as a subset of // ISO20022's PostalAddress25. export interface Location { @@ -2752,16 +2732,7 @@ export interface Location { // Free-form address lines, should not exceed 7 elements. address_lines?: string[]; } -interface Auditor { - // Official name. - name: string; - - // Auditor's public key. - auditor_pub: EddsaPublicKey; - // Base URL of the auditor. - url: string; -} export interface Exchange { // The exchange's base URL. url: string; @@ -2873,10 +2844,11 @@ export const codecForPaymentResponse = (): Codec<PaymentResponse> => .property("sig", codecForString()) .build("TalerMerchantApi.PaymentResponse"); -export const codecForPaymentDeniedLegallyResponse = (): Codec<PaymentDeniedLegallyResponse> => - buildCodecForObject<PaymentDeniedLegallyResponse>() - .property("exchange_base_urls", codecForList(codecForString())) - .build("TalerMerchantApi.PaymentDeniedLegallyResponse"); +export const codecForPaymentDeniedLegallyResponse = + (): Codec<PaymentDeniedLegallyResponse> => + buildCodecForObject<PaymentDeniedLegallyResponse>() + .property("exchange_base_urls", codecForList(codecForString())) + .build("TalerMerchantApi.PaymentDeniedLegallyResponse"); export const codecForStatusPaid = (): Codec<StatusPaid> => buildCodecForObject<StatusPaid>() diff --git a/packages/taler-util/src/types-taler-wallet.ts b/packages/taler-util/src/types-taler-wallet.ts index 244640115..b788155c2 100644 --- a/packages/taler-util/src/types-taler-wallet.ts +++ b/packages/taler-util/src/types-taler-wallet.ts @@ -235,11 +235,13 @@ export const codecForConvertAmountRequest = export interface GetMaxDepositAmountRequest { currency: string; + depositPaytoUri?: string; } export const codecForGetMaxDepositAmountRequest = buildCodecForObject<GetMaxDepositAmountRequest>() .property("currency", codecForString()) + .property("depositPaytoUri", codecOptional(codecForString())) .build("GetAmountRequest"); export interface GetMaxDepositAmountResponse { diff --git a/packages/taler-util/src/types.test.ts b/packages/taler-util/src/types.test.ts index ed55fd16c..bea352128 100644 --- a/packages/taler-util/src/types.test.ts +++ b/packages/taler-util/src/types.test.ts @@ -15,15 +15,14 @@ */ import test from "ava"; -import { codecForContractTerms } from "./index.js"; +import { codecForContractTerms, MerchantContractTerms } from "./index.js"; test("contract terms validation", (t) => { const c = { nonce: "123123123", h_wire: "123", amount: "EUR:1.5", - auditors: [], - exchanges: [{ master_pub: "foo", url: "foo" }], + exchanges: [{ master_pub: "foo", priority: 1, url: "foo" }], fulfillment_url: "foo", max_fee: "EUR:1.5", merchant_pub: "12345", @@ -37,7 +36,7 @@ test("contract terms validation", (t) => { summary: "hello", timestamp: { t_s: 42 }, wire_method: "test", - }; + } satisfies MerchantContractTerms; codecForContractTerms().decode(c); @@ -59,8 +58,7 @@ test("contract terms validation (locations)", (t) => { nonce: "123123123", h_wire: "123", amount: "EUR:1.5", - auditors: [], - exchanges: [{ master_pub: "foo", url: "foo" }], + exchanges: [{ master_pub: "foo", priority: 1, url: "foo" }], fulfillment_url: "foo", max_fee: "EUR:1.5", merchant_pub: "12345", @@ -83,7 +81,7 @@ test("contract terms validation (locations)", (t) => { country: "FR", town: "Rennes", }, - }; + } satisfies MerchantContractTerms; const r = codecForContractTerms().decode(c); diff --git a/packages/taler-wallet-core/src/coinSelection.test.ts b/packages/taler-wallet-core/src/coinSelection.test.ts index c7cb2857e..a13838034 100644 --- a/packages/taler-wallet-core/src/coinSelection.test.ts +++ b/packages/taler-wallet-core/src/coinSelection.test.ts @@ -15,17 +15,21 @@ */ import { AbsoluteTime, + AmountJson, AmountString, Amounts, DenomKeyType, + DenominationPubKey, Duration, + TalerProtocolTimestamp, j2s, } from "@gnu-taler/taler-util"; import test from "ava"; import { - AvailableDenom, + AvailableCoinsOfDenom, CoinSelectionTally, emptyTallyForPeerPayment, + testing_getMaxDepositAmountForAvailableCoins, testing_selectGreedy, } from "./coinSelection.js"; @@ -180,7 +184,7 @@ function createCandidates( numAvailable: number; fromExchange: string; }[], -): AvailableDenom[] { +): AvailableCoinsOfDenom[] { return ar.map((r, idx) => { return { denomPub: { @@ -279,3 +283,163 @@ test("p2p: regression STATER", (t) => { ); t.assert(!!res); }); + +function makeCurrencyHelper(currency: string) { + return (sx: TemplateStringsArray, ...vx: any[]) => { + const s = String.raw({ raw: sx }, ...vx); + return Amounts.parseOrThrow(`${currency}:${s}`); + }; +} + +type TestCoin = [AmountJson, number]; + +const kudos = makeCurrencyHelper("kudos"); + +function defaultFeeConfig( + value: AmountJson, + totalAvailable: number, +): AvailableCoinsOfDenom { + return { + denomPub: undefined as any as DenominationPubKey, + denomPubHash: "123", + feeDeposit: `KUDOS:0.01`, + feeRefresh: `KUDOS:0.01`, + feeRefund: `KUDOS:0.01`, + feeWithdraw: `KUDOS:0.01`, + exchangeBaseUrl: "2", + maxAge: 0, + numAvailable: totalAvailable, + stampExpireDeposit: TalerProtocolTimestamp.never(), + stampExpireLegal: TalerProtocolTimestamp.never(), + stampExpireWithdraw: TalerProtocolTimestamp.never(), + stampStart: TalerProtocolTimestamp.never(), + value: Amounts.stringify(value), + }; +} + +test("deposit max 35", (t) => { + const coinList: TestCoin[] = [ + [kudos`2`, 5], + [kudos`5`, 5], + ]; + const result = testing_getMaxDepositAmountForAvailableCoins( + { + currency: "KUDOS", + }, + { + coinAvailability: coinList.map(([v, t]) => defaultFeeConfig(v, t)), + currentWireFeePerExchange: { + "2": kudos`0`, + }, + }, + ); + t.is(Amounts.stringifyValue(result.rawAmount), "34.9"); + t.is(Amounts.stringifyValue(result.effectiveAmount), "35"); +}); + +test("deposit max 35 with wirefee", (t) => { + const coinList: TestCoin[] = [ + [kudos`2`, 5], + [kudos`5`, 5], + ]; + const result = testing_getMaxDepositAmountForAvailableCoins( + { + currency: "KUDOS", + }, + { + coinAvailability: coinList.map(([v, t]) => defaultFeeConfig(v, t)), + currentWireFeePerExchange: { + "2": kudos`1`, + }, + }, + ); + t.is(Amounts.stringifyValue(result.rawAmount), "33.9"); + t.is(Amounts.stringifyValue(result.effectiveAmount), "35"); +}); + +test("deposit max repeated denom", (t) => { + const coinList: TestCoin[] = [ + [kudos`2`, 1], + [kudos`2`, 1], + [kudos`5`, 1], + ]; + const result = testing_getMaxDepositAmountForAvailableCoins( + { + currency: "KUDOS", + }, + { + coinAvailability: coinList.map(([v, t]) => defaultFeeConfig(v, t)), + currentWireFeePerExchange: { + "2": kudos`0`, + }, + }, + ); + t.is(Amounts.stringifyValue(result.rawAmount), "8.97"); + t.is(Amounts.stringifyValue(result.effectiveAmount), "9"); +}); + +test("demo: deposit max after withdraw raw 25", (t) => { + const coinList: TestCoin[] = [ + [kudos`0.1`, 8], + [kudos`1`, 0], + [kudos`2`, 2], + [kudos`5`, 0], + [kudos`10`, 2], + ]; + const result = testing_getMaxDepositAmountForAvailableCoins( + { + currency: "KUDOS", + }, + { + coinAvailability: coinList.map(([v, t]) => defaultFeeConfig(v, t)), + currentWireFeePerExchange: { + "2": kudos`0.01`, + }, + }, + ); + t.is(Amounts.stringifyValue(result.effectiveAmount), "24.8"); + t.is(Amounts.stringifyValue(result.rawAmount), "24.67"); + + // 8 x 0.1 + // 2 x 0.2 + // 2 x 10.0 + // total effective 24.8 + // deposit fee 12 x 0.01 = 0.12 + // wire fee 0.01 + // total raw: 24.8 - 0.13 = 24.67 + + // current wallet impl fee 0.14 +}); + +test("demo: deposit max after withdraw raw 13", (t) => { + const coinList: TestCoin[] = [ + [kudos`0.1`, 8], + [kudos`1`, 0], + [kudos`2`, 1], + [kudos`5`, 0], + [kudos`10`, 1], + ]; + const result = testing_getMaxDepositAmountForAvailableCoins( + { + currency: "KUDOS", + }, + { + coinAvailability: coinList.map(([v, t]) => defaultFeeConfig(v, t)), + currentWireFeePerExchange: { + "2": kudos`0.01`, + }, + }, + ); + t.is(Amounts.stringifyValue(result.effectiveAmount), "12.8"); + t.is(Amounts.stringifyValue(result.rawAmount), "12.69"); + + // 8 x 0.1 + // 1 x 0.2 + // 1 x 10.0 + // total effective 12.8 + // deposit fee 10 x 0.01 = 0.10 + // wire fee 0.01 + // total raw: 12.8 - 0.11 = 12.69 + + // current wallet impl fee 0.14 +}); diff --git a/packages/taler-wallet-core/src/coinSelection.ts b/packages/taler-wallet-core/src/coinSelection.ts index e6e7892ce..bf24a883a 100644 --- a/packages/taler-wallet-core/src/coinSelection.ts +++ b/packages/taler-wallet-core/src/coinSelection.ts @@ -38,6 +38,8 @@ import { DenominationInfo, ExchangeGlobalFees, ForcedCoinSel, + GetMaxDepositAmountRequest, + GetMaxDepositAmountResponse, j2s, Logger, parsePaytoUri, @@ -180,18 +182,17 @@ async function internalSelectPayCoins( | undefined > { const { contractTermsAmount, depositFeeLimit } = req; - const [candidateDenoms, wireFeesPerExchange] = await selectPayCandidates( - wex, - tx, - { - restrictExchanges: req.restrictExchanges, - instructedAmount: req.contractTermsAmount, - restrictWireMethod: req.restrictWireMethod, - depositPaytoUri: req.depositPaytoUri, - requiredMinimumAge: req.requiredMinimumAge, - includePendingCoins, - }, - ); + const candidateRes = await selectPayCandidates(wex, tx, { + currency: Amounts.currencyOf(req.contractTermsAmount), + restrictExchanges: req.restrictExchanges, + restrictWireMethod: req.restrictWireMethod, + depositPaytoUri: req.depositPaytoUri, + requiredMinimumAge: req.requiredMinimumAge, + includePendingCoins, + }); + + const wireFeesPerExchange = candidateRes.currentWireFeePerExchange; + const candidateDenoms = candidateRes.coinAvailability; if (logger.shouldLogTrace()) { logger.trace( @@ -589,7 +590,7 @@ export interface SelectGreedyRequest { function selectGreedy( req: SelectGreedyRequest, - candidateDenoms: AvailableDenom[], + candidateDenoms: AvailableCoinsOfDenom[], tally: CoinSelectionTally, ): SelResult | undefined { const selectedDenom: SelResult = {}; @@ -652,7 +653,7 @@ function selectGreedy( function selectForced( req: SelectPayCoinRequestNg, - candidateDenoms: AvailableDenom[], + candidateDenoms: AvailableCoinsOfDenom[], ): SelResult | undefined { const selectedDenom: SelResult = {}; @@ -713,7 +714,7 @@ export interface SelectPayCoinRequestNg { depositPaytoUri?: string; } -export type AvailableDenom = DenominationInfo & { +export type AvailableCoinsOfDenom = DenominationInfo & { maxAge: number; numAvailable: number; }; @@ -794,7 +795,7 @@ function checkExchangeAccepted( } interface SelectPayCandidatesRequest { - instructedAmount: AmountJson; + currency: string; restrictWireMethod: string | undefined; depositPaytoUri?: string; restrictExchanges: ExchangeRestrictionSpec | undefined; @@ -808,18 +809,23 @@ interface SelectPayCandidatesRequest { includePendingCoins: boolean; } +export interface PayCoinCandidates { + coinAvailability: AvailableCoinsOfDenom[]; + currentWireFeePerExchange: Record<string, AmountJson>; +} + async function selectPayCandidates( wex: WalletExecutionContext, tx: WalletDbReadOnlyTransaction< ["exchanges", "coinAvailability", "exchangeDetails", "denominations"] >, req: SelectPayCandidatesRequest, -): Promise<[AvailableDenom[], Record<string, AmountJson>]> { +): Promise<PayCoinCandidates> { // FIXME: Use the existing helper (from balance.ts) to // get acceptable exchanges. logger.shouldLogTrace() && logger.trace(`selecting available coin candidates for ${j2s(req)}`); - const denoms: AvailableDenom[] = []; + const denoms: AvailableCoinsOfDenom[] = []; const exchanges = await tx.exchanges.iter().toArray(); const wfPerExchange: Record<string, AmountJson> = {}; for (const exchange of exchanges) { @@ -828,7 +834,7 @@ async function selectPayCandidates( exchange.baseUrl, ); // 1. exchange has same currency - if (exchangeDetails?.currency !== req.instructedAmount.currency) { + if (exchangeDetails?.currency !== req.currency) { logger.shouldLogTrace() && logger.trace(`skipping ${exchange.baseUrl} due to currency mismatch`); continue; @@ -935,7 +941,10 @@ async function selectPayCandidates( Amounts.cmp(o1.feeDeposit, o2.feeDeposit) || strcmp(o1.denomPubHash, o2.denomPubHash), ); - return [denoms, wfPerExchange]; + return { + coinAvailability: denoms, + currentWireFeePerExchange: wfPerExchange, + }; } export interface PeerCoinSelectionDetails { @@ -1072,7 +1081,7 @@ async function internalSelectPeerCoins( | undefined > { const candidatesRes = await selectPayCandidates(wex, tx, { - instructedAmount: req.instructedAmount, + currency: Amounts.currencyOf(req.instructedAmount), restrictExchanges: { auditors: [], exchanges: [ @@ -1085,7 +1094,7 @@ async function internalSelectPeerCoins( restrictWireMethod: undefined, includePendingCoins, }); - const candidates = candidatesRes[0]; + const candidates = candidatesRes.coinAvailability; if (logger.shouldLogTrace()) { logger.trace(`peer payment candidate coins: ${j2s(candidates)}`); } @@ -1258,3 +1267,71 @@ export async function selectPeerCoins( }, ); } + +function getMaxDepositAmountForAvailableCoins( + req: GetMaxDepositAmountRequest, + candidateRes: PayCoinCandidates, +): GetMaxDepositAmountResponse { + const wireFeeCoveredForExchange = new Set<string>(); + + let amountEffective = Amounts.zeroOfCurrency(req.currency); + let fees = Amounts.zeroOfCurrency(req.currency); + + for (const cc of candidateRes.coinAvailability) { + if (!wireFeeCoveredForExchange.has(cc.exchangeBaseUrl)) { + fees = Amounts.add( + fees, + candidateRes.currentWireFeePerExchange[cc.exchangeBaseUrl], + ).amount; + wireFeeCoveredForExchange.add(cc.exchangeBaseUrl); + } + + amountEffective = Amounts.add( + amountEffective, + Amounts.mult(cc.value, cc.numAvailable).amount, + ).amount; + + fees = Amounts.add( + fees, + Amounts.mult(cc.feeDeposit, cc.numAvailable).amount, + ).amount; + } + + return { + effectiveAmount: Amounts.stringify(amountEffective), + rawAmount: Amounts.stringify(Amounts.sub(amountEffective, fees).amount), + }; +} + +/** + * Only used for unit testing getMaxDepositAmountForAvailableCoins. + */ +export const testing_getMaxDepositAmountForAvailableCoins = + getMaxDepositAmountForAvailableCoins; + +export async function getMaxDepositAmount( + wex: WalletExecutionContext, + req: GetMaxDepositAmountRequest, +): Promise<GetMaxDepositAmountResponse> { + return await wex.db.runReadOnlyTx( + { + storeNames: [ + "exchanges", + "coinAvailability", + "denominations", + "exchangeDetails", + ], + }, + async (tx): Promise<GetMaxDepositAmountResponse> => { + const candidateRes = await selectPayCandidates(wex, tx, { + currency: req.currency, + restrictExchanges: undefined, + restrictWireMethod: undefined, + depositPaytoUri: req.depositPaytoUri, + requiredMinimumAge: undefined, + includePendingCoins: true, + }); + return getMaxDepositAmountForAvailableCoins(req, candidateRes); + }, + ); +} diff --git a/packages/taler-wallet-core/src/deposits.ts b/packages/taler-wallet-core/src/deposits.ts index b27b0157f..0ca595e50 100644 --- a/packages/taler-wallet-core/src/deposits.ts +++ b/packages/taler-wallet-core/src/deposits.ts @@ -27,21 +27,21 @@ import { Amounts, BatchDepositRequestCoin, CancellationToken, + CheckDepositRequest, + CheckDepositResponse, CoinRefreshRequest, CreateDepositGroupRequest, CreateDepositGroupResponse, DepositGroupFees, DepositTransactionTrackingState, Duration, + Exchange, ExchangeBatchDepositRequest, - ExchangeHandle, ExchangeRefundRequest, HttpStatusCode, Logger, MerchantContractTerms, NotificationType, - CheckDepositRequest, - CheckDepositResponse, RefreshReason, SelectedProspectiveCoin, TalerError, @@ -1740,7 +1740,7 @@ export async function internalCheckDepositGroup( const amount = Amounts.parseOrThrow(req.amount); const currency = Amounts.currencyOf(amount); - const exchangeInfos: ExchangeHandle[] = []; + const exchangeInfos: Exchange[] = []; await wex.db.runReadOnlyTx( { storeNames: ["exchangeDetails", "exchanges"] }, @@ -1753,6 +1753,7 @@ export async function internalCheckDepositGroup( } exchangeInfos.push({ master_pub: details.masterPublicKey, + priority: 1, url: e.baseUrl, }); } @@ -1948,7 +1949,11 @@ export async function createDepositGroup( const wireSalt = encodeCrock(getRandomBytes(16)); const wireHash = hashWire(req.depositPaytoUri, wireSalt); const contractTerms: MerchantContractTerms = { - exchanges: exchangeInfos, + exchanges: exchangeInfos.map((x) => ({ + master_pub: x.master_pub, + priority: 1, + url: x.url, + })), amount: req.amount, max_fee: Amounts.stringify(amount), wire_method: depositPayto.targetType, diff --git a/packages/taler-wallet-core/src/instructedAmountConversion.test.ts b/packages/taler-wallet-core/src/instructedAmountConversion.test.ts index 03e702568..17c2439c7 100644 --- a/packages/taler-wallet-core/src/instructedAmountConversion.test.ts +++ b/packages/taler-wallet-core/src/instructedAmountConversion.test.ts @@ -26,7 +26,6 @@ import { CoinInfo, convertDepositAmountForAvailableCoins, convertWithdrawalAmountFromAvailableCoins, - getMaxDepositAmountForAvailableCoins, } from "./instructedAmountConversion.js"; function makeCurrencyHelper(currency: string) { @@ -254,76 +253,6 @@ test("deposit with wire fee raw 2", (t) => { * */ -test("deposit max 35", (t) => { - const coinList: Coin[] = [ - [kudos`2`, 5], - [kudos`5`, 5], - ]; - const result = getMaxDepositAmountForAvailableCoins( - { - list: coinList.map(([v, t]) => defaultFeeConfig(v, t)), - exchanges: { - "2": { - wireFee: kudos`0.00`, - purseFee: kudos`0.00`, - creditDeadline: AbsoluteTime.never(), - debitDeadline: AbsoluteTime.never(), - }, - }, - }, - "KUDOS", - ); - t.is(Amounts.stringifyValue(result.raw), "34.9"); - t.is(Amounts.stringifyValue(result.effective), "35"); -}); - -test("deposit max 35 with wirefee", (t) => { - const coinList: Coin[] = [ - [kudos`2`, 5], - [kudos`5`, 5], - ]; - const result = getMaxDepositAmountForAvailableCoins( - { - list: coinList.map(([v, t]) => defaultFeeConfig(v, t)), - exchanges: { - "2": { - wireFee: kudos`1`, - purseFee: kudos`0.00`, - creditDeadline: AbsoluteTime.never(), - debitDeadline: AbsoluteTime.never(), - }, - }, - }, - "KUDOS", - ); - t.is(Amounts.stringifyValue(result.raw), "33.9"); - t.is(Amounts.stringifyValue(result.effective), "35"); -}); - -test("deposit max repeated denom", (t) => { - const coinList: Coin[] = [ - [kudos`2`, 1], - [kudos`2`, 1], - [kudos`5`, 1], - ]; - const result = getMaxDepositAmountForAvailableCoins( - { - list: coinList.map(([v, t]) => defaultFeeConfig(v, t)), - exchanges: { - "2": { - wireFee: kudos`0.00`, - purseFee: kudos`0.00`, - creditDeadline: AbsoluteTime.never(), - debitDeadline: AbsoluteTime.never(), - }, - }, - }, - "KUDOS", - ); - t.is(Amounts.stringifyValue(result.raw), "8.97"); - t.is(Amounts.stringifyValue(result.effective), "9"); -}); - /** * Making a withdrawal with effective amount * @@ -663,42 +592,6 @@ test("demo: withdraw raw 25", (t) => { //shows fee = 0.2 }); -test("demo: deposit max after withdraw raw 25", (t) => { - const coinList: Coin[] = [ - [kudos`0.1`, 8], - [kudos`1`, 0], - [kudos`2`, 2], - [kudos`5`, 0], - [kudos`10`, 2], - ]; - const result = getMaxDepositAmountForAvailableCoins( - { - list: coinList.map(([v, t]) => defaultFeeConfig(v, t)), - exchanges: { - one: { - wireFee: kudos`0.01`, - purseFee: kudos`0.00`, - creditDeadline: AbsoluteTime.never(), - debitDeadline: AbsoluteTime.never(), - }, - }, - }, - "KUDOS", - ); - t.is(Amounts.stringifyValue(result.effective), "24.8"); - t.is(Amounts.stringifyValue(result.raw), "24.67"); - - // 8 x 0.1 - // 2 x 0.2 - // 2 x 10.0 - // total effective 24.8 - // deposit fee 12 x 0.01 = 0.12 - // wire fee 0.01 - // total raw: 24.8 - 0.13 = 24.67 - - // current wallet impl fee 0.14 -}); - test("demo: withdraw raw 13", (t) => { const coinList: Coin[] = [ [kudos`0.1`, 0], @@ -729,39 +622,3 @@ test("demo: withdraw raw 13", (t) => { //current wallet impl: hides the left in reserve fee //shows fee = 0.2 }); - -test("demo: deposit max after withdraw raw 13", (t) => { - const coinList: Coin[] = [ - [kudos`0.1`, 8], - [kudos`1`, 0], - [kudos`2`, 1], - [kudos`5`, 0], - [kudos`10`, 1], - ]; - const result = getMaxDepositAmountForAvailableCoins( - { - list: coinList.map(([v, t]) => defaultFeeConfig(v, t)), - exchanges: { - one: { - wireFee: kudos`0.01`, - purseFee: kudos`0.00`, - creditDeadline: AbsoluteTime.never(), - debitDeadline: AbsoluteTime.never(), - }, - }, - }, - "KUDOS", - ); - t.is(Amounts.stringifyValue(result.effective), "12.8"); - t.is(Amounts.stringifyValue(result.raw), "12.69"); - - // 8 x 0.1 - // 1 x 0.2 - // 1 x 10.0 - // total effective 12.8 - // deposit fee 10 x 0.01 = 0.10 - // wire fee 0.01 - // total raw: 12.8 - 0.11 = 12.69 - - // current wallet impl fee 0.14 -}); diff --git a/packages/taler-wallet-core/src/instructedAmountConversion.ts b/packages/taler-wallet-core/src/instructedAmountConversion.ts index cbdd643a0..c0f835dfa 100644 --- a/packages/taler-wallet-core/src/instructedAmountConversion.ts +++ b/packages/taler-wallet-core/src/instructedAmountConversion.ts @@ -23,7 +23,6 @@ import { Amounts, ConvertAmountRequest, Duration, - GetMaxDepositAmountRequest, TransactionAmountMode, TransactionType, checkDbInvariant, @@ -429,47 +428,6 @@ export function convertDepositAmountForAvailableCoins( return result; } -export async function getMaxDepositAmount( - wex: WalletExecutionContext, - req: GetMaxDepositAmountRequest, -): Promise<AmountResponse> { - const denoms = await getAvailableCoins( - wex, - TransactionType.Deposit, - req.currency, - {}, - ); - - const result = getMaxDepositAmountForAvailableCoins(denoms, req.currency); - return { - effectiveAmount: Amounts.stringify(result.effective), - rawAmount: Amounts.stringify(result.raw), - }; -} - -export function getMaxDepositAmountForAvailableCoins( - denoms: AvailableCoins, - currency: string, -): AmountWithFee { - const zero = Amounts.zeroOfCurrency(currency); - if (!denoms.list.length) { - // no coins in the database - return { effective: zero, raw: zero }; - } - - const result = getTotalEffectiveAndRawForDeposit( - denoms.list.map((info) => { - return { info, size: info.totalAvailable ?? 0 }; - }), - currency, - ); - - const wireFee = Object.values(denoms.exchanges)[0]?.wireFee ?? zero; - result.raw = Amounts.sub(result.raw, wireFee).amount; - - return result; -} - export async function convertWithdrawalAmount( wex: WalletExecutionContext, req: ConvertAmountRequest, diff --git a/packages/taler-wallet-core/src/wallet.ts b/packages/taler-wallet-core/src/wallet.ts index a4ed7db1c..4b01cec60 100644 --- a/packages/taler-wallet-core/src/wallet.ts +++ b/packages/taler-wallet-core/src/wallet.ts @@ -133,6 +133,7 @@ import { codecForAny, codecForApplyDevExperiment, codecForCanonicalizeBaseUrlRequest, + codecForCheckDepositRequest, codecForCheckPayTemplateRequest, codecForCheckPeerPullPaymentRequest, codecForCheckPeerPushDebitRequest, @@ -148,7 +149,6 @@ import { codecForFailTransactionRequest, codecForForceRefreshRequest, codecForForgetKnownBankAccounts, - codecForGetMaxDepositAmountRequest, codecForGetBalanceDetailRequest, codecForGetBankingChoicesForPaytoRequest, codecForGetContractTermsDetails, @@ -157,6 +157,7 @@ import { codecForGetExchangeEntryByUrlRequest, codecForGetExchangeResourcesRequest, codecForGetExchangeTosRequest, + codecForGetMaxDepositAmountRequest, codecForGetQrCodesForPaytoRequest, codecForGetWithdrawalDetailsForAmountRequest, codecForGetWithdrawalDetailsForUri, @@ -170,7 +171,6 @@ import { codecForListExchangesForScopedCurrencyRequest, codecForListKnownBankAccounts, codecForPrepareBankIntegratedWithdrawalRequest, - codecForCheckDepositRequest, codecForPreparePayRequest, codecForPreparePayTemplateRequest, codecForPreparePeerPullPaymentRequest, @@ -233,6 +233,7 @@ import { setWalletDeviceId, } from "./backup/index.js"; import { getBalanceDetail, getBalances } from "./balance.js"; +import { getMaxDepositAmount } from "./coinSelection.js"; import { TalerCryptoInterface } from "./crypto/cryptoImplementation.js"; import { CryptoDispatcher, @@ -276,10 +277,7 @@ import { listExchanges, lookupExchangeByUri, } from "./exchanges.js"; -import { - convertDepositAmount, - getMaxDepositAmount, -} from "./instructedAmountConversion.js"; +import { convertDepositAmount } from "./instructedAmountConversion.js"; import { ObservableDbAccess, ObservableTaskScheduler, |