diff --git a/packages/demobank-ui/src/declaration.d.ts b/packages/demobank-ui/src/declaration.d.ts index 4d8484a4f..a37485272 100644 --- a/packages/demobank-ui/src/declaration.d.ts +++ b/packages/demobank-ui/src/declaration.d.ts @@ -329,6 +329,14 @@ namespace SandboxBackend { // where to send cashouts. cashout_address: string; } + interface CashoutEstimate { + // Amount that the user will get deducted from their regional + // bank account, according to the 'amount_credit' value. + amount_debit: Amount; + // Amount that the user will receive in their fiat + // bank account, according to 'amount_debit'. + amount_credit: Amount; + } interface CashoutRequest { // Optional subject to associate to the // cashout operation. This data will appear diff --git a/packages/demobank-ui/src/hooks/circuit.ts b/packages/demobank-ui/src/hooks/circuit.ts index 137a7aee2..f90469e24 100644 --- a/packages/demobank-ui/src/hooks/circuit.ts +++ b/packages/demobank-ui/src/hooks/circuit.ts @@ -32,6 +32,7 @@ import { // FIX default import https://github.com/microsoft/TypeScript/issues/49189 import _useSWR, { SWRHook } from "swr"; +import { AmountJson, Amounts } from "@gnu-taler/taler-util"; const useSWR = _useSWR as unknown as SWRHook; export function useAdminAccountAPI(): AdminAccountAPI { @@ -215,6 +216,23 @@ export interface CircuitAccountAPI { async function getBusinessStatus( request: ReturnType["request"], basicAuth: { username: string; password: string }, +): Promise { + try { + const url = getInitialBackendBaseURL(); + const result = await request( + url, + `circuit-api/accounts/${basicAuth.username}`, + { basicAuth }, + ); + return result.ok; + } catch (error) { + return false; + } +} + +async function getEstimationByCredit( + request: ReturnType["request"], + basicAuth: { username: string; password: string }, ): Promise { try { const url = getInitialBackendBaseURL(); @@ -227,6 +245,93 @@ async function getBusinessStatus( } } +export type TransferCalculation = { + debit: AmountJson; + credit: AmountJson; + beforeFee: AmountJson; +}; +type EstimatorFunction = ( + amount: AmountJson, + sellFee: AmountJson, + sellRate: number, +) => Promise; + +type CashoutEstimators = { + estimateByCredit: EstimatorFunction; + estimateByDebit: EstimatorFunction; +}; + +export function useEstimator(): CashoutEstimators { + const { state } = useBackendContext(); + const { request } = useApiContext(); + const basicAuth = + state.status === "loggedOut" + ? undefined + : { username: state.username, password: state.password }; + return { + estimateByCredit: async (amount, fee, rate) => { + const zeroBalance = Amounts.zeroOfCurrency(fee.currency); + const zeroFiat = Amounts.zeroOfCurrency(fee.currency); + const zeroCalc = { + debit: zeroBalance, + credit: zeroFiat, + beforeFee: zeroBalance, + }; + const url = getInitialBackendBaseURL(); + const result = await request( + url, + `circuit-api/cashouts/estimates`, + { + basicAuth, + params: { + amount_credit: Amounts.stringify(amount), + }, + }, + ); + // const credit = Amounts.parseOrThrow(result.data.data.amount_credit); + const credit = amount; + const _credit = { ...credit, currency: fee.currency }; + const beforeFee = Amounts.sub(_credit, fee).amount; + + const debit = Amounts.parseOrThrow(result.data.amount_debit); + return { + debit, + beforeFee, + credit, + }; + }, + estimateByDebit: async (amount, fee, rate) => { + const zeroBalance = Amounts.zeroOfCurrency(fee.currency); + const zeroFiat = Amounts.zeroOfCurrency(fee.currency); + const zeroCalc = { + debit: zeroBalance, + credit: zeroFiat, + beforeFee: zeroBalance, + }; + const url = getInitialBackendBaseURL(); + const result = await request( + url, + `circuit-api/cashouts/estimates`, + { + basicAuth, + params: { + amount_debit: Amounts.stringify(amount), + }, + }, + ); + const credit = Amounts.parseOrThrow(result.data.amount_credit); + const _credit = { ...credit, currency: fee.currency }; + const debit = amount; + const beforeFee = Amounts.sub(_credit, fee).amount; + return { + debit, + beforeFee, + credit, + }; + }, + }; +} + export function useBusinessAccountFlag(): boolean | undefined { const [isBusiness, setIsBusiness] = useState(); const { state } = useBackendContext(); diff --git a/packages/demobank-ui/src/pages/BusinessAccount.tsx b/packages/demobank-ui/src/pages/BusinessAccount.tsx index 4c322764e..262376fa2 100644 --- a/packages/demobank-ui/src/pages/BusinessAccount.tsx +++ b/packages/demobank-ui/src/pages/BusinessAccount.tsx @@ -34,6 +34,7 @@ import { useAccountDetails } from "../hooks/access.js"; import { useCashoutDetails, useCircuitAccountAPI, + useEstimator, useRatiosAndFeeConfig, } from "../hooks/circuit.js"; import { @@ -230,7 +231,10 @@ function CreateCashout({ const ratiosResult = useRatiosAndFeeConfig(); const result = useAccountDetails(account); const [error, saveError] = useState(); - + const { + estimateByCredit: calculateFromCredit, + estimateByDebit: calculateFromDebit, + } = useEstimator(); const [form, setForm] = useState>({ isDebit: true }); const { createCashout } = useCircuitAccountAPI(); @@ -256,21 +260,45 @@ function CreateCashout({ if (!sellRate || sellRate < 0) return
error rate
; - const amount = Amounts.parse(`${balance.currency}:${form.amount}`); + const amount = Amounts.parseOrThrow( + `${!form.isDebit ? fiatCurrency : balance.currency}:${ + !form.amount ? "0" : form.amount + }`, + ); useEffect(() => { - if (!amount) { - setCalc(zeroCalc); + if (form.isDebit) { + calculateFromDebit(amount, sellFee, sellRate) + .then((r) => { + setCalc(r); + saveError(undefined); + }) + .catch((error) => { + saveError( + error instanceof RequestError + ? buildRequestErrorMessage(i18n, error.cause) + : { + title: i18n.str`Could not estimate the cashout`, + description: error.message, + }, + ); + }); } else { - if (form.isDebit) { - calculateFromDebit(amount, sellFee, sellRate).then((r) => { + calculateFromCredit(amount, sellFee, sellRate) + .then((r) => { setCalc(r); + saveError(undefined); + }) + .catch((error) => { + saveError( + error instanceof RequestError + ? buildRequestErrorMessage(i18n, error.cause) + : { + title: i18n.str`Could not estimate the cashout`, + description: error.message, + }, + ); }); - } else { - calculateFromCredit(amount, sellFee, sellRate).then((r) => { - setCalc(r); - }); - } } }, [form.amount, form.isDebit]); @@ -326,14 +354,10 @@ function CreateCashout({ type="text" readonly class="currency-indicator" - size={ - !form.isDebit ? fiatCurrency.length : balance.currency.length - } - maxLength={ - !form.isDebit ? fiatCurrency.length : balance.currency.length - } + size={amount?.currency.length ?? 0} + maxLength={amount?.currency.length ?? 0} tabIndex={-1} - value={!form.isDebit ? fiatCurrency : balance.currency} + value={amount?.currency ?? ""} />   { - const debit = amount; - - const beforeFee = truncate(Amounts.divide(debit, 1 / sellRate)); - - const credit = Amounts.sub(beforeFee, sellFee).amount; - return { debit, credit, beforeFee }; -} - -async function calculateFromCredit( - amount: AmountJson, - sellFee: AmountJson, - sellRate: number, -): Promise { - const credit = amount; - - const beforeFee = Amounts.add(credit, sellFee).amount; - - const debit = truncate(Amounts.divide(beforeFee, sellRate)); - - return { debit, credit, beforeFee }; -} - export function assertUnreachable(x: never): never { throw new Error("Didn't expect to get here"); }