mirror of
https://github.com/Such-Software/hash-wallet.git
synced 2026-10-26 06:08:40 +01:00
Reformat the whole codebase with 100 line width (#3403)
This commit is contained in:
@@ -37,8 +37,7 @@ class Output = OutputBase with _$Output;
|
||||
|
||||
abstract class OutputBase with Store {
|
||||
OutputBase(this._wallet, this._appStore, this._fiatConversationStore, this.cryptoCurrencyHandler)
|
||||
:
|
||||
key = UniqueKey(),
|
||||
: key = UniqueKey(),
|
||||
sendAll = false,
|
||||
cryptoAmount = '',
|
||||
cryptoFullBalance = '',
|
||||
@@ -57,7 +56,6 @@ abstract class OutputBase with Store {
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Key key;
|
||||
|
||||
bool get useSatoshi => _appStore.amountParsingProxy.useSatoshi(cryptoCurrencyHandler());
|
||||
@@ -98,7 +96,6 @@ abstract class OutputBase with Store {
|
||||
@observable
|
||||
String extractedAddress;
|
||||
|
||||
|
||||
@computed
|
||||
bool get isParsedAddress =>
|
||||
parsedAddress.addressSource != AddressSource.notParsed && parsedAddress.handle.isNotEmpty;
|
||||
@@ -172,9 +169,11 @@ abstract class OutputBase with Store {
|
||||
break;
|
||||
case WalletType.tron:
|
||||
if (cryptoCurrencyHandler() == CryptoCurrency.trx) {
|
||||
estimatedFee = tron!.getTronNativeEstimatedFee(_wallet) ?? Money.zero(CryptoCurrency.trx);
|
||||
estimatedFee =
|
||||
tron!.getTronNativeEstimatedFee(_wallet) ?? Money.zero(CryptoCurrency.trx);
|
||||
} else {
|
||||
estimatedFee = tron!.getTronTRC20EstimatedFee(_wallet) ?? Money.zero(CryptoCurrency.trx);
|
||||
estimatedFee =
|
||||
tron!.getTronTRC20EstimatedFee(_wallet) ?? Money.zero(CryptoCurrency.trx);
|
||||
}
|
||||
break;
|
||||
|
||||
@@ -200,7 +199,8 @@ abstract class OutputBase with Store {
|
||||
? evm!.getEVMNativeEstimatedFee(_wallet)
|
||||
: evm!.getEVMERC20EstimatedFee(_wallet);
|
||||
|
||||
estimatedFee = Money(BigInt.parse(fee ?? '0.0'), walletTypeToCryptoCurrency(_wallet.type));
|
||||
estimatedFee =
|
||||
Money(BigInt.parse(fee ?? '0.0'), walletTypeToCryptoCurrency(_wallet.type));
|
||||
break;
|
||||
|
||||
/// end EVMs
|
||||
@@ -293,6 +293,7 @@ abstract class OutputBase with Store {
|
||||
}
|
||||
|
||||
@action
|
||||
|
||||
/// [setCryptoAmount] always takes in the canonical representation eg. Bitcoin and not Sats
|
||||
void setCryptoAmount(String amount) {
|
||||
if (amount.toUpperCase() != S.current.all) sendAll = false;
|
||||
|
||||
@@ -35,9 +35,7 @@ abstract class SendTemplateViewModelBase with Store {
|
||||
@action
|
||||
void addRecipient() {
|
||||
recipients.add(TemplateViewModel(
|
||||
wallet: _wallet,
|
||||
appStore: _appStore,
|
||||
fiatConversationStore: _fiatConversationStore));
|
||||
wallet: _wallet, appStore: _appStore, fiatConversationStore: _fiatConversationStore));
|
||||
}
|
||||
|
||||
@action
|
||||
|
||||
@@ -85,8 +85,8 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
|
||||
currencies = wallet.balance.keys.toList();
|
||||
selectedCryptoCurrency =
|
||||
coinTypeToSpendFrom == UnspentCoinType.lightning ? CryptoCurrency.btcln : wallet.currency;
|
||||
hasMultipleTokens = isEVMWallet ||
|
||||
[WalletType.solana, WalletType.tron, WalletType.zano].contains(wallet.type);
|
||||
hasMultipleTokens =
|
||||
isEVMWallet || [WalletType.solana, WalletType.tron, WalletType.zano].contains(wallet.type);
|
||||
|
||||
for (final output in outputs) {
|
||||
output.updateWallet(wallet);
|
||||
@@ -195,7 +195,6 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
|
||||
if (isValidAddress) return null;
|
||||
}
|
||||
|
||||
|
||||
final results = await _adrResService.resolve(
|
||||
query: query,
|
||||
wallet: wallet,
|
||||
@@ -411,9 +410,7 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
|
||||
|
||||
final sp = RegExp(AddressValidator.silentPaymentAddressPatternMainnet);
|
||||
|
||||
final address = output.extractedAddress.isNotEmpty
|
||||
? output.extractedAddress
|
||||
: output.address;
|
||||
final address = output.extractedAddress.isNotEmpty ? output.extractedAddress : output.address;
|
||||
|
||||
return sp.hasMatch(address);
|
||||
}
|
||||
@@ -467,7 +464,7 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
|
||||
FiatCurrency get fiatCurrency => _settingsStore.fiatCurrency;
|
||||
|
||||
set fiatCurrency(FiatCurrency value) {
|
||||
_settingsStore.fiatCurrency = value;
|
||||
_settingsStore.fiatCurrency = value;
|
||||
}
|
||||
|
||||
List<FiatCurrency> fiatCurrencies;
|
||||
@@ -530,7 +527,7 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
|
||||
|
||||
@computed
|
||||
TransactionInfo? get transactionInfo {
|
||||
if(isEVMCompatibleChain(walletType)) {
|
||||
if (isEVMCompatibleChain(walletType)) {
|
||||
return wallet.transactionHistory.transactions[pendingTransaction?.evmTxHashFromRawHex];
|
||||
}
|
||||
if (walletType == WalletType.monero) {
|
||||
@@ -633,7 +630,9 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
|
||||
clearOutputs();
|
||||
|
||||
outputs.first.address = paymentRequest.address;
|
||||
outputs.first.parsedAddress = ParsedAddress(parsedAddressByCurrencyMap: {currency:paymentRequest.address}, handle: ocpRequest!.receiverName);
|
||||
outputs.first.parsedAddress = ParsedAddress(
|
||||
parsedAddressByCurrencyMap: {currency: paymentRequest.address},
|
||||
handle: ocpRequest!.receiverName);
|
||||
outputs.first.setCryptoAmount(paymentRequest.amount);
|
||||
outputs.first.note = ocpRequest!.receiverName;
|
||||
|
||||
@@ -694,13 +693,10 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
|
||||
final routerTo = trade.inputAddress;
|
||||
final routerData = trade.routerData;
|
||||
|
||||
|
||||
if (routerData != null && routerData != '0x') {
|
||||
final tokenContract = (trade.sourceTokenAddress ?? '').toLowerCase();
|
||||
final priority = _settingsStore.getPriority(
|
||||
walletType, chainId: selectedChainId);
|
||||
final routerValueWei = BigInt.tryParse(trade.routerValue ?? '0') ??
|
||||
BigInt.zero;
|
||||
final priority = _settingsStore.getPriority(walletType, chainId: selectedChainId);
|
||||
final routerValueWei = BigInt.tryParse(trade.routerValue ?? '0') ?? BigInt.zero;
|
||||
|
||||
if (routerTo == null || routerTo.isEmpty) {
|
||||
state = FailureState('Invalid router address');
|
||||
@@ -730,33 +726,26 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
|
||||
|
||||
// Smart Swap (Requires Approval)
|
||||
if (selector == swapAndExecuteSig) {
|
||||
final requiredAmount = BigInt.tryParse(
|
||||
(trade.sourceTokenAmountRaw ?? '0').replaceAll('n', '')) ??
|
||||
BigInt.zero;
|
||||
final requiredAmount =
|
||||
BigInt.tryParse((trade.sourceTokenAmountRaw ?? '0').replaceAll('n', '')) ??
|
||||
BigInt.zero;
|
||||
|
||||
final needsApproval = tokenContract.isNotEmpty &&
|
||||
requiredAmount > BigInt.zero
|
||||
? await evm!.isApprovalRequired(
|
||||
wallet, tokenContract, routerTo, requiredAmount)
|
||||
final needsApproval = tokenContract.isNotEmpty && requiredAmount > BigInt.zero
|
||||
? await evm!.isApprovalRequired(wallet, tokenContract, routerTo, requiredAmount)
|
||||
: false;
|
||||
|
||||
printV(
|
||||
'[Swaps.xyz sending flow] Approval required: $needsApproval for token ${trade
|
||||
.from?.title} ${trade.from?.tag ??
|
||||
''} with amount $requiredAmount');
|
||||
'[Swaps.xyz sending flow] Approval required: $needsApproval for token ${trade.from?.title} ${trade.from?.tag ?? ''} with amount $requiredAmount');
|
||||
|
||||
if (needsApproval) {
|
||||
// USDT Approval Flow (Special Case). We must reset allowance to 0 first.
|
||||
final isUSDTMainnet = selectedChainId == 1 &&
|
||||
tokenContract.toLowerCase() ==
|
||||
'0xdac17f958d2ee523a2206206994597c13d831ec7';
|
||||
tokenContract.toLowerCase() == '0xdac17f958d2ee523a2206206994597c13d831ec7';
|
||||
|
||||
if (isUSDTMainnet) {
|
||||
final currentAllowance = await evm!.getAllowance(
|
||||
wallet, tokenContract, routerTo);
|
||||
final currentAllowance = await evm!.getAllowance(wallet, tokenContract, routerTo);
|
||||
|
||||
if (currentAllowance != null &&
|
||||
currentAllowance > BigInt.zero) {
|
||||
if (currentAllowance != null && currentAllowance > BigInt.zero) {
|
||||
printV(
|
||||
'[Swaps.xyz sending flow] currentAllowance USDT: $currentAllowance. Resetting to 0 before setting new allowance.');
|
||||
|
||||
@@ -766,8 +755,7 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
|
||||
requiredAmount: BigInt.zero,
|
||||
// Approve 0
|
||||
sourceTokenDecimals: trade.sourceTokenDecimals,
|
||||
priority: priority
|
||||
);
|
||||
priority: priority);
|
||||
|
||||
if (resetTx != null) {
|
||||
await resetTx.commit();
|
||||
@@ -780,8 +768,7 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
|
||||
);
|
||||
|
||||
if (!resetConfirmed) {
|
||||
state = FailureState(
|
||||
'Failed to reset USDT allowance. Please try again.');
|
||||
state = FailureState('Failed to reset USDT allowance. Please try again.');
|
||||
return null;
|
||||
}
|
||||
printV(
|
||||
@@ -796,8 +783,7 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
|
||||
tokenContract: tokenContract,
|
||||
requiredAmount: requiredAmount,
|
||||
sourceTokenDecimals: trade.sourceTokenDecimals,
|
||||
priority: priority
|
||||
);
|
||||
priority: priority);
|
||||
|
||||
if (approvalTx == null) {
|
||||
state = FailureState('Failed to build approval transaction');
|
||||
@@ -808,8 +794,7 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
|
||||
|
||||
try {
|
||||
printV(
|
||||
'[Swaps.xyz sending flow] Submitting approval transaction for token ${trade
|
||||
.from?.title} ${trade.from?.tag ?? ''} ');
|
||||
'[Swaps.xyz sending flow] Submitting approval transaction for token ${trade.from?.title} ${trade.from?.tag ?? ''} ');
|
||||
await approvalTx.commit();
|
||||
|
||||
// Wait for the approval to be mined on-chain
|
||||
@@ -827,10 +812,8 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
|
||||
printV(
|
||||
'[Swaps.xyz sending flow] Approval transaction confirmed on-chain. Proceeding with swap execution.');
|
||||
} catch (e, s) {
|
||||
printV(
|
||||
'[Swaps.xyz sending flow] Approval transaction error: $e\n$s');
|
||||
state = FailureState(
|
||||
translateErrorMessage(e, wallet.type, wallet.currency));
|
||||
printV('[Swaps.xyz sending flow] Approval transaction error: $e\n$s');
|
||||
state = FailureState(translateErrorMessage(e, wallet.type, wallet.currency));
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -859,8 +842,7 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
|
||||
} catch (e, s) {
|
||||
printV('Swaps.xyz transaction error: $e\n$s');
|
||||
state = FailureState(
|
||||
'Failed to create Swaps.xyz transaction - ${translateErrorMessage(
|
||||
e, wallet.type, wallet.currency)}');
|
||||
'Failed to create Swaps.xyz transaction - ${translateErrorMessage(e, wallet.type, wallet.currency)}');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -902,14 +884,12 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
|
||||
|
||||
// Regular flow
|
||||
|
||||
|
||||
final isSendAll = outputs.any((output) => output.sendAll);
|
||||
|
||||
|
||||
if (!isSendAll) {
|
||||
final estimateTxAmountDouble = outputs.fold<double>(0, (acc, output) =>
|
||||
acc + (double.tryParse(output.cryptoAmount) ?? 0));
|
||||
if (estimateTxAmountDouble <= 0) throw Exception(
|
||||
'Amount must be greater than 0');
|
||||
final estimateTxAmountDouble = outputs.fold<double>(
|
||||
0, (acc, output) => acc + (double.tryParse(output.cryptoAmount) ?? 0));
|
||||
if (estimateTxAmountDouble <= 0) throw Exception('Amount must be greater than 0');
|
||||
}
|
||||
|
||||
pendingTransaction = await wallet.createTransaction(_credentials(provider));
|
||||
@@ -942,7 +922,6 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (wallet.type == WalletType.bitcoin) {
|
||||
final updatedOutputs = bitcoin!.updateOutputs(pendingTransaction!, outputs);
|
||||
|
||||
@@ -1030,7 +1009,6 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
|
||||
throw Exception("Pending transaction doesn't exist. It should not be happened.");
|
||||
}
|
||||
|
||||
|
||||
try {
|
||||
state = wallet.isHardwareWallet && walletType == WalletType.monero
|
||||
? IsAwaitingDeviceResponseState()
|
||||
@@ -1124,7 +1102,9 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
|
||||
}
|
||||
|
||||
// Immediate transaction update for EVM chains, Tron, and Nano
|
||||
if (isEVMWallet || [WalletType.bitcoin, WalletType.solana, WalletType.tron, WalletType.nano].contains(walletType)) {
|
||||
if (isEVMWallet ||
|
||||
[WalletType.bitcoin, WalletType.solana, WalletType.tron, WalletType.nano]
|
||||
.contains(walletType)) {
|
||||
Future.delayed(Duration(seconds: 4), () async {
|
||||
try {
|
||||
await Future.wait([
|
||||
@@ -1140,7 +1120,7 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
|
||||
// FIXME(malik) ideally, this should be done wallet-side.
|
||||
// it is required because evm, solana and tron don't actually save the transaction info when you send something.
|
||||
// instead, they rely on the tx to eventually get fetched at sync time, which can take a while
|
||||
if(isEVMWallet) {
|
||||
if (isEVMWallet) {
|
||||
wallet.transactionHistory.addOne(evm!.getTransactionInfo(
|
||||
id: pendingTransaction!.evmTxHashFromRawHex!,
|
||||
height: 0,
|
||||
@@ -1154,8 +1134,8 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
|
||||
chainId: wallet.chainId ?? 0,
|
||||
));
|
||||
}
|
||||
|
||||
if(walletType == WalletType.solana) {
|
||||
|
||||
if (walletType == WalletType.solana) {
|
||||
wallet.transactionHistory.addOne(solana!.getTransactionInfo(
|
||||
id: pendingTransaction!.id,
|
||||
blockTime: DateTime.now(),
|
||||
@@ -1211,7 +1191,7 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
|
||||
Future<void> updateWalletBalance() async => await wallet.updateBalance();
|
||||
|
||||
Future<void> _addTransactionDescription() async {
|
||||
String address = outputs.fold('', (acc, value) {
|
||||
String address = outputs.fold('', (acc, value) {
|
||||
final canonical = value.extractedAddress.trim().isNotEmpty
|
||||
? value.extractedAddress.trim()
|
||||
: value.address.trim();
|
||||
@@ -1360,15 +1340,15 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
|
||||
@computed
|
||||
int get maxMemoLength => _maxMemoLengths[wallet.type] ?? 9999999;
|
||||
|
||||
|
||||
ContactRecord? newContactAddress() {
|
||||
final Set<String> contactAddresses =
|
||||
Set.from(contactListViewModel.contacts.map((contact) => contact.address))
|
||||
..addAll(contactListViewModel.walletContacts.map((contact) => contact.address));
|
||||
|
||||
for (final output in outputs) {
|
||||
final address =
|
||||
output.isParsedAddress ? output.parsedAddress.parsedAddressByCurrencyMap[selectedCryptoCurrency] ?? '' : output.address;
|
||||
final address = output.isParsedAddress
|
||||
? output.parsedAddress.parsedAddressByCurrencyMap[selectedCryptoCurrency] ?? ''
|
||||
: output.address;
|
||||
|
||||
if (address.isNotEmpty &&
|
||||
!contactAddresses.contains(address) &&
|
||||
@@ -1400,14 +1380,16 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
|
||||
int attempts = 0;
|
||||
const int maxAttempts = 30; // ~60 seconds
|
||||
|
||||
printV('[Swaps.xyz sending flow] Starting allowance check. Target: $requiredAmount (Exact match: $waitForExactMatch)');
|
||||
printV(
|
||||
'[Swaps.xyz sending flow] Starting allowance check. Target: $requiredAmount (Exact match: $waitForExactMatch)');
|
||||
|
||||
while (attempts < maxAttempts) {
|
||||
try {
|
||||
final currentAllowance = await evm!.getAllowance(wallet, tokenContract, spender);
|
||||
|
||||
if (currentAllowance != null) {
|
||||
printV('[Swaps.xyz sending flow] Current Allowance: $currentAllowance / Target: $requiredAmount');
|
||||
printV(
|
||||
'[Swaps.xyz sending flow] Current Allowance: $currentAllowance / Target: $requiredAmount');
|
||||
|
||||
if (waitForExactMatch) {
|
||||
// For Reset (Target 0): We need it to be exactly 0 (or less, though it can't be negative)
|
||||
@@ -1443,17 +1425,16 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
|
||||
required TransactionPriority? priority,
|
||||
int? sourceTokenDecimals,
|
||||
}) async {
|
||||
|
||||
final erc20Token = wallet.balance.keys.whereType<Erc20Token>().firstWhere(
|
||||
(t) => t.contractAddress.toLowerCase() == tokenContract.toLowerCase(),
|
||||
orElse: () => Erc20Token(
|
||||
name: '',
|
||||
symbol: '',
|
||||
contractAddress: tokenContract,
|
||||
decimal: sourceTokenDecimals ?? 18,
|
||||
enabled: true,
|
||||
),
|
||||
);
|
||||
orElse: () => Erc20Token(
|
||||
name: '',
|
||||
symbol: '',
|
||||
contractAddress: tokenContract,
|
||||
decimal: sourceTokenDecimals ?? 18,
|
||||
enabled: true,
|
||||
),
|
||||
);
|
||||
|
||||
return await evm!.createTokenApproval(
|
||||
wallet,
|
||||
@@ -1461,7 +1442,7 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
|
||||
spender,
|
||||
priority,
|
||||
useBlinkProtection:
|
||||
canSupportBlinkProtection(selectedChainId) ? _settingsStore.useBlinkProtection : false,
|
||||
canSupportBlinkProtection(selectedChainId) ? _settingsStore.useBlinkProtection : false,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1646,9 +1627,9 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
|
||||
return S.current.tx_invalid_input;
|
||||
}
|
||||
|
||||
if(wallet.type == WalletType.bitcoin) {
|
||||
if (wallet.type == WalletType.bitcoin) {
|
||||
final lnError = getLightningErrorMessage(error);
|
||||
if(lnError != null) return lnError;
|
||||
if (lnError != null) return lnError;
|
||||
}
|
||||
|
||||
return errorMessage;
|
||||
@@ -1656,12 +1637,10 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
|
||||
|
||||
String? getLightningErrorMessage(Object error) {
|
||||
// TODO add more patterns
|
||||
Map<String, String> errorPatterns = {
|
||||
"insufficient funds": S.current.insufficient_funds_for_tx
|
||||
};
|
||||
Map<String, String> errorPatterns = {"insufficient funds": S.current.insufficient_funds_for_tx};
|
||||
|
||||
for(final pattern in errorPatterns.keys) {
|
||||
if(error.toString().contains(pattern)) return errorPatterns[pattern]!;
|
||||
for (final pattern in errorPatterns.keys) {
|
||||
if (error.toString().contains(pattern)) return errorPatterns[pattern]!;
|
||||
}
|
||||
|
||||
return null;
|
||||
@@ -1677,7 +1656,6 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
|
||||
|
||||
Future<void> registerSwapsXyzTransaction(Trade trade) async {
|
||||
try {
|
||||
|
||||
// register only for vmId is alt-vm or bridgeId is alt-vm (trade.needToRegisterInSwapXyz)
|
||||
final needToRegister = trade.needToRegisterInSwapXyz ?? false;
|
||||
if (!needToRegister) return;
|
||||
@@ -1688,9 +1666,7 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
|
||||
return;
|
||||
}
|
||||
|
||||
final txHash = pendingTransaction?.evmTxHashFromRawHex
|
||||
?? pendingTransaction?.id
|
||||
?? '';
|
||||
final txHash = pendingTransaction?.evmTxHashFromRawHex ?? pendingTransaction?.id ?? '';
|
||||
|
||||
if (txHash.isEmpty) {
|
||||
printV('SwapsXyz: transaction register: skipped (txHash empty)');
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import 'package:cake_wallet/core/execution_state.dart';
|
||||
|
||||
class IsDeviceSigningResponseState extends IsExecutingState {}
|
||||
|
||||
class IsAwaitingDeviceResponseState extends IsExecutingState {}
|
||||
|
||||
class TransactionCommitting extends ExecutionState {}
|
||||
|
||||
class TransactionCommitted extends ExecutionState {}
|
||||
|
||||
Reference in New Issue
Block a user