introducing getBalanceDetail for getting all depositable/transferable amount for a currency

This commit is contained in:
Sebastian 2023-01-20 15:41:08 -03:00
parent 81dda3b6b1
commit 7ea8321ddd
No known key found for this signature in database
GPG Key ID: BE4FF68352439FC1
13 changed files with 146 additions and 34 deletions

View File

@ -105,6 +105,16 @@ export class CreateReserveResponse {
reservePub: string; reservePub: string;
} }
export interface GetBalanceDetailRequest {
currency: string;
}
export const codecForGetBalanceDetailRequest = (): Codec<GetBalanceDetailRequest> =>
buildCodecForObject<GetBalanceDetailRequest>()
.property("currency", codecForString())
.build("GetBalanceDetailRequest");
export interface Balance { export interface Balance {
available: AmountString; available: AmountString;
pendingIncoming: AmountString; pendingIncoming: AmountString;
@ -215,11 +225,11 @@ export interface CoinDumpJson {
withdrawal_reserve_pub: string | undefined; withdrawal_reserve_pub: string | undefined;
coin_status: CoinStatus; coin_status: CoinStatus;
spend_allocation: spend_allocation:
| { | {
id: string; id: string;
amount: string; amount: string;
} }
| undefined; | undefined;
/** /**
* Information about the age restriction * Information about the age restriction
*/ */
@ -1792,6 +1802,7 @@ export const codecForUserAttentionsRequest = (): Codec<UserAttentionsRequest> =>
) )
.build("UserAttentionsRequest"); .build("UserAttentionsRequest");
export interface UserAttentionsRequest { export interface UserAttentionsRequest {
priority?: AttentionPriority; priority?: AttentionPriority;
} }

View File

@ -48,14 +48,12 @@
*/ */
import { import {
AmountJson, AmountJson,
BalancesResponse,
Amounts, Amounts,
Logger, BalancesResponse,
AuditorHandle,
ExchangeHandle,
canonicalizeBaseUrl, canonicalizeBaseUrl,
GetBalanceDetailRequest,
Logger,
parsePaytoUri, parsePaytoUri,
TalerErrorCode,
} from "@gnu-taler/taler-util"; } from "@gnu-taler/taler-util";
import { import {
AllowedAuditorInfo, AllowedAuditorInfo,
@ -63,11 +61,10 @@ import {
RefreshGroupRecord, RefreshGroupRecord,
WalletStoresV1, WalletStoresV1,
} from "../db.js"; } from "../db.js";
import { GetReadOnlyAccess } from "../util/query.js";
import { InternalWalletState } from "../internal-wallet-state.js"; import { InternalWalletState } from "../internal-wallet-state.js";
import { getExchangeDetails } from "./exchanges.js";
import { checkLogicInvariant } from "../util/invariants.js"; import { checkLogicInvariant } from "../util/invariants.js";
import { TalerError } from "../errors.js"; import { GetReadOnlyAccess } from "../util/query.js";
import { getExchangeDetails } from "./exchanges.js";
/** /**
* Logger. * Logger.
@ -429,6 +426,43 @@ export async function getMerchantPaymentBalanceDetails(
return d; return d;
} }
export async function getBalanceDetail(
ws: InternalWalletState,
req: GetBalanceDetailRequest,
): Promise<MerchantPaymentBalanceDetails> {
const exchanges: { exchangeBaseUrl: string; exchangePub: string }[] = [];
const wires = new Array<string>();
await ws.db
.mktx((x) => [x.exchanges, x.exchangeDetails])
.runReadOnly(async (tx) => {
const allExchanges = await tx.exchanges.iter().toArray();
for (const e of allExchanges) {
const details = await getExchangeDetails(tx, e.baseUrl);
if (!details || req.currency !== details.currency) {
continue;
}
details.wireInfo.accounts.forEach((a) => {
const payto = parsePaytoUri(a.payto_uri);
if (payto && !wires.includes(payto.targetType)) {
wires.push(payto.targetType);
}
});
exchanges.push({
exchangePub: details.masterPublicKey,
exchangeBaseUrl: e.baseUrl,
});
}
});
return await getMerchantPaymentBalanceDetails(ws, {
currency: req.currency,
acceptedAuditors: [],
acceptedExchanges: exchanges,
acceptedWireMethods: wires,
minAge: 0,
});
}
export interface PeerPaymentRestrictionsForBalance { export interface PeerPaymentRestrictionsForBalance {
currency: string; currency: string;
restrictExchangeTo?: string; restrictExchangeTo?: string;

View File

@ -457,7 +457,6 @@ export async function prepareDepositGroup(
}; };
} }
export async function createDepositGroup( export async function createDepositGroup(
ws: InternalWalletState, ws: InternalWalletState,
req: CreateDepositGroupRequest, req: CreateDepositGroupRequest,

View File

@ -24,7 +24,7 @@
* Imports. * Imports.
*/ */
import { import {
AbortTransactionRequest as AbortTransactionRequest, AbortTransactionRequest,
AcceptBankIntegratedWithdrawalRequest, AcceptBankIntegratedWithdrawalRequest,
AcceptExchangeTosRequest, AcceptExchangeTosRequest,
AcceptManualWithdrawalRequest, AcceptManualWithdrawalRequest,
@ -56,6 +56,7 @@ import {
ExchangesListResponse, ExchangesListResponse,
ForceRefreshRequest, ForceRefreshRequest,
ForgetKnownBankAccountsRequest, ForgetKnownBankAccountsRequest,
GetBalanceDetailRequest,
GetContractTermsDetailsRequest, GetContractTermsDetailsRequest,
GetExchangeTosRequest, GetExchangeTosRequest,
GetExchangeTosResult, GetExchangeTosResult,
@ -66,14 +67,12 @@ import {
InitiatePeerPullPaymentResponse, InitiatePeerPullPaymentResponse,
InitiatePeerPushPaymentRequest, InitiatePeerPushPaymentRequest,
InitiatePeerPushPaymentResponse, InitiatePeerPushPaymentResponse,
InitRequest,
InitResponse, InitResponse,
IntegrationTestArgs, IntegrationTestArgs,
KnownBankAccounts, KnownBankAccounts,
ListKnownBankAccountsRequest, ListKnownBankAccountsRequest,
ManualWithdrawalDetails, ManualWithdrawalDetails,
UserAttentionsCountResponse,
UserAttentionsRequest,
UserAttentionsResponse,
PrepareDepositRequest, PrepareDepositRequest,
PrepareDepositResponse, PrepareDepositResponse,
PreparePayRequest, PreparePayRequest,
@ -99,14 +98,16 @@ import {
TransactionByIdRequest, TransactionByIdRequest,
TransactionsRequest, TransactionsRequest,
TransactionsResponse, TransactionsResponse,
UserAttentionByIdRequest,
UserAttentionsCountResponse,
UserAttentionsRequest,
UserAttentionsResponse,
WalletBackupContentV1, WalletBackupContentV1,
WalletCoreVersion, WalletCoreVersion,
WalletCurrencyInfo, WalletCurrencyInfo,
WithdrawFakebankRequest, WithdrawFakebankRequest,
WithdrawTestBalanceRequest, WithdrawTestBalanceRequest,
WithdrawUriInfoResponse, WithdrawUriInfoResponse,
UserAttentionByIdRequest,
InitRequest,
} from "@gnu-taler/taler-util"; } from "@gnu-taler/taler-util";
import { WalletContractData } from "./db.js"; import { WalletContractData } from "./db.js";
import { import {
@ -116,6 +117,7 @@ import {
RemoveBackupProviderRequest, RemoveBackupProviderRequest,
RunBackupCycleRequest, RunBackupCycleRequest,
} from "./operations/backup/index.js"; } from "./operations/backup/index.js";
import { MerchantPaymentBalanceDetails } from "./operations/balance.js";
import { PendingOperationsResponse as PendingTasksResponse } from "./pending-types.js"; import { PendingOperationsResponse as PendingTasksResponse } from "./pending-types.js";
export enum WalletApiOperation { export enum WalletApiOperation {
@ -138,6 +140,7 @@ export enum WalletApiOperation {
GetWithdrawalDetailsForAmount = "getWithdrawalDetailsForAmount", GetWithdrawalDetailsForAmount = "getWithdrawalDetailsForAmount",
AcceptManualWithdrawal = "acceptManualWithdrawal", AcceptManualWithdrawal = "acceptManualWithdrawal",
GetBalances = "getBalances", GetBalances = "getBalances",
GetBalanceDetail = "getBalanceDetail",
GetUserAttentionRequests = "getUserAttentionRequests", GetUserAttentionRequests = "getUserAttentionRequests",
GetUserAttentionUnreadCount = "getUserAttentionUnreadCount", GetUserAttentionUnreadCount = "getUserAttentionUnreadCount",
MarkAttentionRequestAsRead = "markAttentionRequestAsRead", MarkAttentionRequestAsRead = "markAttentionRequestAsRead",
@ -221,6 +224,11 @@ export type GetBalancesOp = {
request: EmptyObject; request: EmptyObject;
response: BalancesResponse; response: BalancesResponse;
}; };
export type GetBalancesDetailOp = {
op: WalletApiOperation.GetBalanceDetail;
request: GetBalanceDetailRequest;
response: MerchantPaymentBalanceDetails;
};
// group: Managing Transactions // group: Managing Transactions
@ -831,6 +839,7 @@ export type WalletOperations = {
[WalletApiOperation.ConfirmPay]: ConfirmPayOp; [WalletApiOperation.ConfirmPay]: ConfirmPayOp;
[WalletApiOperation.AbortTransaction]: AbortTransactionOp; [WalletApiOperation.AbortTransaction]: AbortTransactionOp;
[WalletApiOperation.GetBalances]: GetBalancesOp; [WalletApiOperation.GetBalances]: GetBalancesOp;
[WalletApiOperation.GetBalanceDetail]: GetBalancesDetailOp;
[WalletApiOperation.GetTransactions]: GetTransactionsOp; [WalletApiOperation.GetTransactions]: GetTransactionsOp;
[WalletApiOperation.GetTransactionById]: GetTransactionByIdOp; [WalletApiOperation.GetTransactionById]: GetTransactionByIdOp;
[WalletApiOperation.RetryPendingNow]: RetryPendingNowOp; [WalletApiOperation.RetryPendingNow]: RetryPendingNowOp;

View File

@ -45,6 +45,7 @@ import {
codecForDeleteTransactionRequest, codecForDeleteTransactionRequest,
codecForForceRefreshRequest, codecForForceRefreshRequest,
codecForForgetKnownBankAccounts, codecForForgetKnownBankAccounts,
codecForGetBalanceDetailRequest,
codecForGetContractTermsDetails, codecForGetContractTermsDetails,
codecForGetExchangeTosRequest, codecForGetExchangeTosRequest,
codecForGetFeeForDeposit, codecForGetFeeForDeposit,
@ -87,6 +88,7 @@ import {
ExchangesListResponse, ExchangesListResponse,
ExchangeTosStatusDetails, ExchangeTosStatusDetails,
FeeDescription, FeeDescription,
GetBalanceDetailRequest,
GetExchangeTosResult, GetExchangeTosResult,
InitResponse, InitResponse,
j2s, j2s,
@ -154,7 +156,11 @@ import {
runBackupCycle, runBackupCycle,
} from "./operations/backup/index.js"; } from "./operations/backup/index.js";
import { setWalletDeviceId } from "./operations/backup/state.js"; import { setWalletDeviceId } from "./operations/backup/state.js";
import { getBalances } from "./operations/balance.js"; import {
getBalanceDetail,
getBalances,
getMerchantPaymentBalanceDetails,
} from "./operations/balance.js";
import { import {
getExchangeTosStatus, getExchangeTosStatus,
makeExchangeListItem, makeExchangeListItem,
@ -948,9 +954,9 @@ async function dumpCoins(ws: InternalWalletState): Promise<CoinDumpJson> {
ageCommitmentProof: c.ageCommitmentProof, ageCommitmentProof: c.ageCommitmentProof,
spend_allocation: c.spendAllocation spend_allocation: c.spendAllocation
? { ? {
amount: c.spendAllocation.amount, amount: c.spendAllocation.amount,
id: c.spendAllocation.id, id: c.spendAllocation.id,
} }
: undefined, : undefined,
}); });
} }
@ -1111,6 +1117,10 @@ async function dispatchRequestInternal<Op extends WalletApiOperation>(
case WalletApiOperation.GetBalances: { case WalletApiOperation.GetBalances: {
return await getBalances(ws); return await getBalances(ws);
} }
case WalletApiOperation.GetBalanceDetail: {
const req = codecForGetBalanceDetailRequest().decode(payload);
return await getBalanceDetail(ws, req);
}
case WalletApiOperation.GetUserAttentionRequests: { case WalletApiOperation.GetUserAttentionRequests: {
const req = codecForUserAttentionsRequest().decode(payload); const req = codecForUserAttentionsRequest().decode(payload);
return await getUserAttentions(ws, req); return await getUserAttentions(ws, req);
@ -1350,7 +1360,8 @@ async function dispatchRequestInternal<Op extends WalletApiOperation>(
{ {
amount: Amounts.stringify(amount), amount: Amounts.stringify(amount),
reserve_pub: wres.reservePub, reserve_pub: wres.reservePub,
debit_account: "payto://x-taler-bank/localhost/testdebtor?receiver-name=Foo", debit_account:
"payto://x-taler-bank/localhost/testdebtor?receiver-name=Foo",
}, },
); );
const fbResp = await readSuccessResponseJsonOrThrow(fbReq, codecForAny()); const fbResp = await readSuccessResponseJsonOrThrow(fbReq, codecForAny());

View File

@ -66,6 +66,7 @@ export namespace State {
error: undefined; error: undefined;
type: Props["type"]; type: Props["type"];
selectCurrency: ButtonHandler; selectCurrency: ButtonHandler;
sendAll: ButtonHandler;
previous: Contact[]; previous: Contact[];
goToBank: ButtonHandler; goToBank: ButtonHandler;
goToWallet: ButtonHandler; goToWallet: ButtonHandler;

View File

@ -27,10 +27,21 @@ import { Contact, Props, State } from "./index.js";
export function useComponentState(props: Props): RecursiveState<State> { export function useComponentState(props: Props): RecursiveState<State> {
const api = useBackendContext(); const api = useBackendContext();
const { pushAlertOnError } = useAlertContext(); const { pushAlertOnError } = useAlertContext();
const parsedInitialAmount = !props.amount const parsedInitialAmount = !props.amount
? undefined ? undefined
: Amounts.parse(props.amount); : Amounts.parse(props.amount);
const hook = useAsyncAsHook(async () => {
if (!parsedInitialAmount) return undefined;
const resp = await api.wallet.call(WalletApiOperation.GetBalanceDetail, {
currency: parsedInitialAmount.currency,
});
return resp;
});
const total = hook && !hook.hasError ? hook.response : undefined;
// const initialCurrency = parsedInitialAmount?.currency; // const initialCurrency = parsedInitialAmount?.currency;
const [amount, setAmount] = useState( const [amount, setAmount] = useState(
@ -120,6 +131,14 @@ export function useComponentState(props: Props): RecursiveState<State> {
props.goToWalletBankDeposit(currencyAndAmount); props.goToWalletBankDeposit(currencyAndAmount);
}), }),
}, },
sendAll: {
onClick:
total === undefined
? undefined
: pushAlertOnError(async () => {
setAmount(total.balanceMerchantDepositable);
}),
},
goToWallet: { goToWallet: {
onClick: invalid onClick: invalid
? undefined ? undefined
@ -143,6 +162,7 @@ export function useComponentState(props: Props): RecursiveState<State> {
setAmount(undefined); setAmount(undefined);
}), }),
}, },
sendAll: {},
goToBank: { goToBank: {
onClick: invalid onClick: invalid
? undefined ? undefined

View File

@ -35,6 +35,7 @@ export const GetCash = tests.createExample(ReadyView, {
}, },
}, },
goToBank: {}, goToBank: {},
sendAll: {},
goToWallet: {}, goToWallet: {},
previous: [], previous: [],
selectCurrency: {}, selectCurrency: {},
@ -49,6 +50,7 @@ export const SendCash = tests.createExample(ReadyView, {
}, },
}, },
goToBank: {}, goToBank: {},
sendAll: {},
goToWallet: {}, goToWallet: {},
previous: [], previous: [],
selectCurrency: {}, selectCurrency: {},

View File

@ -118,6 +118,16 @@ describe("Destination selection states", () => {
expect(state.goToBank.onClick).not.eq(undefined); expect(state.goToBank.onClick).not.eq(undefined);
expect(state.goToWallet.onClick).not.eq(undefined); expect(state.goToWallet.onClick).not.eq(undefined);
expect(state.amountHandler.value).deep.eq(
Amounts.parseOrThrow("ARS:2"),
);
},
(state) => {
if (state.status !== "ready") expect.fail();
if (state.error) expect.fail();
expect(state.goToBank.onClick).not.eq(undefined);
expect(state.goToWallet.onClick).not.eq(undefined);
expect(state.amountHandler.value).deep.eq( expect(state.amountHandler.value).deep.eq(
Amounts.parseOrThrow("ARS:2"), Amounts.parseOrThrow("ARS:2"),
); );

View File

@ -17,6 +17,7 @@
import { styled } from "@linaria/react"; import { styled } from "@linaria/react";
import { Fragment, h, VNode } from "preact"; import { Fragment, h, VNode } from "preact";
import { AmountField } from "../../components/AmountField.js"; import { AmountField } from "../../components/AmountField.js";
import { JustInDevMode } from "../../components/JustInDevMode.js";
import { SelectList } from "../../components/SelectList.js"; import { SelectList } from "../../components/SelectList.js";
import { import {
Input, Input,
@ -283,6 +284,7 @@ export function ReadySendView({
goToBank, goToBank,
goToWallet, goToWallet,
previous, previous,
sendAll,
}: State.Ready): VNode { }: State.Ready): VNode {
const { i18n } = useTranslationContext(); const { i18n } = useTranslationContext();
@ -292,13 +294,18 @@ export function ReadySendView({
<i18n.Translate>Specify the amount and the destination</i18n.Translate> <i18n.Translate>Specify the amount and the destination</i18n.Translate>
</h1> </h1>
<div> <Grid container columns={2} justifyContent="space-between">
<AmountField <AmountField
label={i18n.str`Amount`} label={i18n.str`Amount`}
required required
handler={amountHandler} handler={amountHandler}
/> />
</div> <JustInDevMode>
<Button onClick={sendAll.onClick}>
<i18n.Translate>Send all</i18n.Translate>
</Button>
</JustInDevMode>
</Grid>
<Grid container spacing={1} columns={1}> <Grid container spacing={1} columns={1}>
{previous.length > 0 ? ( {previous.length > 0 ? (

View File

@ -111,8 +111,10 @@ export function HistoryView({
balances: Balance[]; balances: Balance[];
}): VNode { }): VNode {
const { i18n } = useTranslationContext(); const { i18n } = useTranslationContext();
const currencies = balances.map((b) => b.available.split(":")[0]);
const { pushAlertOnError } = useAlertContext(); const { pushAlertOnError } = useAlertContext();
const currencies = balances
.filter((b) => Amounts.isNonZero(b.available))
.map((b) => b.available.split(":")[0]);
const defaultCurrencyIndex = currencies.findIndex( const defaultCurrencyIndex = currencies.findIndex(
(c) => c === defaultCurrency, (c) => c === defaultCurrency,

View File

@ -88,8 +88,8 @@ export interface BackgroundOperations {
}; };
setLoggingLevel: { setLoggingLevel: {
request: { request: {
tag?: string, tag?: string;
level: LogLevel level: LogLevel;
}; };
response: void; response: void;
}; };

View File

@ -186,12 +186,18 @@ const backendHandlers: BackendHandlerType = {
setLoggingLevel, setLoggingLevel,
}; };
async function setLoggingLevel({ tag, level }: { tag?: string, level: LogLevel }): Promise<void> { async function setLoggingLevel({
logger.info(`setting ${tag} to ${level}`) tag,
level,
}: {
tag?: string;
level: LogLevel;
}): Promise<void> {
logger.info(`setting ${tag} to ${level}`);
if (!tag) { if (!tag) {
setGlobalLogLevelFromString(level) setGlobalLogLevelFromString(level);
} else { } else {
setLogLevelFromString(tag, level) setLogLevelFromString(tag, level);
} }
} }