remove some type literal and pretty
This commit is contained in:
parent
d97f440f25
commit
444c5427f4
@ -243,7 +243,9 @@ export interface TalerCryptoInterface {
|
||||
|
||||
signRefund(req: SignRefundRequest): Promise<SignRefundResponse>;
|
||||
|
||||
signDeletePurse(req: SignDeletePurseRequest): Promise<SignDeletePurseResponse>;
|
||||
signDeletePurse(
|
||||
req: SignDeletePurseRequest,
|
||||
): Promise<SignDeletePurseResponse>;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -1695,7 +1697,7 @@ export const nativeCryptoR: TalerCryptoInterfaceR = {
|
||||
});
|
||||
return {
|
||||
sig: sigResp.sig,
|
||||
}
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
|
@ -271,7 +271,6 @@ export interface SignRefundResponse {
|
||||
sig: string;
|
||||
}
|
||||
|
||||
|
||||
export interface SignDeletePurseRequest {
|
||||
pursePriv: string;
|
||||
}
|
||||
|
@ -120,13 +120,7 @@ export interface TopupReserveWithDemobankArgs {
|
||||
export async function topupReserveWithDemobank(
|
||||
args: TopupReserveWithDemobankArgs,
|
||||
) {
|
||||
const {
|
||||
http,
|
||||
bankAccessApiBaseUrl,
|
||||
amount,
|
||||
exchangeInfo,
|
||||
reservePub,
|
||||
} = args;
|
||||
const { http, bankAccessApiBaseUrl, amount, exchangeInfo, reservePub } = args;
|
||||
const bankHandle: BankServiceHandle = {
|
||||
bankAccessApiBaseUrl: bankAccessApiBaseUrl,
|
||||
http,
|
||||
|
@ -62,11 +62,7 @@ import { InternalWalletState } from "../../internal-wallet-state.js";
|
||||
import { assertUnreachable } from "../../util/assertUnreachable.js";
|
||||
import { checkLogicInvariant } from "../../util/invariants.js";
|
||||
import { GetReadOnlyAccess, GetReadWriteAccess } from "../../util/query.js";
|
||||
import {
|
||||
makeCoinAvailable,
|
||||
makeTombstoneId,
|
||||
TombstoneTag,
|
||||
} from "../common.js";
|
||||
import { makeCoinAvailable, makeTombstoneId, TombstoneTag } from "../common.js";
|
||||
import { getExchangeDetails } from "../exchanges.js";
|
||||
import { extractContractData } from "../pay-merchant.js";
|
||||
import { provideBackupState } from "./state.js";
|
||||
|
@ -464,7 +464,7 @@ export type ParsedTombstone =
|
||||
tag: TombstoneTag.DeleteWithdrawalGroup;
|
||||
withdrawalGroupId: string;
|
||||
}
|
||||
| { tag: TombstoneTag.DeleteRefund; refundGroupId: string }
|
||||
| { tag: TombstoneTag.DeleteRefund; refundGroupId: string };
|
||||
|
||||
export function constructTombstone(p: ParsedTombstone): TombstoneIdStr {
|
||||
switch (p.tag) {
|
||||
|
@ -495,8 +495,7 @@ async function waitForRefreshOnDepositGroup(
|
||||
if (refreshGroup.operationStatus === RefreshOperationStatus.Finished) {
|
||||
newOpState = DepositOperationStatus.Aborted;
|
||||
} else if (
|
||||
refreshGroup.operationStatus ===
|
||||
RefreshOperationStatus.Failed
|
||||
refreshGroup.operationStatus === RefreshOperationStatus.Failed
|
||||
) {
|
||||
newOpState = DepositOperationStatus.Aborted;
|
||||
}
|
||||
|
@ -208,14 +208,13 @@ async function longpollKycStatus(
|
||||
const transitionInfo = await ws.db
|
||||
.mktx((x) => [x.peerPullPaymentInitiations])
|
||||
.runReadWrite(async (tx) => {
|
||||
const peerIni = await tx.peerPullPaymentInitiations.get(
|
||||
pursePub,
|
||||
);
|
||||
const peerIni = await tx.peerPullPaymentInitiations.get(pursePub);
|
||||
if (!peerIni) {
|
||||
return;
|
||||
}
|
||||
if (
|
||||
peerIni.status !== PeerPullPaymentInitiationStatus.PendingMergeKycRequired
|
||||
peerIni.status !==
|
||||
PeerPullPaymentInitiationStatus.PendingMergeKycRequired
|
||||
) {
|
||||
return;
|
||||
}
|
||||
@ -254,10 +253,7 @@ async function processPeerPullCreditAbortingDeletePurse(
|
||||
const sigResp = await ws.cryptoApi.signDeletePurse({
|
||||
pursePriv,
|
||||
});
|
||||
const purseUrl = new URL(
|
||||
`purses/${pursePub}`,
|
||||
peerPullIni.exchangeBaseUrl,
|
||||
);
|
||||
const purseUrl = new URL(`purses/${pursePub}`, peerPullIni.exchangeBaseUrl);
|
||||
const resp = await ws.http.fetch(purseUrl.href, {
|
||||
method: "DELETE",
|
||||
headers: {
|
||||
@ -517,9 +513,7 @@ async function processPeerPullCreditKycRequired(
|
||||
const { transitionInfo, result } = await ws.db
|
||||
.mktx((x) => [x.peerPullPaymentInitiations])
|
||||
.runReadWrite(async (tx) => {
|
||||
const peerInc = await tx.peerPullPaymentInitiations.get(
|
||||
pursePub,
|
||||
);
|
||||
const peerInc = await tx.peerPullPaymentInitiations.get(pursePub);
|
||||
if (!peerInc) {
|
||||
return {
|
||||
transitionInfo: undefined,
|
||||
@ -532,7 +526,8 @@ async function processPeerPullCreditKycRequired(
|
||||
requirementRow: kycPending.requirement_row,
|
||||
};
|
||||
peerInc.kycUrl = kycStatus.kyc_url;
|
||||
peerInc.status = PeerPullPaymentInitiationStatus.PendingMergeKycRequired;
|
||||
peerInc.status =
|
||||
PeerPullPaymentInitiationStatus.PendingMergeKycRequired;
|
||||
const newTxState = computePeerPullCreditTransactionState(peerInc);
|
||||
await tx.peerPullPaymentInitiations.put(peerInc);
|
||||
// We'll remove this eventually! New clients should rely on the
|
||||
|
@ -45,14 +45,24 @@ import {
|
||||
PreparePayResultType,
|
||||
} from "@gnu-taler/taler-util";
|
||||
import { InternalWalletState } from "../internal-wallet-state.js";
|
||||
import { confirmPay, preparePayForUri, startRefundQueryForUri } from "./pay-merchant.js";
|
||||
import {
|
||||
confirmPay,
|
||||
preparePayForUri,
|
||||
startRefundQueryForUri,
|
||||
} from "./pay-merchant.js";
|
||||
import { getBalances } from "./balance.js";
|
||||
import { checkLogicInvariant } from "../util/invariants.js";
|
||||
import { acceptWithdrawalFromUri } from "./withdraw.js";
|
||||
import { updateExchangeFromUrl } from "./exchanges.js";
|
||||
import { initiatePeerPullPayment } from "./pay-peer-pull-credit.js";
|
||||
import { preparePeerPullDebit, confirmPeerPullDebit } from "./pay-peer-pull-debit.js";
|
||||
import { preparePeerPushCredit, confirmPeerPushCredit } from "./pay-peer-push-credit.js";
|
||||
import {
|
||||
preparePeerPullDebit,
|
||||
confirmPeerPullDebit,
|
||||
} from "./pay-peer-pull-debit.js";
|
||||
import {
|
||||
preparePeerPushCredit,
|
||||
confirmPeerPushCredit,
|
||||
} from "./pay-peer-push-credit.js";
|
||||
import { initiatePeerPushDebit } from "./pay-peer-push-debit.js";
|
||||
|
||||
const logger = new Logger("operations/testing.ts");
|
||||
|
@ -15,6 +15,8 @@
|
||||
*/
|
||||
import test, { ExecutionContext } from "ava";
|
||||
import {
|
||||
AmountMode,
|
||||
OperationType,
|
||||
calculatePlanFormAvailableCoins,
|
||||
selectCoinForOperation,
|
||||
} from "./coinSelection.js";
|
||||
@ -24,6 +26,7 @@ import {
|
||||
AmountJson,
|
||||
Amounts,
|
||||
Duration,
|
||||
TransactionAmountMode,
|
||||
TransactionType,
|
||||
} from "@gnu-taler/taler-util";
|
||||
|
||||
@ -67,10 +70,15 @@ test("get effective 2", (t) => {
|
||||
[kudos(2), 5],
|
||||
[kudos(5), 5],
|
||||
];
|
||||
const result = selectCoinForOperation("credit", kudos(2), "net", {
|
||||
list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
|
||||
exchanges: {},
|
||||
});
|
||||
const result = selectCoinForOperation(
|
||||
OperationType.Credit,
|
||||
kudos(2),
|
||||
AmountMode.Net,
|
||||
{
|
||||
list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
|
||||
exchanges: {},
|
||||
},
|
||||
);
|
||||
expect(t, result.coins).deep.equal(["KUDOS:2"]);
|
||||
t.assert(result.refresh === undefined);
|
||||
});
|
||||
@ -80,10 +88,15 @@ test("get raw 4", (t) => {
|
||||
[kudos(2), 5],
|
||||
[kudos(5), 5],
|
||||
];
|
||||
const result = selectCoinForOperation("credit", kudos(4), "gross", {
|
||||
list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
|
||||
exchanges: {},
|
||||
});
|
||||
const result = selectCoinForOperation(
|
||||
OperationType.Credit,
|
||||
kudos(4),
|
||||
AmountMode.Gross,
|
||||
{
|
||||
list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
|
||||
exchanges: {},
|
||||
},
|
||||
);
|
||||
|
||||
expect(t, result.coins).deep.equal(["KUDOS:2", "KUDOS:2"]);
|
||||
t.assert(result.refresh === undefined);
|
||||
@ -94,10 +107,15 @@ test("send effective 6", (t) => {
|
||||
[kudos(2), 5],
|
||||
[kudos(5), 5],
|
||||
];
|
||||
const result = selectCoinForOperation("debit", kudos(6), "gross", {
|
||||
list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
|
||||
exchanges: {},
|
||||
});
|
||||
const result = selectCoinForOperation(
|
||||
OperationType.Debit,
|
||||
kudos(6),
|
||||
AmountMode.Gross,
|
||||
{
|
||||
list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
|
||||
exchanges: {},
|
||||
},
|
||||
);
|
||||
|
||||
expect(t, result.coins).deep.equal(["KUDOS:5"]);
|
||||
t.assert(result.refresh?.selected === "KUDOS:2");
|
||||
@ -108,10 +126,15 @@ test("send raw 6", (t) => {
|
||||
[kudos(2), 5],
|
||||
[kudos(5), 5],
|
||||
];
|
||||
const result = selectCoinForOperation("debit", kudos(6), "gross", {
|
||||
list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
|
||||
exchanges: {},
|
||||
});
|
||||
const result = selectCoinForOperation(
|
||||
OperationType.Debit,
|
||||
kudos(6),
|
||||
AmountMode.Gross,
|
||||
{
|
||||
list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
|
||||
exchanges: {},
|
||||
},
|
||||
);
|
||||
|
||||
expect(t, result.coins).deep.equal(["KUDOS:5"]);
|
||||
t.assert(result.refresh?.selected === "KUDOS:2");
|
||||
@ -122,10 +145,15 @@ test("send raw 20 (not enough)", (t) => {
|
||||
[kudos(2), 1],
|
||||
[kudos(5), 2],
|
||||
];
|
||||
const result = selectCoinForOperation("debit", kudos(20), "gross", {
|
||||
list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
|
||||
exchanges: {},
|
||||
});
|
||||
const result = selectCoinForOperation(
|
||||
OperationType.Debit,
|
||||
kudos(20),
|
||||
AmountMode.Gross,
|
||||
{
|
||||
list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
|
||||
exchanges: {},
|
||||
},
|
||||
);
|
||||
|
||||
expect(t, result.coins).deep.equal(["KUDOS:5", "KUDOS:5", "KUDOS:2"]);
|
||||
t.assert(result.refresh === undefined);
|
||||
@ -147,7 +175,7 @@ test("deposit effective 2 ", (t) => {
|
||||
const result = calculatePlanFormAvailableCoins(
|
||||
TransactionType.Deposit,
|
||||
kudos(2),
|
||||
"effective",
|
||||
TransactionAmountMode.Effective,
|
||||
{
|
||||
list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
|
||||
exchanges: {
|
||||
@ -173,7 +201,7 @@ test("deposit raw 2 ", (t) => {
|
||||
const result = calculatePlanFormAvailableCoins(
|
||||
TransactionType.Deposit,
|
||||
kudos(2),
|
||||
"raw",
|
||||
TransactionAmountMode.Raw,
|
||||
{
|
||||
list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
|
||||
exchanges: {
|
||||
@ -199,7 +227,7 @@ test("withdraw raw 21 ", (t) => {
|
||||
const result = calculatePlanFormAvailableCoins(
|
||||
TransactionType.Withdrawal,
|
||||
kudos(21),
|
||||
"raw",
|
||||
TransactionAmountMode.Raw,
|
||||
{
|
||||
list: coinList.map(([v, t]) => defaultFeeConfig(v, t)),
|
||||
exchanges: {
|
||||
|
@ -46,6 +46,7 @@ import {
|
||||
PayCoinSelection,
|
||||
PayMerchantInsufficientBalanceDetails,
|
||||
strcmp,
|
||||
TransactionAmountMode,
|
||||
TransactionType,
|
||||
} from "@gnu-taler/taler-util";
|
||||
import {
|
||||
@ -818,7 +819,7 @@ function getCoinsFilter(req: GetPlanForOperationRequest): CoinsFilter {
|
||||
export function calculatePlanFormAvailableCoins(
|
||||
transactionType: TransactionType,
|
||||
amount: AmountJson,
|
||||
mode: "effective" | "raw",
|
||||
mode: TransactionAmountMode,
|
||||
availableCoins: AvailableCoins,
|
||||
) {
|
||||
const operationType = getOperationType(transactionType);
|
||||
@ -828,7 +829,9 @@ export function calculatePlanFormAvailableCoins(
|
||||
usableCoins = selectCoinForOperation(
|
||||
operationType,
|
||||
amount,
|
||||
mode === "effective" ? "net" : "gross",
|
||||
mode === TransactionAmountMode.Effective
|
||||
? AmountMode.Net
|
||||
: AmountMode.Gross,
|
||||
availableCoins,
|
||||
);
|
||||
break;
|
||||
@ -839,11 +842,11 @@ export function calculatePlanFormAvailableCoins(
|
||||
//are from that exchange
|
||||
const wireFee = Object.values(availableCoins.exchanges)[0].wireFee!;
|
||||
|
||||
if (mode === "effective") {
|
||||
if (mode === TransactionAmountMode.Effective) {
|
||||
usableCoins = selectCoinForOperation(
|
||||
operationType,
|
||||
amount,
|
||||
"gross",
|
||||
AmountMode.Gross,
|
||||
availableCoins,
|
||||
);
|
||||
|
||||
@ -857,7 +860,7 @@ export function calculatePlanFormAvailableCoins(
|
||||
usableCoins = selectCoinForOperation(
|
||||
operationType,
|
||||
adjustedAmount,
|
||||
"net",
|
||||
AmountMode.Net,
|
||||
availableCoins,
|
||||
);
|
||||
|
||||
@ -913,6 +916,27 @@ export async function getPlanForOperation(
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* If the operation going to be plan subtracts
|
||||
* or adds amount in the wallet db
|
||||
*/
|
||||
export enum OperationType {
|
||||
Credit = "credit",
|
||||
Debit = "debit",
|
||||
}
|
||||
|
||||
/**
|
||||
* How the amount should be interpreted
|
||||
* net = without fee
|
||||
* gross = with fee
|
||||
*
|
||||
* Net value is always lower than gross
|
||||
*/
|
||||
export enum AmountMode {
|
||||
Net = "net",
|
||||
Gross = "gross",
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param op defined which fee are we taking into consideration: deposits or withdraw
|
||||
@ -922,9 +946,9 @@ export async function getPlanForOperation(
|
||||
* @returns
|
||||
*/
|
||||
export function selectCoinForOperation(
|
||||
op: "debit" | "credit",
|
||||
op: OperationType,
|
||||
limit: AmountJson,
|
||||
mode: "net" | "gross",
|
||||
mode: AmountMode,
|
||||
coins: AvailableCoins,
|
||||
): SelectedCoins {
|
||||
const result: SelectedCoins = {
|
||||
@ -951,8 +975,11 @@ export function selectCoinForOperation(
|
||||
iterateDenoms: while (denomIdx < coins.list.length) {
|
||||
const denom = coins.list[denomIdx];
|
||||
let total =
|
||||
op === "credit" ? Number.MAX_SAFE_INTEGER : denom.totalAvailable ?? 0;
|
||||
const opFee = op === "credit" ? denom.denomWithdraw : denom.denomDeposit;
|
||||
op === OperationType.Credit
|
||||
? Number.MAX_SAFE_INTEGER
|
||||
: denom.totalAvailable ?? 0;
|
||||
const opFee =
|
||||
op === OperationType.Credit ? denom.denomWithdraw : denom.denomDeposit;
|
||||
const contribution = Amounts.sub(denom.value, opFee).amount;
|
||||
|
||||
if (Amounts.isZero(contribution)) {
|
||||
@ -969,7 +996,7 @@ export function selectCoinForOperation(
|
||||
contribution,
|
||||
).amount;
|
||||
|
||||
const progress = mode === "gross" ? nextValue : nextContribution;
|
||||
const progress = mode === AmountMode.Gross ? nextValue : nextContribution;
|
||||
|
||||
if (Amounts.cmp(progress, limit) === 1) {
|
||||
//the current coin is more than we need, try next denom
|
||||
@ -1008,14 +1035,15 @@ export function selectCoinForOperation(
|
||||
// we made it
|
||||
return result;
|
||||
}
|
||||
if (op === "credit") {
|
||||
if (op === OperationType.Credit) {
|
||||
//doing withdraw there is no way to cover the gap
|
||||
return result;
|
||||
}
|
||||
//tried all the coins but there is a gap
|
||||
//doing deposit we can try refreshing coins
|
||||
|
||||
const total = mode === "gross" ? result.totalValue : result.totalContribution;
|
||||
const total =
|
||||
mode === AmountMode.Gross ? result.totalValue : result.totalContribution;
|
||||
const gap = Amounts.sub(limit, total).amount;
|
||||
|
||||
//about recursive calls
|
||||
@ -1027,7 +1055,7 @@ export function selectCoinForOperation(
|
||||
refreshIteration: while (refreshIdx < coins.list.length) {
|
||||
const d = coins.list[refreshIdx];
|
||||
const denomContribution =
|
||||
mode === "gross"
|
||||
mode === AmountMode.Gross
|
||||
? Amounts.sub(d.value, d.denomRefresh).amount
|
||||
: Amounts.sub(d.value, d.denomDeposit, d.denomRefresh).amount;
|
||||
|
||||
@ -1038,7 +1066,7 @@ export function selectCoinForOperation(
|
||||
}
|
||||
|
||||
const changeCost = selectCoinForOperation(
|
||||
"credit",
|
||||
OperationType.Credit,
|
||||
changeAfterDeposit,
|
||||
mode,
|
||||
coins,
|
||||
@ -1067,7 +1095,7 @@ export function selectCoinForOperation(
|
||||
refreshIdx++;
|
||||
}
|
||||
if (choice) {
|
||||
if (mode === "gross") {
|
||||
if (mode === AmountMode.Gross) {
|
||||
result.totalValue = Amounts.add(result.totalValue, gap).amount;
|
||||
result.totalContribution = Amounts.add(
|
||||
result.totalContribution,
|
||||
@ -1096,9 +1124,9 @@ export function selectCoinForOperation(
|
||||
}
|
||||
|
||||
type CompareCoinsFunction = (d1: CoinInfo, d2: CoinInfo) => -1 | 0 | 1;
|
||||
function buildRankingForCoins(op: "debit" | "credit"): CompareCoinsFunction {
|
||||
function buildRankingForCoins(op: OperationType): CompareCoinsFunction {
|
||||
function getFee(d: CoinInfo) {
|
||||
return op === "credit" ? d.denomWithdraw : d.denomDeposit;
|
||||
return op === OperationType.Credit ? d.denomWithdraw : d.denomDeposit;
|
||||
}
|
||||
//different exchanges may have different wireFee
|
||||
//ranking should take the relative contribution in the exchange
|
||||
@ -1116,28 +1144,32 @@ function buildRankingForCoins(op: "debit" | "credit"): CompareCoinsFunction {
|
||||
};
|
||||
}
|
||||
|
||||
function getOperationType(txType: TransactionType): "credit" | "debit" {
|
||||
function getOperationType(txType: TransactionType): OperationType {
|
||||
const operationType =
|
||||
txType === TransactionType.Withdrawal
|
||||
? ("credit" as const)
|
||||
? OperationType.Credit
|
||||
: txType === TransactionType.Deposit
|
||||
? ("debit" as const)
|
||||
? OperationType.Debit
|
||||
: undefined;
|
||||
if (!operationType) {
|
||||
throw Error(`operation type ${txType} not supported`);
|
||||
throw Error(`operation type ${txType} not yet supported`);
|
||||
}
|
||||
return operationType;
|
||||
}
|
||||
|
||||
function getAmountsWithFee(
|
||||
op: "debit" | "credit",
|
||||
op: OperationType,
|
||||
value: AmountJson,
|
||||
contribution: AmountJson,
|
||||
details: any,
|
||||
): GetPlanForOperationResponse {
|
||||
return {
|
||||
rawAmount: Amounts.stringify(op === "credit" ? value : contribution),
|
||||
effectiveAmount: Amounts.stringify(op === "credit" ? contribution : value),
|
||||
rawAmount: Amounts.stringify(
|
||||
op === OperationType.Credit ? value : contribution,
|
||||
),
|
||||
effectiveAmount: Amounts.stringify(
|
||||
op === OperationType.Credit ? contribution : value,
|
||||
),
|
||||
details,
|
||||
};
|
||||
}
|
||||
@ -1202,7 +1234,7 @@ interface CoinsFilter {
|
||||
*/
|
||||
async function getAvailableCoins(
|
||||
ws: InternalWalletState,
|
||||
op: "credit" | "debit",
|
||||
op: OperationType,
|
||||
currency: string,
|
||||
filters: CoinsFilter = {},
|
||||
): Promise<AvailableCoins> {
|
||||
@ -1286,7 +1318,7 @@ async function getAvailableCoins(
|
||||
let creditDeadline = AbsoluteTime.never();
|
||||
let debitDeadline = AbsoluteTime.never();
|
||||
//4.- filter coins restricted by age
|
||||
if (op === "credit") {
|
||||
if (op === OperationType.Credit) {
|
||||
const ds = await tx.denominations.indexes.byExchangeBaseUrl.getAll(
|
||||
exchangeBaseUrl,
|
||||
);
|
||||
|
@ -458,7 +458,9 @@ export function isWithdrawableDenom(
|
||||
): boolean {
|
||||
const now = AbsoluteTime.now();
|
||||
const start = AbsoluteTime.fromProtocolTimestamp(d.stampStart);
|
||||
const withdrawExpire = AbsoluteTime.fromProtocolTimestamp(d.stampExpireWithdraw);
|
||||
const withdrawExpire = AbsoluteTime.fromProtocolTimestamp(
|
||||
d.stampExpireWithdraw,
|
||||
);
|
||||
const started = AbsoluteTime.cmp(now, start) >= 0;
|
||||
let lastPossibleWithdraw: AbsoluteTime;
|
||||
if (denomselAllowLate) {
|
||||
|
Loading…
Reference in New Issue
Block a user