wallet-core, wallet-cli: add status to exchange list, add detail query to CLI

This commit is contained in:
Florian Dold 2022-10-15 21:26:36 +02:00
parent d98d49aa58
commit fbb7dd9e7e
No known key found for this signature in database
GPG Key ID: D2E4F00F29D02A4B
12 changed files with 133 additions and 75 deletions

View File

@ -905,13 +905,27 @@ export enum ExchangeTosStatus {
Accepted = "accepted", Accepted = "accepted",
Changed = "changed", Changed = "changed",
NotFound = "not-found", NotFound = "not-found",
Unknown = "unknown",
} }
export enum ExchangeEntryStatus {
Unknown = "unknown",
Outdated = "outdated",
Ok = "ok",
}
// FIXME: This should probably include some error status.
export interface ExchangeListItem { export interface ExchangeListItem {
exchangeBaseUrl: string; exchangeBaseUrl: string;
currency: string; currency: string | undefined;
paytoUris: string[]; paytoUris: string[];
tosStatus: ExchangeTosStatus; tosStatus: ExchangeTosStatus;
exchangeStatus: ExchangeEntryStatus;
/**
* Permanently added to the wallet, as opposed to just
* temporarily queried.
*/
permanent: boolean;
} }
const codecForAuditorDenomSig = (): Codec<AuditorDenomSig> => const codecForAuditorDenomSig = (): Codec<AuditorDenomSig> =>
@ -984,6 +998,8 @@ export const codecForExchangeListItem = (): Codec<ExchangeListItem> =>
.property("exchangeBaseUrl", codecForString()) .property("exchangeBaseUrl", codecForString())
.property("paytoUris", codecForList(codecForString())) .property("paytoUris", codecForList(codecForString()))
.property("tosStatus", codecForAny()) .property("tosStatus", codecForAny())
.property("exchangeStatus", codecForAny())
.property("permanent", codecForBoolean())
.build("ExchangeListItem"); .build("ExchangeListItem");
export const codecForExchangesListResponse = (): Codec<ExchangesListResponse> => export const codecForExchangesListResponse = (): Codec<ExchangesListResponse> =>

View File

@ -549,6 +549,25 @@ exchangesCli
}); });
}); });
exchangesCli
.subcommand("exchangesShowCmd", "show", {
help: "Show exchange details",
})
.requiredArgument("url", clk.STRING, {
help: "Base URL of the exchange.",
})
.action(async (args) => {
await withWallet(args, async (wallet) => {
const resp = await wallet.client.call(
WalletApiOperation.GetExchangeDetailedInfo,
{
exchangeBaseUrl: args.exchangesShowCmd.url,
},
);
console.log(JSON.stringify(resp, undefined, 2));
});
});
exchangesCli exchangesCli
.subcommand("exchangesAddCmd", "add", { .subcommand("exchangesAddCmd", "add", {
help: "Add an exchange by base URL.", help: "Add an exchange by base URL.",

View File

@ -17,45 +17,42 @@
/** /**
* Imports. * Imports.
*/ */
import { Event, IDBDatabase } from "@gnu-taler/idb-bridge";
import { import {
describeStore, AgeCommitmentProof,
describeContents,
describeIndex,
} from "./util/query.js";
import {
AmountJson, AmountJson,
AmountString, AmountString,
ExchangeAuditor, CoinEnvelope,
CoinDepositPermission, CoinRefreshRequest,
CoinStatus,
ContractTerms, ContractTerms,
DenominationInfo,
DenominationPubKey, DenominationPubKey,
ExchangeSignKeyJson, DenomSelectionState,
EddsaPublicKeyString,
EddsaSignatureString,
ExchangeAuditor,
ExchangeGlobalFees,
InternationalizedString, InternationalizedString,
Location,
MerchantInfo, MerchantInfo,
PayCoinSelection,
PeerContractTerms,
Product, Product,
RefreshReason, RefreshReason,
TalerErrorDetail, TalerErrorDetail,
UnblindedSignature,
CoinEnvelope,
TalerProtocolTimestamp,
TalerProtocolDuration, TalerProtocolDuration,
AgeCommitmentProof, TalerProtocolTimestamp,
PayCoinSelection,
PeerContractTerms,
Location,
WireInfo,
DenominationInfo,
GlobalFees,
ExchangeGlobalFees,
DenomSelectionState,
TransactionIdStr, TransactionIdStr,
CoinRefreshRequest, UnblindedSignature,
CoinStatus, WireInfo,
EddsaPublicKeyString,
EddsaSignatureString,
} from "@gnu-taler/taler-util"; } from "@gnu-taler/taler-util";
import {
describeContents,
describeIndex,
describeStore,
} from "./util/query.js";
import { RetryInfo, RetryTags } from "./util/retries.js"; import { RetryInfo, RetryTags } from "./util/retries.js";
import { Event, IDBDatabase } from "@gnu-taler/idb-bridge";
/** /**
* This file contains the database schema of the Taler wallet together * This file contains the database schema of the Taler wallet together
@ -354,8 +351,6 @@ export interface DenominationRecord {
* Was this denomination still offered by the exchange the last time * Was this denomination still offered by the exchange the last time
* we checked? * we checked?
* Only false when the exchange redacts a previously published denomination. * Only false when the exchange redacts a previously published denomination.
*
* FIXME: Consider rolling this and isRevoked into some bitfield?
*/ */
isOffered: boolean; isOffered: boolean;
@ -526,6 +521,8 @@ export interface ExchangeRecord {
* Should usually not change. Only changes when the * Should usually not change. Only changes when the
* exchange advertises a different master public key and/or * exchange advertises a different master public key and/or
* currency. * currency.
*
* FIXME: Use a rowId here?
*/ */
detailsPointer: ExchangeDetailsPointer | undefined; detailsPointer: ExchangeDetailsPointer | undefined;
@ -1168,8 +1165,6 @@ export interface PurchaseRecord {
/** /**
* Timestamp of the first time that sending a payment to the merchant * Timestamp of the first time that sending a payment to the merchant
* for this purchase was successful. * for this purchase was successful.
*
* FIXME: Does this need to be a timestamp, doesn't boolean suffice?
*/ */
timestampFirstSuccessfulPay: TalerProtocolTimestamp | undefined; timestampFirstSuccessfulPay: TalerProtocolTimestamp | undefined;
@ -1369,6 +1364,8 @@ export interface WithdrawalGroupRecord {
/** /**
* Wire information (as payto URI) for the bank account that * Wire information (as payto URI) for the bank account that
* transferred funds for this reserve. * transferred funds for this reserve.
*
* FIXME: Doesn't this belong to the bankAccounts object store?
*/ */
senderWire?: string; senderWire?: string;
@ -1604,7 +1601,7 @@ export interface GhostDepositGroupRecord {
export interface TombstoneRecord { export interface TombstoneRecord {
/** /**
* Tombstone ID, with the syntax "<type>:<key>". * Tombstone ID, with the syntax "tmb:<type>:<key>".
*/ */
id: string; id: string;
} }

View File

@ -22,6 +22,8 @@ import {
Amounts, Amounts,
CoinRefreshRequest, CoinRefreshRequest,
CoinStatus, CoinStatus,
ExchangeEntryStatus,
ExchangeListItem,
ExchangeTosStatus, ExchangeTosStatus,
j2s, j2s,
Logger, Logger,
@ -32,7 +34,12 @@ import {
TransactionIdStr, TransactionIdStr,
TransactionType, TransactionType,
} from "@gnu-taler/taler-util"; } from "@gnu-taler/taler-util";
import { WalletStoresV1, CoinRecord, ExchangeDetailsRecord } from "../db.js"; import {
WalletStoresV1,
CoinRecord,
ExchangeDetailsRecord,
ExchangeRecord,
} from "../db.js";
import { makeErrorDetail, TalerError } from "../errors.js"; import { makeErrorDetail, TalerError } from "../errors.js";
import { InternalWalletState } from "../internal-wallet-state.js"; import { InternalWalletState } from "../internal-wallet-state.js";
import { checkDbInvariant, checkLogicInvariant } from "../util/invariants.js"; import { checkDbInvariant, checkLogicInvariant } from "../util/invariants.js";
@ -320,3 +327,29 @@ export function getExchangeTosStatus(
} }
return ExchangeTosStatus.Changed; return ExchangeTosStatus.Changed;
} }
export function makeExchangeListItem(
r: ExchangeRecord,
exchangeDetails: ExchangeDetailsRecord | undefined,
): ExchangeListItem {
if (!exchangeDetails) {
return {
exchangeBaseUrl: r.baseUrl,
currency: undefined,
tosStatus: ExchangeTosStatus.Unknown,
paytoUris: [],
exchangeStatus: ExchangeEntryStatus.Unknown,
permanent: r.permanent,
};
}
let exchangeStatus;
exchangeStatus = ExchangeEntryStatus.Ok;
return {
exchangeBaseUrl: r.baseUrl,
currency: exchangeDetails.currency,
tosStatus: getExchangeTosStatus(exchangeDetails),
paytoUris: exchangeDetails.wireInfo.accounts.map((x) => x.payto_uri),
exchangeStatus,
permanent: r.permanent,
};
}

View File

@ -705,8 +705,6 @@ export async function updateExchangeFromUrlHandler(
}; };
} }
await tx.exchanges.put(r); await tx.exchanges.put(r);
logger.info(`existing details ${j2s(existingDetails)}`);
logger.info(`inserting new details ${j2s(newDetails)}`);
const drRowId = await tx.exchangeDetails.put(newDetails); const drRowId = await tx.exchangeDetails.put(newDetails);
checkDbInvariant(typeof drRowId.key === "number"); checkDbInvariant(typeof drRowId.key === "number");

View File

@ -85,6 +85,7 @@ import { InternalWalletState } from "../internal-wallet-state.js";
import { import {
getExchangeTosStatus, getExchangeTosStatus,
makeCoinAvailable, makeCoinAvailable,
makeExchangeListItem,
runOperationWithErrorReporting, runOperationWithErrorReporting,
} from "../operations/common.js"; } from "../operations/common.js";
import { walletCoreDebugFlags } from "../util/debugFlags.js"; import { walletCoreDebugFlags } from "../util/debugFlags.js";
@ -1367,18 +1368,7 @@ export async function getWithdrawalDetailsForUri(
.iter(r.baseUrl) .iter(r.baseUrl)
.toArray(); .toArray();
if (exchangeDetails && denominations) { if (exchangeDetails && denominations) {
const tosRecord = await tx.exchangeTos.get([ exchanges.push(makeExchangeListItem(r, exchangeDetails));
exchangeDetails.exchangeBaseUrl,
exchangeDetails.tosCurrentEtag,
]);
exchanges.push({
exchangeBaseUrl: exchangeDetails.exchangeBaseUrl,
currency: exchangeDetails.currency,
paytoUris: exchangeDetails.wireInfo.accounts.map(
(x) => x.payto_uri,
),
tosStatus: getExchangeTosStatus(exchangeDetails),
});
} }
} }
}); });

View File

@ -49,7 +49,6 @@ export enum OperationAttemptResultType {
Longpoll = "longpoll", Longpoll = "longpoll",
} }
// FIXME: not part of DB!
export type OperationAttemptResult<TSuccess = unknown, TPending = unknown> = export type OperationAttemptResult<TSuccess = unknown, TPending = unknown> =
| OperationAttemptFinishedResult<TSuccess> | OperationAttemptFinishedResult<TSuccess>
| OperationAttemptErrorResult | OperationAttemptErrorResult

View File

@ -48,6 +48,7 @@ import {
CreateDepositGroupRequest, CreateDepositGroupRequest,
CreateDepositGroupResponse, CreateDepositGroupResponse,
DeleteTransactionRequest, DeleteTransactionRequest,
ExchangeDetailedResponse,
ExchangesListResponse, ExchangesListResponse,
ForceRefreshRequest, ForceRefreshRequest,
GetExchangeTosRequest, GetExchangeTosRequest,
@ -109,6 +110,7 @@ export enum WalletApiOperation {
ApplyRefund = "applyRefund", ApplyRefund = "applyRefund",
AcceptBankIntegratedWithdrawal = "acceptBankIntegratedWithdrawal", AcceptBankIntegratedWithdrawal = "acceptBankIntegratedWithdrawal",
GetExchangeTos = "getExchangeTos", GetExchangeTos = "getExchangeTos",
GetExchangeDetailedInfo = "getExchangeDetailedInfo",
RetryPendingNow = "retryPendingNow", RetryPendingNow = "retryPendingNow",
AbortFailedPayWithRefund = "abortFailedPayWithRefund", AbortFailedPayWithRefund = "abortFailedPayWithRefund",
ConfirmPay = "confirmPay", ConfirmPay = "confirmPay",
@ -333,6 +335,15 @@ export type GetExchangeTosOp = {
response: GetExchangeTosResult; response: GetExchangeTosResult;
}; };
/**
* Get the current terms of a service of an exchange.
*/
export type GetExchangeDetailedInfoOp = {
op: WalletApiOperation.GetExchangeDetailedInfo;
request: AddExchangeRequest;
response: ExchangeDetailedResponse;
};
/** /**
* List currencies known to the wallet. * List currencies known to the wallet.
*/ */
@ -661,6 +672,7 @@ export type WalletOperations = {
[WalletApiOperation.AddExchange]: AddExchangeOp; [WalletApiOperation.AddExchange]: AddExchangeOp;
[WalletApiOperation.SetExchangeTosAccepted]: SetExchangeTosAcceptedOp; [WalletApiOperation.SetExchangeTosAccepted]: SetExchangeTosAcceptedOp;
[WalletApiOperation.GetExchangeTos]: GetExchangeTosOp; [WalletApiOperation.GetExchangeTos]: GetExchangeTosOp;
[WalletApiOperation.GetExchangeDetailedInfo]: GetExchangeDetailedInfoOp;
[WalletApiOperation.TrackDepositGroup]: TrackDepositGroupOp; [WalletApiOperation.TrackDepositGroup]: TrackDepositGroupOp;
[WalletApiOperation.CreateDepositGroup]: CreateDepositGroupOp; [WalletApiOperation.CreateDepositGroup]: CreateDepositGroupOp;
[WalletApiOperation.SetWalletDeviceId]: SetWalletDeviceIdOp; [WalletApiOperation.SetWalletDeviceId]: SetWalletDeviceIdOp;

View File

@ -97,6 +97,8 @@ import {
ExchangeTosStatusDetails, ExchangeTosStatusDetails,
CoinRefreshRequest, CoinRefreshRequest,
CoinStatus, CoinStatus,
ExchangeEntryStatus,
ExchangeTosStatus,
} from "@gnu-taler/taler-util"; } from "@gnu-taler/taler-util";
import { TalerCryptoInterface } from "./crypto/cryptoImplementation.js"; import { TalerCryptoInterface } from "./crypto/cryptoImplementation.js";
import { import {
@ -146,7 +148,11 @@ import {
} 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 { getBalances } from "./operations/balance.js";
import { getExchangeTosStatus, runOperationWithErrorReporting } from "./operations/common.js"; import {
getExchangeTosStatus,
makeExchangeListItem,
runOperationWithErrorReporting,
} from "./operations/common.js";
import { import {
createDepositGroup, createDepositGroup,
getFeeForDeposit, getFeeForDeposit,
@ -645,32 +651,8 @@ async function getExchanges(
.runReadOnly(async (tx) => { .runReadOnly(async (tx) => {
const exchangeRecords = await tx.exchanges.iter().toArray(); const exchangeRecords = await tx.exchanges.iter().toArray();
for (const r of exchangeRecords) { for (const r of exchangeRecords) {
const dp = r.detailsPointer;
if (!dp) {
continue;
}
const { currency } = dp;
const exchangeDetails = await getExchangeDetails(tx, r.baseUrl); const exchangeDetails = await getExchangeDetails(tx, r.baseUrl);
if (!exchangeDetails) { exchanges.push(makeExchangeListItem(r, exchangeDetails));
continue;
}
const denominations = await tx.denominations.indexes.byExchangeBaseUrl
.iter(r.baseUrl)
.toArray();
if (!denominations) {
continue;
}
const tos = await getExchangeTosStatusDetails(tx, exchangeDetails);
exchanges.push({
exchangeBaseUrl: r.baseUrl,
currency,
tosStatus: getExchangeTosStatus(exchangeDetails),
paytoUris: exchangeDetails.wireInfo.accounts.map((x) => x.payto_uri),
});
} }
}); });
return { exchanges }; return { exchanges };

View File

@ -185,7 +185,11 @@ export function SelectCurrency({
); );
} }
const list: Record<string, string> = {}; const list: Record<string, string> = {};
hook.response.exchanges.forEach((e) => (list[e.currency] = e.currency)); hook.response.exchanges.forEach((e) => {
if (e.currency) {
list[e.currency] = e.currency;
}
});
list[""] = "Select a currency"; list[""] = "Select a currency";
return <SelectCurrencyView onChange={onChange} list={list} />; return <SelectCurrencyView onChange={onChange} list={list} />;
} }

View File

@ -112,7 +112,7 @@ export function ManualWithdrawPage({ amount, onCancel }: Props): VNode {
const exchangeList = state.response.exchanges.reduce( const exchangeList = state.response.exchanges.reduce(
(p, c) => ({ (p, c) => ({
...p, ...p,
[c.exchangeBaseUrl]: c.currency, [c.exchangeBaseUrl]: c.currency || "??",
}), }),
{} as Record<string, string>, {} as Record<string, string>,
); );

View File

@ -204,6 +204,14 @@ export function SettingsView({
<i18n.Translate>not accepted</i18n.Translate> <i18n.Translate>not accepted</i18n.Translate>
</DestructiveText> </DestructiveText>
); );
case ExchangeTosStatus.Unknown:
return (
<DestructiveText>
<i18n.Translate>
unknown (exchange status should be updated)
</i18n.Translate>
</DestructiveText>
);
} }
} }
return ( return (