listExchangesDetailed to getExchangeDetailedInfo & ageRestriction taken from the denoms

This commit is contained in:
Sebastian 2022-09-06 17:17:44 -03:00
parent 49c9279c1e
commit 1e00724a0d
No known key found for this signature in database
GPG Key ID: BE4FF68352439FC1
22 changed files with 157 additions and 120 deletions

View File

@ -190,7 +190,6 @@ export interface TransactionWithdrawal extends TransactionCommon {
export interface PeerInfoShort { export interface PeerInfoShort {
expiration: TalerProtocolTimestamp | undefined; expiration: TalerProtocolTimestamp | undefined;
summary: string | undefined; summary: string | undefined;
completed: boolean;
} }
/** /**

View File

@ -568,12 +568,12 @@ export interface DepositInfo {
ageCommitmentProof?: AgeCommitmentProof; ageCommitmentProof?: AgeCommitmentProof;
} }
export interface ExchangesListRespose { export interface ExchangesListResponse {
exchanges: ExchangeListItem[]; exchanges: ExchangeListItem[];
} }
export interface ExchangeDetailledListRespose { export interface ExchangeDetailedResponse {
exchanges: ExchangeFullDetailsListItem[]; exchange: ExchangeFullDetails;
} }
export interface WalletCoreVersion { export interface WalletCoreVersion {
@ -733,7 +733,7 @@ export interface DenominationInfo {
stampExpireDeposit: TalerProtocolTimestamp; stampExpireDeposit: TalerProtocolTimestamp;
} }
export interface ExchangeFullDetailsListItem { export interface ExchangeFullDetails {
exchangeBaseUrl: string; exchangeBaseUrl: string;
currency: string; currency: string;
paytoUris: string[]; paytoUris: string[];
@ -771,9 +771,9 @@ const codecForExchangeTos = (): Codec<ExchangeTos> =>
.property("content", codecOptional(codecForString())) .property("content", codecOptional(codecForString()))
.build("ExchangeTos"); .build("ExchangeTos");
export const codecForExchangeFullDetailsListItem = export const codecForExchangeFullDetails =
(): Codec<ExchangeFullDetailsListItem> => (): Codec<ExchangeFullDetails> =>
buildCodecForObject<ExchangeFullDetailsListItem>() buildCodecForObject<ExchangeFullDetails>()
.property("currency", codecForString()) .property("currency", codecForString())
.property("exchangeBaseUrl", codecForString()) .property("exchangeBaseUrl", codecForString())
.property("paytoUris", codecForList(codecForString())) .property("paytoUris", codecForList(codecForString()))
@ -791,10 +791,10 @@ export const codecForExchangeListItem = (): Codec<ExchangeListItem> =>
.property("tos", codecForExchangeTos()) .property("tos", codecForExchangeTos())
.build("ExchangeListItem"); .build("ExchangeListItem");
export const codecForExchangesListResponse = (): Codec<ExchangesListRespose> => export const codecForExchangesListResponse = (): Codec<ExchangesListResponse> =>
buildCodecForObject<ExchangesListRespose>() buildCodecForObject<ExchangesListResponse>()
.property("exchanges", codecForList(codecForExchangeFullDetailsListItem())) .property("exchanges", codecForList(codecForExchangeListItem()))
.build("ExchangesListRespose"); .build("ExchangesListResponse");
export interface AcceptManualWithdrawalResult { export interface AcceptManualWithdrawalResult {
/** /**
@ -965,6 +965,7 @@ export const codecForGetWithdrawalDetailsForAmountRequest =
buildCodecForObject<GetWithdrawalDetailsForAmountRequest>() buildCodecForObject<GetWithdrawalDetailsForAmountRequest>()
.property("exchangeBaseUrl", codecForString()) .property("exchangeBaseUrl", codecForString())
.property("amount", codecForString()) .property("amount", codecForString())
.property("restrictAge", codecOptional(codecForNumber()))
.build("GetWithdrawalDetailsForAmountRequest"); .build("GetWithdrawalDetailsForAmountRequest");
export interface AcceptExchangeTosRequest { export interface AcceptExchangeTosRequest {
@ -1022,6 +1023,7 @@ export interface GetExchangeWithdrawalInfo {
exchangeBaseUrl: string; exchangeBaseUrl: string;
amount: AmountJson; amount: AmountJson;
tosAcceptedFormat?: string[]; tosAcceptedFormat?: string[];
ageRestricted?: number;
} }
export const codecForGetExchangeWithdrawalInfo = export const codecForGetExchangeWithdrawalInfo =

View File

@ -82,10 +82,15 @@ export async function prepareTip(
logger.trace("new tip, creating tip record"); logger.trace("new tip, creating tip record");
await updateExchangeFromUrl(ws, tipPickupStatus.exchange_url); await updateExchangeFromUrl(ws, tipPickupStatus.exchange_url);
//FIXME: is this needed? withdrawDetails is not used
// * if the intention is to update the exchange information in the database
// maybe we can use another name. `get` seems like a pure-function
const withdrawDetails = await getExchangeWithdrawalInfo( const withdrawDetails = await getExchangeWithdrawalInfo(
ws, ws,
tipPickupStatus.exchange_url, tipPickupStatus.exchange_url,
amount, amount,
undefined
); );
const walletTipId = encodeCrock(getRandomBytes(32)); const walletTipId = encodeCrock(getRandomBytes(32));

View File

@ -161,7 +161,6 @@ export async function getTransactions(
info: { info: {
expiration: pi.contractTerms.purse_expiration, expiration: pi.contractTerms.purse_expiration,
summary: pi.contractTerms.summary, summary: pi.contractTerms.summary,
completed: Amounts.isZero(amount),
}, },
frozen: false, frozen: false,
pending: !pi.purseCreated, pending: !pi.purseCreated,
@ -199,7 +198,6 @@ export async function getTransactions(
info: { info: {
expiration: pi.contractTerms.purse_expiration, expiration: pi.contractTerms.purse_expiration,
summary: pi.contractTerms.summary, summary: pi.contractTerms.summary,
completed: pi.paid
}, },
timestamp: pi.timestampCreated, timestamp: pi.timestampCreated,
transactionId: makeEventId( transactionId: makeEventId(
@ -234,7 +232,6 @@ export async function getTransactions(
info: { info: {
expiration: wsr.wgInfo.contractTerms.purse_expiration, expiration: wsr.wgInfo.contractTerms.purse_expiration,
summary: wsr.wgInfo.contractTerms.summary, summary: wsr.wgInfo.contractTerms.summary,
completed: !!wsr.timestampFinish
}, },
talerUri: constructPayPullUri({ talerUri: constructPayPullUri({
exchangeBaseUrl: wsr.exchangeBaseUrl, exchangeBaseUrl: wsr.exchangeBaseUrl,
@ -259,7 +256,6 @@ export async function getTransactions(
info: { info: {
expiration: wsr.wgInfo.contractTerms.purse_expiration, expiration: wsr.wgInfo.contractTerms.purse_expiration,
summary: wsr.wgInfo.contractTerms.summary, summary: wsr.wgInfo.contractTerms.summary,
completed: !!wsr.timestampFinish,
}, },
pending: !wsr.timestampFinish, pending: !wsr.timestampFinish,
timestamp: wsr.timestampStart, timestamp: wsr.timestampStart,

View File

@ -192,6 +192,13 @@ export interface ExchangeWithdrawDetails {
* Amount that will actually be added to the wallet's balance. * Amount that will actually be added to the wallet's balance.
*/ */
withdrawalAmountEffective: AmountString; withdrawalAmountEffective: AmountString;
/**
* If the exchange supports age-restricted coins it will return
* the array of ages.
*
*/
ageRestrictionOptions?: number[],
} }
/** /**
@ -242,7 +249,7 @@ export function selectWithdrawalDenominations(
for (const d of denoms) { for (const d of denoms) {
let count = 0; let count = 0;
const cost = Amounts.add(d.value, d.feeWithdraw).amount; const cost = Amounts.add(d.value, d.feeWithdraw).amount;
for (;;) { for (; ;) {
if (Amounts.cmp(remaining, cost) < 0) { if (Amounts.cmp(remaining, cost) < 0) {
break; break;
} }
@ -903,8 +910,7 @@ export async function updateWithdrawalDenoms(
denom.verificationStatus === DenominationVerificationStatus.Unverified denom.verificationStatus === DenominationVerificationStatus.Unverified
) { ) {
logger.trace( logger.trace(
`Validating denomination (${current + 1}/${ `Validating denomination (${current + 1}/${denominations.length
denominations.length
}) signature of ${denom.denomPubHash}`, }) signature of ${denom.denomPubHash}`,
); );
let valid = false; let valid = false;
@ -1031,7 +1037,7 @@ async function queryReserve(
if ( if (
resp.status === 404 && resp.status === 404 &&
result.talerErrorResponse.code === result.talerErrorResponse.code ===
TalerErrorCode.EXCHANGE_RESERVES_STATUS_UNKNOWN TalerErrorCode.EXCHANGE_RESERVES_STATUS_UNKNOWN
) { ) {
ws.notify({ ws.notify({
type: NotificationType.ReserveNotYetFound, type: NotificationType.ReserveNotYetFound,
@ -1255,10 +1261,13 @@ async function processWithdrawGroupImpl(
} }
} }
const AGE_MASK_GROUPS = "8:10:12:14:16:18".split(":").map(n => parseInt(n, 10))
export async function getExchangeWithdrawalInfo( export async function getExchangeWithdrawalInfo(
ws: InternalWalletState, ws: InternalWalletState,
exchangeBaseUrl: string, exchangeBaseUrl: string,
instructedAmount: AmountJson, instructedAmount: AmountJson,
ageRestricted: number | undefined,
): Promise<ExchangeWithdrawDetails> { ): Promise<ExchangeWithdrawDetails> {
const { exchange, exchangeDetails } = const { exchange, exchangeDetails } =
await ws.exchangeOps.updateExchangeFromUrl(ws, exchangeBaseUrl); await ws.exchangeOps.updateExchangeFromUrl(ws, exchangeBaseUrl);
@ -1287,6 +1296,8 @@ export async function getExchangeWithdrawalInfo(
exchange, exchange,
); );
let hasDenomWithAgeRestriction = false
let earliestDepositExpiration: TalerProtocolTimestamp | undefined; let earliestDepositExpiration: TalerProtocolTimestamp | undefined;
for (let i = 0; i < selectedDenoms.selectedDenoms.length; i++) { for (let i = 0; i < selectedDenoms.selectedDenoms.length; i++) {
const ds = selectedDenoms.selectedDenoms[i]; const ds = selectedDenoms.selectedDenoms[i];
@ -1310,6 +1321,7 @@ export async function getExchangeWithdrawalInfo(
) { ) {
earliestDepositExpiration = expireDeposit; earliestDepositExpiration = expireDeposit;
} }
hasDenomWithAgeRestriction = hasDenomWithAgeRestriction || denom.denomPub.age_mask > 0
} }
checkLogicInvariant(!!earliestDepositExpiration); checkLogicInvariant(!!earliestDepositExpiration);
@ -1337,7 +1349,7 @@ export async function getExchangeWithdrawalInfo(
) { ) {
logger.warn( logger.warn(
`wallet's support for exchange protocol version ${WALLET_EXCHANGE_PROTOCOL_VERSION} might be outdated ` + `wallet's support for exchange protocol version ${WALLET_EXCHANGE_PROTOCOL_VERSION} might be outdated ` +
`(exchange has ${exchangeDetails.protocolVersion}), checking for updates`, `(exchange has ${exchangeDetails.protocolVersion}), checking for updates`,
); );
} }
} }
@ -1370,6 +1382,9 @@ export async function getExchangeWithdrawalInfo(
termsOfServiceAccepted: tosAccepted, termsOfServiceAccepted: tosAccepted,
withdrawalAmountEffective: Amounts.stringify(selectedDenoms.totalCoinValue), withdrawalAmountEffective: Amounts.stringify(selectedDenoms.totalCoinValue),
withdrawalAmountRaw: Amounts.stringify(instructedAmount), withdrawalAmountRaw: Amounts.stringify(instructedAmount),
// TODO: remove hardcoding, this should be calculated from the denominations info
// force enabled for testing
ageRestrictionOptions: hasDenomWithAgeRestriction ? AGE_MASK_GROUPS : undefined
}; };
return ret; return ret;
} }

View File

@ -46,7 +46,7 @@ import {
CreateDepositGroupRequest, CreateDepositGroupRequest,
CreateDepositGroupResponse, CreateDepositGroupResponse,
DeleteTransactionRequest, DeleteTransactionRequest,
ExchangesListRespose, ExchangesListResponse,
ForceRefreshRequest, ForceRefreshRequest,
GetExchangeTosRequest, GetExchangeTosRequest,
GetExchangeTosResult, GetExchangeTosResult,
@ -227,7 +227,7 @@ export type WalletOperations = {
}; };
[WalletApiOperation.ListExchanges]: { [WalletApiOperation.ListExchanges]: {
request: {}; request: {};
response: ExchangesListRespose; response: ExchangesListResponse;
}; };
[WalletApiOperation.AddExchange]: { [WalletApiOperation.AddExchange]: {
request: AddExchangeRequest; request: AddExchangeRequest;

View File

@ -72,8 +72,8 @@ import {
Duration, Duration,
durationFromSpec, durationFromSpec,
durationMin, durationMin,
ExchangeFullDetailsListItem, ExchangeFullDetails,
ExchangesListRespose, ExchangesListResponse,
GetExchangeTosResult, GetExchangeTosResult,
j2s, j2s,
KnownBankAccounts, KnownBankAccounts,
@ -232,8 +232,9 @@ async function getWithdrawalDetailsForAmount(
ws: InternalWalletState, ws: InternalWalletState,
exchangeBaseUrl: string, exchangeBaseUrl: string,
amount: AmountJson, amount: AmountJson,
restrictAge: number | undefined,
): Promise<ManualWithdrawalDetails> { ): Promise<ManualWithdrawalDetails> {
const wi = await getExchangeWithdrawalInfo(ws, exchangeBaseUrl, amount); const wi = await getExchangeWithdrawalInfo(ws, exchangeBaseUrl, amount, restrictAge);
const paytoUris = wi.exchangeDetails.wireInfo.accounts.map( const paytoUris = wi.exchangeDetails.wireInfo.accounts.map(
(x) => x.payto_uri, (x) => x.payto_uri,
); );
@ -568,7 +569,7 @@ async function listKnownBankAccounts(
async function getExchanges( async function getExchanges(
ws: InternalWalletState, ws: InternalWalletState,
): Promise<ExchangesListRespose> { ): Promise<ExchangesListResponse> {
const exchanges: ExchangeListItem[] = []; const exchanges: ExchangeListItem[] = [];
await ws.db await ws.db
.mktx((x) => ({ .mktx((x) => ({
@ -613,54 +614,56 @@ async function getExchanges(
return { exchanges }; return { exchanges };
} }
async function getExchangesDetailled( async function getExchangeDetailedInfo(
ws: InternalWalletState, ws: InternalWalletState,
): Promise<ExchangesListRespose> { exchangeBaseurl: string,
const exchanges: ExchangeFullDetailsListItem[] = []; ): Promise<ExchangeFullDetails> {
await ws.db //TODO: should we use the forceUpdate parameter?
const exchange = await ws.db
.mktx((x) => ({ .mktx((x) => ({
exchanges: x.exchanges, exchanges: x.exchanges,
exchangeDetails: x.exchangeDetails, exchangeDetails: x.exchangeDetails,
denominations: x.denominations, denominations: x.denominations,
})) }))
.runReadOnly(async (tx) => { .runReadOnly(async (tx) => {
const exchangeRecords = await tx.exchanges.iter().toArray(); const ex = await tx.exchanges.get(exchangeBaseurl)
for (const r of exchangeRecords) { const dp = ex?.detailsPointer;
const dp = r.detailsPointer; if (!dp) {
if (!dp) { return;
continue; }
} const { currency } = dp;
const { currency } = dp; const exchangeDetails = await getExchangeDetails(tx, ex.baseUrl);
const exchangeDetails = await getExchangeDetails(tx, r.baseUrl); if (!exchangeDetails) {
if (!exchangeDetails) { return;
continue; }
}
const denominations = await tx.denominations.indexes.byExchangeBaseUrl const denominations = await tx.denominations.indexes.byExchangeBaseUrl
.iter(r.baseUrl) .iter(ex.baseUrl)
.toArray(); .toArray();
if (!denominations) { if (!denominations) {
continue; return;
} }
exchanges.push({ return {
exchangeBaseUrl: r.baseUrl, exchangeBaseUrl: ex.baseUrl,
currency, currency,
tos: { tos: {
acceptedVersion: exchangeDetails.termsOfServiceAcceptedEtag, acceptedVersion: exchangeDetails.termsOfServiceAcceptedEtag,
currentVersion: exchangeDetails.termsOfServiceLastEtag, currentVersion: exchangeDetails.termsOfServiceLastEtag,
contentType: exchangeDetails.termsOfServiceContentType, contentType: exchangeDetails.termsOfServiceContentType,
content: exchangeDetails.termsOfServiceText, content: exchangeDetails.termsOfServiceText,
}, },
paytoUris: exchangeDetails.wireInfo.accounts.map((x) => x.payto_uri), paytoUris: exchangeDetails.wireInfo.accounts.map((x) => x.payto_uri),
auditors: exchangeDetails.auditors, auditors: exchangeDetails.auditors,
wireInfo: exchangeDetails.wireInfo, wireInfo: exchangeDetails.wireInfo,
denominations: denominations, denominations: denominations,
});
} }
}); });
return { exchanges }; if (!exchange) {
throw Error(`exchange with base url "${exchangeBaseurl}" not found`)
}
return exchange;
} }
async function setCoinSuspended( async function setCoinSuspended(
@ -834,8 +837,9 @@ async function dispatchRequestInternal(
case "listExchanges": { case "listExchanges": {
return await getExchanges(ws); return await getExchanges(ws);
} }
case "listExchangesDetailled": { case "getExchangeDetailedInfo": {
return await getExchangesDetailled(ws); const req = codecForAddExchangeRequest().decode(payload);
return await getExchangeDetailedInfo(ws, req.exchangeBaseUrl);
} }
case "listKnownBankAccounts": { case "listKnownBankAccounts": {
const req = codecForListKnownBankAccounts().decode(payload); const req = codecForListKnownBankAccounts().decode(payload);
@ -852,6 +856,7 @@ async function dispatchRequestInternal(
ws, ws,
req.exchangeBaseUrl, req.exchangeBaseUrl,
req.amount, req.amount,
req.ageRestricted,
); );
} }
case "acceptManualWithdrawal": { case "acceptManualWithdrawal": {
@ -870,6 +875,7 @@ async function dispatchRequestInternal(
ws, ws,
req.exchangeBaseUrl, req.exchangeBaseUrl,
Amounts.parseOrThrow(req.amount), Amounts.parseOrThrow(req.amount),
req.restrictAge
); );
} }
case "getBalances": { case "getBalances": {
@ -1067,6 +1073,7 @@ async function dispatchRequestInternal(
ws, ws,
req.exchange, req.exchange,
amount, amount,
undefined
); );
const wres = await createManualWithdrawal(ws, { const wres = await createManualWithdrawal(ws, {
amount: amount, amount: amount,

View File

@ -30,7 +30,7 @@ export function useComponentState(
const [subject, setSubject] = useState(""); const [subject, setSubject] = useState("");
const [talerUri, setTalerUri] = useState("") const [talerUri, setTalerUri] = useState("")
const hook = useAsyncAsHook(api.listExchangesDetailled); const hook = useAsyncAsHook(api.listExchanges);
const [exchangeIdx, setExchangeIdx] = useState("0") const [exchangeIdx, setExchangeIdx] = useState("0")
const [operationError, setOperationError] = useState<TalerErrorDetail | undefined>(undefined) const [operationError, setOperationError] = useState<TalerErrorDetail | undefined>(undefined)

View File

@ -30,7 +30,7 @@ export function useComponentStateFromParams(
const [ageRestricted, setAgeRestricted] = useState(0); const [ageRestricted, setAgeRestricted] = useState(0);
const exchangeHook = useAsyncAsHook(api.listExchangesDetailled); const exchangeHook = useAsyncAsHook(api.listExchanges);
const exchangeHookDep = const exchangeHookDep =
!exchangeHook || exchangeHook.hasError || !exchangeHook.response !exchangeHook || exchangeHook.hasError || !exchangeHook.response
@ -65,6 +65,7 @@ export function useComponentStateFromParams(
exchangeBaseUrl: exchange.exchangeBaseUrl, exchangeBaseUrl: exchange.exchangeBaseUrl,
amount: chosenAmount, amount: chosenAmount,
tosAcceptedFormat: ["text/xml"], tosAcceptedFormat: ["text/xml"],
ageRestricted,
}); });
const withdrawAmount = { const withdrawAmount = {
@ -72,7 +73,7 @@ export function useComponentStateFromParams(
effective: Amounts.parseOrThrow(info.withdrawalAmountEffective), effective: Amounts.parseOrThrow(info.withdrawalAmountEffective),
} }
return { amount: withdrawAmount }; return { amount: withdrawAmount, ageRestrictionOptions: info.ageRestrictionOptions };
}, [exchangeHookDep]); }, [exchangeHookDep]);
const [reviewing, setReviewing] = useState<boolean>(false); const [reviewing, setReviewing] = useState<boolean>(false);
@ -172,16 +173,16 @@ export function useComponentStateFromParams(
termsState !== undefined && termsState !== undefined &&
(termsState.status === "changed" || termsState.status === "new"); (termsState.status === "changed" || termsState.status === "new");
const ageRestrictionOptions: Record<string, string> | undefined = "6:12:18" const ageRestrictionOptions = amountHook.response.
.split(":") ageRestrictionOptions?.
.reduce((p, c) => ({ ...p, [c]: `under ${c}` }), {}); reduce((p, c) => ({ ...p, [c]: `under ${c}` }), {} as Record<string, string>)
if (ageRestrictionOptions) { const ageRestrictionEnabled = ageRestrictionOptions !== undefined
if (ageRestrictionEnabled) {
ageRestrictionOptions["0"] = "Not restricted"; ageRestrictionOptions["0"] = "Not restricted";
} }
//TODO: calculate based on exchange info //TODO: calculate based on exchange info
const ageRestrictionEnabled = false;
const ageRestriction = ageRestrictionEnabled ? { const ageRestriction = ageRestrictionEnabled ? {
list: ageRestrictionOptions, list: ageRestrictionOptions,
value: String(ageRestricted), value: String(ageRestricted),
@ -269,6 +270,7 @@ export function useComponentStateFromURI(
exchangeBaseUrl: uriHookDep?.thisExchange, exchangeBaseUrl: uriHookDep?.thisExchange,
amount: Amounts.parseOrThrow(uriHookDep.amount), amount: Amounts.parseOrThrow(uriHookDep.amount),
tosAcceptedFormat: ["text/xml"], tosAcceptedFormat: ["text/xml"],
ageRestricted,
}); });
const withdrawAmount = { const withdrawAmount = {
@ -276,7 +278,7 @@ export function useComponentStateFromURI(
effective: Amounts.parseOrThrow(info.withdrawalAmountEffective), effective: Amounts.parseOrThrow(info.withdrawalAmountEffective),
} }
return { amount: withdrawAmount }; return { amount: withdrawAmount, ageRestrictionOptions: info.ageRestrictionOptions };
}, [uriHookDep]); }, [uriHookDep]);
const [reviewing, setReviewing] = useState<boolean>(false); const [reviewing, setReviewing] = useState<boolean>(false);
@ -385,16 +387,16 @@ export function useComponentStateFromURI(
termsState !== undefined && termsState !== undefined &&
(termsState.status === "changed" || termsState.status === "new"); (termsState.status === "changed" || termsState.status === "new");
const ageRestrictionOptions: Record<string, string> | undefined = "6:12:18" const ageRestrictionOptions = amountHook.response.
.split(":") ageRestrictionOptions?.
.reduce((p, c) => ({ ...p, [c]: `under ${c}` }), {}); reduce((p, c) => ({ ...p, [c]: `under ${c}` }), {} as Record<string, string>)
if (ageRestrictionOptions) { const ageRestrictionEnabled = ageRestrictionOptions !== undefined
if (ageRestrictionEnabled) {
ageRestrictionOptions["0"] = "Not restricted"; ageRestrictionOptions["0"] = "Not restricted";
} }
//TODO: calculate based on exchange info //TODO: calculate based on exchange info
const ageRestrictionEnabled = false;
const ageRestriction = ageRestrictionEnabled ? { const ageRestriction = ageRestrictionEnabled ? {
list: ageRestrictionOptions, list: ageRestrictionOptions,
value: String(ageRestricted), value: String(ageRestricted),

View File

@ -21,7 +21,7 @@
import { import {
Amounts, Amounts,
ExchangeFullDetailsListItem, ExchangeFullDetails,
ExchangeListItem, ExchangeListItem,
GetExchangeTosResult, GetExchangeTosResult,
} from "@gnu-taler/taler-util"; } from "@gnu-taler/taler-util";
@ -30,7 +30,7 @@ import { expect } from "chai";
import { mountHook } from "../../test-utils.js"; import { mountHook } from "../../test-utils.js";
import { useComponentStateFromURI } from "./state.js"; import { useComponentStateFromURI } from "./state.js";
const exchanges: ExchangeFullDetailsListItem[] = [ const exchanges: ExchangeFullDetails[] = [
{ {
currency: "ARS", currency: "ARS",
exchangeBaseUrl: "http://exchange.demo.taler.net", exchangeBaseUrl: "http://exchange.demo.taler.net",

View File

@ -171,7 +171,7 @@ export function SelectCurrency({
}): VNode { }): VNode {
const { i18n } = useTranslationContext(); const { i18n } = useTranslationContext();
const hook = useAsyncAsHook(wxApi.listExchangesDetailled); const hook = useAsyncAsHook(wxApi.listExchanges);
if (!hook) { if (!hook) {
return <Loading />; return <Loading />;

View File

@ -45,7 +45,7 @@ export function DeveloperPage(): VNode {
const response = useAsyncAsHook(async () => { const response = useAsyncAsHook(async () => {
const op = await wxApi.getPendingOperations(); const op = await wxApi.getPendingOperations();
const c = await wxApi.dumpCoins(); const c = await wxApi.dumpCoins();
const ex = await wxApi.listExchangesDetailled(); const ex = await wxApi.listExchanges();
return { return {
operations: op.pendingOperations, operations: op.pendingOperations,
coins: c.coins, coins: c.coins,

View File

@ -36,7 +36,7 @@ export function ExchangeAddPage({ currency, onBack }: Props): VNode {
{ url: string; config: TalerConfigResponse } | undefined { url: string; config: TalerConfigResponse } | undefined
>(undefined); >(undefined);
const knownExchangesResponse = useAsyncAsHook(wxApi.listExchangesDetailled); const knownExchangesResponse = useAsyncAsHook(wxApi.listExchanges);
const knownExchanges = !knownExchangesResponse const knownExchanges = !knownExchangesResponse
? [] ? []
: knownExchangesResponse.hasError : knownExchangesResponse.hasError

View File

@ -15,7 +15,7 @@
*/ */
import { import {
ExchangeFullDetailsListItem, ExchangeFullDetails,
ExchangeListItem, ExchangeListItem,
} from "@gnu-taler/taler-util"; } from "@gnu-taler/taler-util";
@ -24,7 +24,7 @@ import {
* @author Sebastian Javier Marchano (sebasjm) * @author Sebastian Javier Marchano (sebasjm)
*/ */
export const bitcoinExchanges: ExchangeFullDetailsListItem[] = [ export const bitcoinExchanges: ExchangeFullDetails[] = [
{ {
exchangeBaseUrl: "https://bitcoin1.ice.bfh.ch/", exchangeBaseUrl: "https://bitcoin1.ice.bfh.ch/",
currency: "BITCOINBTC", currency: "BITCOINBTC",
@ -11781,7 +11781,7 @@ export const bitcoinExchanges: ExchangeFullDetailsListItem[] = [
}, },
] as any; ] as any;
export const kudosExchanges: ExchangeFullDetailsListItem[] = [ export const kudosExchanges: ExchangeFullDetails[] = [
{ {
exchangeBaseUrl: "https://exchange1.demo.taler.net/", exchangeBaseUrl: "https://exchange1.demo.taler.net/",
currency: "KUDOS", currency: "KUDOS",

View File

@ -14,7 +14,7 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/ */
import { AbsoluteTime, AmountJson, ExchangeFullDetailsListItem } from "@gnu-taler/taler-util"; import { AbsoluteTime, AmountJson, ExchangeFullDetails } from "@gnu-taler/taler-util";
import { Loading } from "../../components/Loading.js"; import { Loading } from "../../components/Loading.js";
import { HookError } from "../../hooks/useAsyncAsHook.js"; import { HookError } from "../../hooks/useAsyncAsHook.js";
import { ButtonHandler, SelectFieldHandler } from "../../mui/handlers.js"; import { ButtonHandler, SelectFieldHandler } from "../../mui/handlers.js";
@ -52,7 +52,7 @@ export namespace State {
export interface BaseInfo { export interface BaseInfo {
exchanges: SelectFieldHandler; exchanges: SelectFieldHandler;
selected: ExchangeFullDetailsListItem; selected: ExchangeFullDetails;
nextFeeUpdate: AbsoluteTime; nextFeeUpdate: AbsoluteTime;
error: undefined; error: undefined;
} }

View File

@ -25,11 +25,21 @@ export function useComponentState(
{ onCancel, onSelection, currency }: Props, { onCancel, onSelection, currency }: Props,
api: typeof wxApi, api: typeof wxApi,
): State { ): State {
const hook = useAsyncAsHook(api.listExchangesDetailled);
const initialValue = 0 const initialValue = 0
const [value, setValue] = useState(String(initialValue)); const [value, setValue] = useState(String(initialValue));
const hook = useAsyncAsHook(async () => {
const { exchanges } = await api.listExchanges()
const selectedIdx = parseInt(value, 10)
const selectedExchange = exchanges.length == 0 ? undefined : exchanges[selectedIdx]
const selected = !selectedExchange ? undefined : await api.getExchangeDetailedInfo(selectedExchange.exchangeBaseUrl)
const initialExchange = selectedIdx === initialValue ? undefined : exchanges[initialValue]
const original = !initialExchange ? undefined : await api.getExchangeDetailedInfo(initialExchange.exchangeBaseUrl)
return { exchanges, selected, original }
});
if (!hook) { if (!hook) {
return { return {
status: "loading", status: "loading",
@ -43,18 +53,16 @@ export function useComponentState(
}; };
} }
const exchanges = hook.response.exchanges; const { exchanges, selected, original } = hook.response;
if (exchanges.length === 0) { if (!selected) {
//!selected <=> exchanges.length === 0
return { return {
status: "no-exchanges", status: "no-exchanges",
error: undefined error: undefined
} }
} }
const original = exchanges[initialValue];
const selected = exchanges[Number(value)];
let nextFeeUpdate = TalerProtocolTimestamp.never(); let nextFeeUpdate = TalerProtocolTimestamp.never();
nextFeeUpdate = Object.values(selected.wireInfo.feesForType).reduce( nextFeeUpdate = Object.values(selected.wireInfo.feesForType).reduce(
@ -97,7 +105,8 @@ export function useComponentState(
const exchangeMap = exchanges.reduce((prev, cur, idx) => ({ ...prev, [cur.exchangeBaseUrl]: String(idx) }), {} as Record<string, string>) const exchangeMap = exchanges.reduce((prev, cur, idx) => ({ ...prev, [cur.exchangeBaseUrl]: String(idx) }), {} as Record<string, string>)
if (original === selected) { if (!original) {
// !original <=> selected == original
return { return {
status: "ready", status: "ready",
exchanges: { exchanges: {

View File

@ -19,7 +19,7 @@
* @author Sebastian Javier Marchano (sebasjm) * @author Sebastian Javier Marchano (sebasjm)
*/ */
import { ExchangeFullDetailsListItem, ExchangeListItem } from "@gnu-taler/taler-util"; import { ExchangeFullDetails, ExchangeListItem } from "@gnu-taler/taler-util";
import { createExample } from "../../test-utils.js"; import { createExample } from "../../test-utils.js";
import { bitcoinExchanges, kudosExchanges } from "./example.js"; import { bitcoinExchanges, kudosExchanges } from "./example.js";
import { FeeDescription, FeeDescriptionPair, OperationMap } from "./index.js"; import { FeeDescription, FeeDescriptionPair, OperationMap } from "./index.js";
@ -34,7 +34,7 @@ export default {
}; };
function timelineForExchange( function timelineForExchange(
ex: ExchangeFullDetailsListItem, ex: ExchangeFullDetails,
): OperationMap<FeeDescription[]> { ): OperationMap<FeeDescription[]> {
return { return {
deposit: createDenominationTimeline( deposit: createDenominationTimeline(
@ -61,8 +61,8 @@ function timelineForExchange(
} }
function timelinePairForExchange( function timelinePairForExchange(
ex1: ExchangeFullDetailsListItem, ex1: ExchangeFullDetails,
ex2: ExchangeFullDetailsListItem, ex2: ExchangeFullDetails,
): OperationMap<FeeDescriptionPair[]> { ): OperationMap<FeeDescriptionPair[]> {
const om1 = timelineForExchange(ex1); const om1 = timelineForExchange(ex1);
const om2 = timelineForExchange(ex2); const om2 = timelineForExchange(ex2);

View File

@ -50,7 +50,7 @@ export function ManualWithdrawPage({ amount, onCancel }: Props): VNode {
>(undefined); >(undefined);
const [error, setError] = useState<string | undefined>(undefined); const [error, setError] = useState<string | undefined>(undefined);
const state = useAsyncAsHook(wxApi.listExchangesDetailled); const state = useAsyncAsHook(wxApi.listExchanges);
useEffect(() => { useEffect(() => {
return wxApi.onUpdateNotification([NotificationType.ExchangeAdded], () => { return wxApi.onUpdateNotification([NotificationType.ExchangeAdded], () => {
state?.retry(); state?.retry();

View File

@ -49,7 +49,7 @@ export function SettingsPage(): VNode {
const webex = platform.getWalletWebExVersion(); const webex = platform.getWalletWebExVersion();
const exchangesHook = useAsyncAsHook(async () => { const exchangesHook = useAsyncAsHook(async () => {
const list = await wxApi.listExchangesDetailled(); const list = await wxApi.listExchanges();
const version = await wxApi.getVersion(); const version = await wxApi.getVersion();
return { exchanges: list.exchanges, version }; return { exchanges: list.exchanges, version };
}); });

View File

@ -563,7 +563,7 @@ export const InvoiceCreditComplete = createExample(TestedComponent, {
export const InvoiceCreditIncomplete = createExample(TestedComponent, { export const InvoiceCreditIncomplete = createExample(TestedComponent, {
transaction: { transaction: {
...exampleData.pull_credit, ...exampleData.pull_credit,
info: { ...exampleData.pull_credit.info, completed: false }, pending: true,
}, },
}); });
@ -581,9 +581,6 @@ export const TransferDebitComplete = createExample(TestedComponent, {
export const TransferDebitIncomplete = createExample(TestedComponent, { export const TransferDebitIncomplete = createExample(TestedComponent, {
transaction: { transaction: {
...exampleData.push_debit, ...exampleData.push_debit,
info: { pending: true,
...exampleData.push_debit.info,
completed: false,
},
}, },
}); });

View File

@ -167,6 +167,8 @@ export function TransactionView({
} }
} }
const SHOWING_RETRY_THRESHOLD_SECS = 30;
const { i18n } = useTranslationContext(); const { i18n } = useTranslationContext();
function TransactionTemplate({ function TransactionTemplate({
@ -174,15 +176,16 @@ export function TransactionView({
}: { }: {
children: ComponentChildren; children: ComponentChildren;
}): VNode { }): VNode {
const showSend = const showSend = false;
(transaction.type === TransactionType.PeerPullCredit || // (transaction.type === TransactionType.PeerPullCredit ||
transaction.type === TransactionType.PeerPushDebit) && // transaction.type === TransactionType.PeerPushDebit) &&
!transaction.info.completed; // !transaction.info.completed;
const showRetry = const showRetry =
transaction.error !== undefined || transaction.error !== undefined ||
transaction.timestamp.t_s === "never" || transaction.timestamp.t_s === "never" ||
(transaction.pending && (transaction.pending &&
differenceInSeconds(new Date(), transaction.timestamp.t_s * 1000) > 10); differenceInSeconds(new Date(), transaction.timestamp.t_s * 1000) >
SHOWING_RETRY_THRESHOLD_SECS);
return ( return (
<Fragment> <Fragment>
@ -624,7 +627,7 @@ export function TransactionView({
text={transaction.exchangeBaseUrl} text={transaction.exchangeBaseUrl}
kind="neutral" kind="neutral"
/> />
{!transaction.info.completed && ( {transaction.pending && (
<Part <Part
title={<i18n.Translate>URI</i18n.Translate>} title={<i18n.Translate>URI</i18n.Translate>}
text={<ShowQrWithCopy text={transaction.talerUri} />} text={<ShowQrWithCopy text={transaction.talerUri} />}
@ -710,7 +713,7 @@ export function TransactionView({
text={transaction.exchangeBaseUrl} text={transaction.exchangeBaseUrl}
kind="neutral" kind="neutral"
/> />
{!transaction.info.completed && ( {transaction.pending && (
<Part <Part
title={<i18n.Translate>URI</i18n.Translate>} title={<i18n.Translate>URI</i18n.Translate>}
text={<ShowQrWithCopy text={transaction.talerUri} />} text={<ShowQrWithCopy text={transaction.talerUri} />}

View File

@ -42,7 +42,7 @@ import {
CreateDepositGroupRequest, CreateDepositGroupRequest,
CreateDepositGroupResponse, CreateDepositGroupResponse,
DeleteTransactionRequest, DeleteTransactionRequest,
ExchangesListRespose, ExchangesListResponse,
GetExchangeTosResult, GetExchangeTosResult,
GetExchangeWithdrawalInfo, GetExchangeWithdrawalInfo,
GetFeeForDepositRequest, GetFeeForDepositRequest,
@ -67,7 +67,7 @@ import {
WalletDiagnostics, WalletDiagnostics,
WalletCoreVersion, WalletCoreVersion,
WithdrawUriInfoResponse, WithdrawUriInfoResponse,
ExchangeDetailledListRespose, ExchangeFullDetails,
} from "@gnu-taler/taler-util"; } from "@gnu-taler/taler-util";
import { import {
AddBackupProviderRequest, AddBackupProviderRequest,
@ -253,12 +253,14 @@ export function listKnownCurrencies(): Promise<ListOfKnownCurrencies> {
}); });
} }
export function listExchanges(): Promise<ExchangesListRespose> { export function listExchanges(): Promise<ExchangesListResponse> {
return callBackend("listExchanges", {}); return callBackend("listExchanges", {});
} }
export function listExchangesDetailled(): Promise<ExchangeDetailledListRespose> { export function getExchangeDetailedInfo(exchangeBaseUrl: string): Promise<ExchangeFullDetails> {
return callBackend("listExchangesDetailled", {}); return callBackend("getExchangeDetailedInfo", {
exchangeBaseUrl
});
} }
export function getVersion(): Promise<WalletCoreVersion> { export function getVersion(): Promise<WalletCoreVersion> {