anastasis: use new truth API
This commit is contained in:
parent
a30a547ac5
commit
f33d9dad47
@ -867,19 +867,10 @@ async function pollChallenges(
|
|||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
async function getResponseHash(
|
||||||
* Request a truth, optionally with a challenge solution
|
|
||||||
* provided by the user.
|
|
||||||
*/
|
|
||||||
async function requestTruth(
|
|
||||||
state: ReducerStateRecovery,
|
|
||||||
truth: EscrowMethod,
|
truth: EscrowMethod,
|
||||||
solveRequest?: ActionArgsSolveChallengeRequest,
|
solveRequest: ActionArgsSolveChallengeRequest,
|
||||||
): Promise<ReducerStateRecovery | ReducerStateError> {
|
): Promise<string> {
|
||||||
const url = new URL(`/truth/${truth.uuid}`, truth.url);
|
|
||||||
|
|
||||||
if (solveRequest) {
|
|
||||||
logger.info(`handling solve request ${j2s(solveRequest)}`);
|
|
||||||
let respHash: string;
|
let respHash: string;
|
||||||
switch (truth.escrow_type) {
|
switch (truth.escrow_type) {
|
||||||
case ChallengeType.Question: {
|
case ChallengeType.Question: {
|
||||||
@ -918,17 +909,36 @@ async function requestTruth(
|
|||||||
default:
|
default:
|
||||||
throw Error(`unsupported challenge type "${truth.escrow_type}""`);
|
throw Error(`unsupported challenge type "${truth.escrow_type}""`);
|
||||||
}
|
}
|
||||||
url.searchParams.set("response", respHash);
|
return respHash;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Request a truth, optionally with a challenge solution
|
||||||
|
* provided by the user.
|
||||||
|
*/
|
||||||
|
async function requestTruth(
|
||||||
|
state: ReducerStateRecovery,
|
||||||
|
truth: EscrowMethod,
|
||||||
|
solveRequest: ActionArgsSolveChallengeRequest,
|
||||||
|
): Promise<ReducerStateRecovery | ReducerStateError> {
|
||||||
|
const url = new URL(`/truth/${truth.uuid}/solve`, truth.url);
|
||||||
|
|
||||||
|
const hresp = await getResponseHash(truth, solveRequest);
|
||||||
|
|
||||||
const resp = await fetch(url.href, {
|
const resp = await fetch(url.href, {
|
||||||
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
"Anastasis-Truth-Decryption-Key": truth.truth_key,
|
Accept: "application/json",
|
||||||
|
"Content-Type": "application/json",
|
||||||
},
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
truth_decryption_key: truth.truth_key,
|
||||||
|
h_response: hresp,
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
`got GET /truth response from ${truth.url}, http status ${resp.status}`,
|
`got POST /truth/.../solve response from ${truth.url}, http status ${resp.status}`,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (resp.status === HttpStatusCode.Ok) {
|
if (resp.status === HttpStatusCode.Ok) {
|
||||||
@ -975,66 +985,6 @@ async function requestTruth(
|
|||||||
return tryRecoverSecret(newState);
|
return tryRecoverSecret(newState);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (resp.status === HttpStatusCode.Forbidden) {
|
|
||||||
const body = await resp.json();
|
|
||||||
if (
|
|
||||||
body.code === TalerErrorCode.ANASTASIS_TRUTH_CHALLENGE_RESPONSE_REQUIRED
|
|
||||||
) {
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
recovery_state: RecoveryStates.ChallengeSolving,
|
|
||||||
challenge_feedback: {
|
|
||||||
...state.challenge_feedback,
|
|
||||||
[truth.uuid]: {
|
|
||||||
state: ChallengeFeedbackStatus.Pending,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
recovery_state: RecoveryStates.ChallengeSolving,
|
|
||||||
challenge_feedback: {
|
|
||||||
...state.challenge_feedback,
|
|
||||||
[truth.uuid]: {
|
|
||||||
state: ChallengeFeedbackStatus.Message,
|
|
||||||
message: body.hint ?? "Challenge should be solved",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (resp.status === HttpStatusCode.Accepted) {
|
|
||||||
const body = await resp.json();
|
|
||||||
logger.info(`got body ${j2s(body)}`);
|
|
||||||
if (body.method === "iban") {
|
|
||||||
const b = body as IbanExternalAuthResponse;
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
recovery_state: RecoveryStates.ChallengeSolving,
|
|
||||||
challenge_feedback: {
|
|
||||||
...state.challenge_feedback,
|
|
||||||
[truth.uuid]: {
|
|
||||||
state: ChallengeFeedbackStatus.AuthIban,
|
|
||||||
answer_code: b.answer_code,
|
|
||||||
business_name: b.details.business_name,
|
|
||||||
challenge_amount: b.details.challenge_amount,
|
|
||||||
credit_iban: b.details.credit_iban,
|
|
||||||
wire_transfer_subject: b.details.wire_transfer_subject,
|
|
||||||
details: b.details,
|
|
||||||
method: "iban",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
return {
|
|
||||||
code: TalerErrorCode.ANASTASIS_TRUTH_CHALLENGE_FAILED,
|
|
||||||
hint: "unknown external authentication method",
|
|
||||||
http_status: resp.status,
|
|
||||||
} as ReducerStateError;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
code: TalerErrorCode.ANASTASIS_TRUTH_CHALLENGE_FAILED,
|
code: TalerErrorCode.ANASTASIS_TRUTH_CHALLENGE_FAILED,
|
||||||
hint: "got unexpected /truth/ response status",
|
hint: "got unexpected /truth/ response status",
|
||||||
@ -1053,6 +1003,7 @@ async function solveChallenge(
|
|||||||
if (!truth) {
|
if (!truth) {
|
||||||
throw Error("truth for challenge not found");
|
throw Error("truth for challenge not found");
|
||||||
}
|
}
|
||||||
|
|
||||||
return requestTruth(state, truth, ta);
|
return requestTruth(state, truth, ta);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1096,7 +1047,58 @@ async function selectChallenge(
|
|||||||
throw "truth for challenge not found";
|
throw "truth for challenge not found";
|
||||||
}
|
}
|
||||||
|
|
||||||
return requestTruth({ ...state, selected_challenge_uuid: ta.uuid }, truth);
|
const url = new URL(`/truth/${truth.uuid}/challenge`, truth.url);
|
||||||
|
|
||||||
|
if (truth.escrow_type === ChallengeType.Question) {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
recovery_state: RecoveryStates.ChallengeSolving,
|
||||||
|
selected_challenge_uuid: truth.uuid,
|
||||||
|
challenge_feedback: {
|
||||||
|
...state.challenge_feedback,
|
||||||
|
[truth.uuid]: {
|
||||||
|
state: ChallengeFeedbackStatus.Pending,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const resp = await fetch(url.href, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
Accept: "application/json",
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
truth_decryption_key: truth.truth_key,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
`got GET /truth/.../challenge response from ${truth.url}, http status ${resp.status}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (resp.status === HttpStatusCode.Ok) {
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
recovery_state: RecoveryStates.ChallengeSolving,
|
||||||
|
selected_challenge_uuid: truth.uuid,
|
||||||
|
challenge_feedback: {
|
||||||
|
...state.challenge_feedback,
|
||||||
|
[truth.uuid]: {
|
||||||
|
state: ChallengeFeedbackStatus.Pending,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// FIXME: look at response, include in challenge_feedback!
|
||||||
|
|
||||||
|
return {
|
||||||
|
code: TalerErrorCode.ANASTASIS_TRUTH_CHALLENGE_FAILED,
|
||||||
|
hint: "got unexpected /truth/.../challenge response status",
|
||||||
|
http_status: resp.status,
|
||||||
|
} as ReducerStateError;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function backupSelectContinent(
|
async function backupSelectContinent(
|
||||||
@ -1140,15 +1142,15 @@ interface TransitionImpl<S, T> {
|
|||||||
handler: (s: S, args: T) => Promise<S | ReducerStateError>;
|
handler: (s: S, args: T) => Promise<S | ReducerStateError>;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Transition<S, T> {
|
interface Transition<S> {
|
||||||
[x: string]: TransitionImpl<S, T>;
|
[x: string]: TransitionImpl<S, any>;
|
||||||
}
|
}
|
||||||
|
|
||||||
function transition<S, T>(
|
function transition<S, T>(
|
||||||
action: string,
|
action: string,
|
||||||
argCodec: Codec<T>,
|
argCodec: Codec<T>,
|
||||||
handler: (s: S, args: T) => Promise<S | ReducerStateError>,
|
handler: (s: S, args: T) => Promise<S | ReducerStateError>,
|
||||||
): Transition<S, T> {
|
): Transition<S> {
|
||||||
return {
|
return {
|
||||||
[action]: {
|
[action]: {
|
||||||
argCodec,
|
argCodec,
|
||||||
@ -1160,7 +1162,7 @@ function transition<S, T>(
|
|||||||
function transitionBackupJump(
|
function transitionBackupJump(
|
||||||
action: string,
|
action: string,
|
||||||
st: BackupStates,
|
st: BackupStates,
|
||||||
): Transition<ReducerStateBackup, void> {
|
): Transition<ReducerStateBackup> {
|
||||||
return {
|
return {
|
||||||
[action]: {
|
[action]: {
|
||||||
argCodec: codecForAny(),
|
argCodec: codecForAny(),
|
||||||
@ -1172,7 +1174,7 @@ function transitionBackupJump(
|
|||||||
function transitionRecoveryJump(
|
function transitionRecoveryJump(
|
||||||
action: string,
|
action: string,
|
||||||
st: RecoveryStates,
|
st: RecoveryStates,
|
||||||
): Transition<ReducerStateRecovery, void> {
|
): Transition<ReducerStateRecovery> {
|
||||||
return {
|
return {
|
||||||
[action]: {
|
[action]: {
|
||||||
argCodec: codecForAny(),
|
argCodec: codecForAny(),
|
||||||
@ -1440,7 +1442,7 @@ async function updateSecretExpiration(
|
|||||||
|
|
||||||
const backupTransitions: Record<
|
const backupTransitions: Record<
|
||||||
BackupStates,
|
BackupStates,
|
||||||
Transition<ReducerStateBackup, any>
|
Transition<ReducerStateBackup>
|
||||||
> = {
|
> = {
|
||||||
[BackupStates.ContinentSelecting]: {
|
[BackupStates.ContinentSelecting]: {
|
||||||
...transition(
|
...transition(
|
||||||
@ -1511,7 +1513,7 @@ const backupTransitions: Record<
|
|||||||
|
|
||||||
const recoveryTransitions: Record<
|
const recoveryTransitions: Record<
|
||||||
RecoveryStates,
|
RecoveryStates,
|
||||||
Transition<ReducerStateRecovery, any>
|
Transition<ReducerStateRecovery>
|
||||||
> = {
|
> = {
|
||||||
[RecoveryStates.ContinentSelecting]: {
|
[RecoveryStates.ContinentSelecting]: {
|
||||||
...transition(
|
...transition(
|
||||||
|
@ -6,9 +6,7 @@ import {
|
|||||||
codecForNumber,
|
codecForNumber,
|
||||||
codecForString,
|
codecForString,
|
||||||
codecForTimestamp,
|
codecForTimestamp,
|
||||||
Duration,
|
|
||||||
TalerProtocolTimestamp,
|
TalerProtocolTimestamp,
|
||||||
AbsoluteTime,
|
|
||||||
} from "@gnu-taler/taler-util";
|
} from "@gnu-taler/taler-util";
|
||||||
import { ChallengeFeedback } from "./challenge-feedback-types.js";
|
import { ChallengeFeedback } from "./challenge-feedback-types.js";
|
||||||
import { KeyShare } from "./crypto.js";
|
import { KeyShare } from "./crypto.js";
|
||||||
|
@ -18,7 +18,7 @@
|
|||||||
*
|
*
|
||||||
* @author Sebastian Javier Marchano (sebasjm)
|
* @author Sebastian Javier Marchano (sebasjm)
|
||||||
*/
|
*/
|
||||||
import { useState } from "preact/hooks";
|
import { useCallback, useEffect, useRef, useState } from "preact/hooks";
|
||||||
// import { cancelPendingRequest } from "./backend";
|
// import { cancelPendingRequest } from "./backend";
|
||||||
|
|
||||||
export interface Options {
|
export interface Options {
|
||||||
@ -34,6 +34,17 @@ export interface AsyncOperationApi<T> {
|
|||||||
error: string | undefined;
|
error: string | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function useIsMounted() {
|
||||||
|
const isMountedRef = useRef(true);
|
||||||
|
const isMounted = useCallback(() => isMountedRef.current, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => void (isMountedRef.current = false);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return isMounted;
|
||||||
|
}
|
||||||
|
|
||||||
export function useAsync<T>(
|
export function useAsync<T>(
|
||||||
fn?: (...args: any) => Promise<T>,
|
fn?: (...args: any) => Promise<T>,
|
||||||
{ slowTolerance: tooLong }: Options = { slowTolerance: 1000 },
|
{ slowTolerance: tooLong }: Options = { slowTolerance: 1000 },
|
||||||
@ -42,11 +53,15 @@ export function useAsync<T>(
|
|||||||
const [isLoading, setLoading] = useState<boolean>(false);
|
const [isLoading, setLoading] = useState<boolean>(false);
|
||||||
const [error, setError] = useState<any>(undefined);
|
const [error, setError] = useState<any>(undefined);
|
||||||
const [isSlow, setSlow] = useState(false);
|
const [isSlow, setSlow] = useState(false);
|
||||||
|
const isMounted = useIsMounted();
|
||||||
|
|
||||||
const request = async (...args: any) => {
|
const request = async (...args: any) => {
|
||||||
if (!fn) return;
|
if (!fn) return;
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const handler = setTimeout(() => {
|
const handler = setTimeout(() => {
|
||||||
|
if (!isMounted()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
setSlow(true);
|
setSlow(true);
|
||||||
}, tooLong);
|
}, tooLong);
|
||||||
|
|
||||||
@ -54,6 +69,10 @@ export function useAsync<T>(
|
|||||||
console.log("calling async", args);
|
console.log("calling async", args);
|
||||||
const result = await fn(...args);
|
const result = await fn(...args);
|
||||||
console.log("async back", result);
|
console.log("async back", result);
|
||||||
|
if (!isMounted()) {
|
||||||
|
// Possibly calling fn(...) resulted in the component being unmounted.
|
||||||
|
return;
|
||||||
|
}
|
||||||
setData(result);
|
setData(result);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setError(error);
|
setError(error);
|
||||||
|
@ -220,6 +220,13 @@ export enum TalerErrorCode {
|
|||||||
*/
|
*/
|
||||||
GENERIC_FAILED_COMPUTE_JSON_HASH = 61,
|
GENERIC_FAILED_COMPUTE_JSON_HASH = 61,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The service could not compute an amount.
|
||||||
|
* Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
|
||||||
|
* (A value of 0 indicates that the error is generated client-side).
|
||||||
|
*/
|
||||||
|
GENERIC_FAILED_COMPUTE_AMOUNT = 62,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The HTTP server had insufficient memory to parse the request.
|
* The HTTP server had insufficient memory to parse the request.
|
||||||
* Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
|
* Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
|
||||||
@ -395,6 +402,20 @@ export enum TalerErrorCode {
|
|||||||
*/
|
*/
|
||||||
EXCHANGE_GENERIC_CLOCK_SKEW = 1020,
|
EXCHANGE_GENERIC_CLOCK_SKEW = 1020,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The specified amount for the coin is higher than the value of the denomination of the coin.
|
||||||
|
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
|
||||||
|
* (A value of 0 indicates that the error is generated client-side).
|
||||||
|
*/
|
||||||
|
EXCHANGE_GENERIC_AMOUNT_EXCEEDS_DENOMINATION_VALUE = 1021,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The exchange was not properly configured with global fees.
|
||||||
|
* Returned with an HTTP status code of #MHD_HTTP_INTERNAL_SERVER_ERROR (500).
|
||||||
|
* (A value of 0 indicates that the error is generated client-side).
|
||||||
|
*/
|
||||||
|
EXCHANGE_GENERIC_GLOBAL_FEES_MISSING = 1022,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The exchange did not find information about the specified transaction in the database.
|
* The exchange did not find information about the specified transaction in the database.
|
||||||
* Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
|
* Returned with an HTTP status code of #MHD_HTTP_NOT_FOUND (404).
|
||||||
@ -1081,6 +1102,83 @@ export enum TalerErrorCode {
|
|||||||
*/
|
*/
|
||||||
EXCHANGE_MANAGEMENT_GLOBAL_FEE_SIGNATURE_INVALID = 1817,
|
EXCHANGE_MANAGEMENT_GLOBAL_FEE_SIGNATURE_INVALID = 1817,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The purse was previously created with different meta data.
|
||||||
|
* Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
|
||||||
|
* (A value of 0 indicates that the error is generated client-side).
|
||||||
|
*/
|
||||||
|
EXCHANGE_PURSE_CREATE_CONFLICTING_META_DATA = 1850,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The purse was previously created with a different contract.
|
||||||
|
* Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
|
||||||
|
* (A value of 0 indicates that the error is generated client-side).
|
||||||
|
*/
|
||||||
|
EXCHANGE_PURSE_CREATE_CONFLICTING_CONTRACT_STORED = 1851,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A coin signature for a deposit into the purse is invalid.
|
||||||
|
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
|
||||||
|
* (A value of 0 indicates that the error is generated client-side).
|
||||||
|
*/
|
||||||
|
EXCHANGE_PURSE_CREATE_COIN_SIGNATURE_INVALID = 1852,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The purse expiration time is in the past.
|
||||||
|
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
|
||||||
|
* (A value of 0 indicates that the error is generated client-side).
|
||||||
|
*/
|
||||||
|
EXCHANGE_PURSE_CREATE_EXPIRATION_BEFORE_NOW = 1853,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The purse expiration time is "never".
|
||||||
|
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
|
||||||
|
* (A value of 0 indicates that the error is generated client-side).
|
||||||
|
*/
|
||||||
|
EXCHANGE_PURSE_CREATE_EXPIRATION_IS_NEVER = 1854,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The purse signature over the purse meta data is invalid.
|
||||||
|
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
|
||||||
|
* (A value of 0 indicates that the error is generated client-side).
|
||||||
|
*/
|
||||||
|
EXCHANGE_PURSE_CREATE_SIGNATURE_INVALID = 1855,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The signature over the encrypted contract is invalid.
|
||||||
|
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
|
||||||
|
* (A value of 0 indicates that the error is generated client-side).
|
||||||
|
*/
|
||||||
|
EXCHANGE_PURSE_ECONTRACT_SIGNATURE_INVALID = 1856,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The signature from the exchange over the confirmation is invalid.
|
||||||
|
* Returned with an HTTP status code of #MHD_HTTP_UNINITIALIZED (0).
|
||||||
|
* (A value of 0 indicates that the error is generated client-side).
|
||||||
|
*/
|
||||||
|
EXCHANGE_PURSE_CREATE_EXCHANGE_SIGNATURE_INVALID = 1857,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The coin was previously deposited with different meta data.
|
||||||
|
* Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
|
||||||
|
* (A value of 0 indicates that the error is generated client-side).
|
||||||
|
*/
|
||||||
|
EXCHANGE_PURSE_DEPOSIT_CONFLICTING_META_DATA = 1858,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The encrypted contract was previously uploaded with different meta data.
|
||||||
|
* Returned with an HTTP status code of #MHD_HTTP_CONFLICT (409).
|
||||||
|
* (A value of 0 indicates that the error is generated client-side).
|
||||||
|
*/
|
||||||
|
EXCHANGE_PURSE_ECONTRACT_CONFLICTING_META_DATA = 1859,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The deposited amount is less than the purse fee.
|
||||||
|
* Returned with an HTTP status code of #MHD_HTTP_BAD_REQUEST (400).
|
||||||
|
* (A value of 0 indicates that the error is generated client-side).
|
||||||
|
*/
|
||||||
|
EXCHANGE_CREATE_PURSE_NEGATIVE_VALUE_AFTER_FEE = 1860,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The auditor signature over the denomination meta data is invalid.
|
* The auditor signature over the denomination meta data is invalid.
|
||||||
* Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
|
* Returned with an HTTP status code of #MHD_HTTP_FORBIDDEN (403).
|
||||||
|
Loading…
Reference in New Issue
Block a user