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",
Changed = "changed",
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 {
exchangeBaseUrl: string;
currency: string;
currency: string | undefined;
paytoUris: string[];
tosStatus: ExchangeTosStatus;
exchangeStatus: ExchangeEntryStatus;
/**
* Permanently added to the wallet, as opposed to just
* temporarily queried.
*/
permanent: boolean;
}
const codecForAuditorDenomSig = (): Codec<AuditorDenomSig> =>
@ -984,6 +998,8 @@ export const codecForExchangeListItem = (): Codec<ExchangeListItem> =>
.property("exchangeBaseUrl", codecForString())
.property("paytoUris", codecForList(codecForString()))
.property("tosStatus", codecForAny())
.property("exchangeStatus", codecForAny())
.property("permanent", codecForBoolean())
.build("ExchangeListItem");
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
.subcommand("exchangesAddCmd", "add", {
help: "Add an exchange by base URL.",

View File

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

View File

@ -22,6 +22,8 @@ import {
Amounts,
CoinRefreshRequest,
CoinStatus,
ExchangeEntryStatus,
ExchangeListItem,
ExchangeTosStatus,
j2s,
Logger,
@ -32,7 +34,12 @@ import {
TransactionIdStr,
TransactionType,
} 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 { InternalWalletState } from "../internal-wallet-state.js";
import { checkDbInvariant, checkLogicInvariant } from "../util/invariants.js";
@ -320,3 +327,29 @@ export function getExchangeTosStatus(
}
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);
logger.info(`existing details ${j2s(existingDetails)}`);
logger.info(`inserting new details ${j2s(newDetails)}`);
const drRowId = await tx.exchangeDetails.put(newDetails);
checkDbInvariant(typeof drRowId.key === "number");

View File

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

View File

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

View File

@ -48,6 +48,7 @@ import {
CreateDepositGroupRequest,
CreateDepositGroupResponse,
DeleteTransactionRequest,
ExchangeDetailedResponse,
ExchangesListResponse,
ForceRefreshRequest,
GetExchangeTosRequest,
@ -109,6 +110,7 @@ export enum WalletApiOperation {
ApplyRefund = "applyRefund",
AcceptBankIntegratedWithdrawal = "acceptBankIntegratedWithdrawal",
GetExchangeTos = "getExchangeTos",
GetExchangeDetailedInfo = "getExchangeDetailedInfo",
RetryPendingNow = "retryPendingNow",
AbortFailedPayWithRefund = "abortFailedPayWithRefund",
ConfirmPay = "confirmPay",
@ -333,6 +335,15 @@ export type GetExchangeTosOp = {
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.
*/
@ -661,6 +672,7 @@ export type WalletOperations = {
[WalletApiOperation.AddExchange]: AddExchangeOp;
[WalletApiOperation.SetExchangeTosAccepted]: SetExchangeTosAcceptedOp;
[WalletApiOperation.GetExchangeTos]: GetExchangeTosOp;
[WalletApiOperation.GetExchangeDetailedInfo]: GetExchangeDetailedInfoOp;
[WalletApiOperation.TrackDepositGroup]: TrackDepositGroupOp;
[WalletApiOperation.CreateDepositGroup]: CreateDepositGroupOp;
[WalletApiOperation.SetWalletDeviceId]: SetWalletDeviceIdOp;

View File

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

View File

@ -185,7 +185,11 @@ export function SelectCurrency({
);
}
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";
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(
(p, c) => ({
...p,
[c.exchangeBaseUrl]: c.currency,
[c.exchangeBaseUrl]: c.currency || "??",
}),
{} as Record<string, string>,
);

View File

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