mirror of
https://github.com/trezor/trezor-wallet
synced 2025-01-24 06:51:23 +00:00
update actions and reducers
This commit is contained in:
parent
e4552b0998
commit
1379a2e745
@ -34,12 +34,8 @@ import type { Token } from 'reducers/TokensReducer';
|
||||
import type { NetworkToken } from 'reducers/LocalStorageReducer';
|
||||
|
||||
export type BlockchainAction = {
|
||||
type: typeof BLOCKCHAIN.START | typeof BLOCKCHAIN.CONNECTING,
|
||||
network: string,
|
||||
} | {
|
||||
type: typeof BLOCKCHAIN.STOP,
|
||||
network: string,
|
||||
};
|
||||
type: typeof BLOCKCHAIN.READY,
|
||||
}
|
||||
|
||||
export const discoverAccount = (device: TrezorDevice, xpub: string, network: string): PromiseAction<AccountDiscovery> => async (dispatch: Dispatch, getState: GetState): Promise<AccountDiscovery> => {
|
||||
// get data from connect
|
||||
@ -93,36 +89,22 @@ export const estimateGasLimit = (network: string, data: string, value: string, g
|
||||
export const onBlockMined = (network: string): PromiseAction<void> => async (dispatch: Dispatch, getState: GetState): Promise<void> => {
|
||||
|
||||
// try to resolve pending transactions
|
||||
// await dispatch( Web3Actions.resolvePendingTransactions(network) );
|
||||
await dispatch( Web3Actions.resolvePendingTransactions(network) );
|
||||
|
||||
// get network accounts
|
||||
// const accounts: Array<any> = getState().accounts.filter(a => a.network === network).map(a => {
|
||||
// return {
|
||||
// address: a.address,
|
||||
// block: a.block,
|
||||
// transactions: a.transactions
|
||||
// }
|
||||
// });
|
||||
const accounts: Array<any> = getState().accounts.filter(a => a.network === network);
|
||||
|
||||
// find out which account changed
|
||||
const response = await TrezorConnect.ethereumGetAccountInfo({
|
||||
accounts,
|
||||
coin: network,
|
||||
});
|
||||
|
||||
if (!response.success) {
|
||||
|
||||
} else {
|
||||
if (response.success) {
|
||||
response.payload.forEach((a, i) => {
|
||||
if (a.transactions > 0) {
|
||||
// dispatch( Web3Actions.updateAccount(accounts[i], a, network) )
|
||||
dispatch( Web3Actions.updateAccount(accounts[i], a, network) )
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
//return await dispatch( Web3Actions.estimateGasLimit(network, { to: '', data, value, gasPrice }) );
|
||||
console.warn("onBlockMined", response)
|
||||
}
|
||||
|
||||
export const onNotification = (payload: any): PromiseAction<void> => async (dispatch: Dispatch, getState: GetState): Promise<void> => {
|
||||
@ -177,4 +159,38 @@ export const subscribe = (network: string): PromiseAction<void> => async (dispat
|
||||
accounts: [],
|
||||
coin: network
|
||||
});
|
||||
}
|
||||
|
||||
// Conditionally subscribe to blockchain backend
|
||||
// called after TrezorConnect.init successfully emits TRANSPORT.START event
|
||||
// checks if there are discovery processes loaded from LocalStorage
|
||||
// if so starts subscription to proper networks
|
||||
export const init = (): PromiseAction<void> => async (dispatch: Dispatch, getState: GetState): Promise<void> => {
|
||||
if (getState().discovery.length > 0) {
|
||||
// get unique networks
|
||||
const networks: Array<string> = [];
|
||||
getState().discovery.forEach(discovery => {
|
||||
if (networks.indexOf(discovery.network) < 0) {
|
||||
networks.push(discovery.network);
|
||||
}
|
||||
});
|
||||
|
||||
// subscribe
|
||||
for (let i = 0; i < networks.length; i++) {
|
||||
await dispatch( subscribe(networks[i]) );
|
||||
}
|
||||
} else {
|
||||
await dispatch( subscribe('ropsten') );
|
||||
}
|
||||
|
||||
// continue wallet initialization
|
||||
dispatch({
|
||||
type: BLOCKCHAIN.READY
|
||||
});
|
||||
}
|
||||
|
||||
// Handle BLOCKCHAIN.ERROR event from TrezorConnect
|
||||
// disconnect and remove Web3 webscocket instance if exists
|
||||
export const error = (payload: any): PromiseAction<void> => async (dispatch: Dispatch, getState: GetState): Promise<void> => {
|
||||
dispatch( Web3Actions.disconnect(payload.coin) );
|
||||
}
|
@ -6,7 +6,7 @@ import * as DISCOVERY from 'actions/constants/discovery';
|
||||
import * as ACCOUNT from 'actions/constants/account';
|
||||
import * as NOTIFICATION from 'actions/constants/notification';
|
||||
import type {
|
||||
ThunkAction, AsyncAction, Action, GetState, Dispatch, TrezorDevice,
|
||||
ThunkAction, AsyncAction, PromiseAction, Action, GetState, Dispatch, TrezorDevice,
|
||||
} from 'flowtype';
|
||||
import type { Discovery, State } from 'reducers/DiscoveryReducer';
|
||||
import * as AccountsActions from './AccountsActions';
|
||||
@ -26,7 +26,7 @@ export type DiscoveryStartAction = {
|
||||
}
|
||||
|
||||
export type DiscoveryWaitingAction = {
|
||||
type: typeof DISCOVERY.WAITING_FOR_DEVICE | typeof DISCOVERY.WAITING_FOR_BACKEND,
|
||||
type: typeof DISCOVERY.WAITING_FOR_DEVICE | typeof DISCOVERY.WAITING_FOR_BLOCKCHAIN,
|
||||
device: TrezorDevice,
|
||||
network: string
|
||||
}
|
||||
@ -80,8 +80,12 @@ export const start = (device: TrezorDevice, network: string, ignoreCompleted?: b
|
||||
}
|
||||
|
||||
const blockchain = getState().blockchain.find(b => b.name === network);
|
||||
if (blockchain && !blockchain.connected) {
|
||||
console.error("NO BACKEND!") // TODO
|
||||
if (blockchain && !blockchain.connected && (!discoveryProcess || !discoveryProcess.completed)) {
|
||||
dispatch({
|
||||
type: DISCOVERY.WAITING_FOR_BLOCKCHAIN,
|
||||
device,
|
||||
network,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
@ -93,7 +97,7 @@ export const start = (device: TrezorDevice, network: string, ignoreCompleted?: b
|
||||
device,
|
||||
network,
|
||||
});
|
||||
} else if (discoveryProcess.interrupted || discoveryProcess.waitingForDevice) {
|
||||
} else if (discoveryProcess.interrupted || discoveryProcess.waitingForDevice || discoveryProcess.waitingForBlockchain) {
|
||||
// discovery cycle was interrupted
|
||||
// start from beginning
|
||||
dispatch(begin(device, network));
|
||||
@ -268,12 +272,19 @@ const finish = (device: TrezorDevice, discoveryProcess: Discovery): AsyncAction
|
||||
|
||||
}
|
||||
|
||||
export const reconnect = (network: string): PromiseAction<void> => async (dispatch: Dispatch, getState: GetState): Promise<void> => {
|
||||
await dispatch(BlockchainActions.subscribe(network));
|
||||
dispatch(restore());
|
||||
}
|
||||
|
||||
export const restore = (): ThunkAction => (dispatch: Dispatch, getState: GetState): void => {
|
||||
const selected = getState().wallet.selectedDevice;
|
||||
|
||||
if (selected && selected.connected && selected.features) {
|
||||
const discoveryProcess: ?Discovery = getState().discovery.find(d => d.deviceState === selected.state && d.waitingForDevice);
|
||||
const discoveryProcess: ?Discovery = getState().discovery.find(d => d.deviceState === selected.state && (d.interrupted || d.waitingForDevice || d.waitingForBlockchain));
|
||||
console.warn("AAAA2")
|
||||
if (discoveryProcess) {
|
||||
console.warn("AAAA3", discoveryProcess)
|
||||
dispatch(start(selected, discoveryProcess.network));
|
||||
}
|
||||
}
|
||||
|
@ -9,7 +9,6 @@ import { getDuplicateInstanceNumber } from 'reducers/utils';
|
||||
|
||||
import { push } from 'react-router-redux';
|
||||
|
||||
|
||||
import type {
|
||||
DeviceMessage,
|
||||
DeviceMessageType,
|
||||
@ -129,8 +128,6 @@ export const init = (): AsyncAction => async (dispatch: Dispatch, getState: GetS
|
||||
// $FlowIssue LOCAL not declared
|
||||
window.__TREZOR_CONNECT_SRC = typeof LOCAL === 'string' ? LOCAL : 'https://connect.trezor.io/5/';
|
||||
// window.__TREZOR_CONNECT_SRC = typeof LOCAL === 'string' ? LOCAL : 'https://sisyfos.trezor.io/connect/';
|
||||
//window.__TREZOR_CONNECT_SRC = 'https://sisyfos.trezor.io/connect/';
|
||||
// window.__TREZOR_CONNECT_SRC = 'https://localhost:8088/';
|
||||
|
||||
try {
|
||||
await TrezorConnect.init({
|
||||
|
@ -16,6 +16,7 @@ import * as PENDING from 'actions/constants/pendingTx';
|
||||
import type {
|
||||
Dispatch,
|
||||
GetState,
|
||||
ThunkAction,
|
||||
AsyncAction,
|
||||
PromiseAction,
|
||||
AccountDiscovery,
|
||||
@ -23,6 +24,7 @@ import type {
|
||||
EthereumPreparedTx
|
||||
} from 'flowtype';
|
||||
|
||||
import type { EthereumAccount } from 'trezor-connect';
|
||||
import type { Account } from 'reducers/AccountsReducer';
|
||||
import type { PendingTx } from 'reducers/PendingTxReducer';
|
||||
import type { Web3Instance } from 'reducers/Web3Reducer';
|
||||
@ -48,7 +50,7 @@ export type Web3Action = {
|
||||
} | {
|
||||
type: typeof WEB3.START,
|
||||
} | {
|
||||
type: typeof WEB3.CREATE,
|
||||
type: typeof WEB3.CREATE | typeof WEB3.DISCONNECT,
|
||||
instance: Web3Instance
|
||||
} | Web3UpdateBlockAction
|
||||
| Web3UpdateGasPriceAction;
|
||||
@ -108,13 +110,8 @@ export const initWeb3 = (network: string, urlIndex: number = 0): PromiseAction<W
|
||||
}
|
||||
|
||||
const onEnd = async () => {
|
||||
web3.currentProvider.removeAllListeners('connect');
|
||||
web3.currentProvider.removeAllListeners('end');
|
||||
web3.currentProvider.removeAllListeners('error');
|
||||
web3.currentProvider.reset();
|
||||
// if (web3.eth)
|
||||
// web3.eth.clearSubscriptions();
|
||||
|
||||
web3.currentProvider.reset();
|
||||
const instance = getState().web3.find(w3 => w3.network === network);
|
||||
|
||||
if (instance && instance.web3.currentProvider.connected) {
|
||||
@ -263,14 +260,25 @@ export const getTxInput = (): PromiseAction<void> => async (dispatch: Dispatch,
|
||||
}
|
||||
|
||||
|
||||
export const updateAccount = (account: Account, newAccount: AccountDiscovery, network: string): PromiseAction<void> => async (dispatch: Dispatch, getState: GetState): Promise<void> => {
|
||||
export const updateAccount = (account: Account, newAccount: EthereumAccount, network: string): PromiseAction<void> => async (dispatch: Dispatch, getState: GetState): Promise<void> => {
|
||||
const instance: Web3Instance = await dispatch( initWeb3(network) );
|
||||
const balance = await instance.web3.eth.getBalance(account.address);
|
||||
const nonce = await instance.web3.eth.getTransactionCount(account.address);
|
||||
|
||||
dispatch( AccountsActions.update( { ...account, ...newAccount, balance: EthereumjsUnits.convert(balance, 'wei', 'ether'), nonce }) );
|
||||
// TODO update tokens for this account
|
||||
|
||||
|
||||
// update tokens for this account
|
||||
const tokens = getState().tokens.filter(t => t.network === account.network && t.ethAddress === account.address);
|
||||
for (const token of tokens) {
|
||||
const balance = await dispatch( getTokenBalance(token) );
|
||||
// const newBalance: string = balance.dividedBy(Math.pow(10, token.decimals)).toString(10);
|
||||
if (balance !== token.balance) {
|
||||
dispatch(TokenActions.setBalance(
|
||||
token.address,
|
||||
token.ethAddress,
|
||||
balance,
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const getTokenInfo = (address: string, network: string): PromiseAction<NetworkToken> => async (dispatch: Dispatch, getState: GetState): Promise<NetworkToken> => {
|
||||
@ -337,48 +345,18 @@ export const estimateGasLimit = (network: string, options: EstimateGasOptions):
|
||||
return limit;
|
||||
};
|
||||
|
||||
// export const prepareTx = (tx: EthereumTxRequest): PromiseAction<EthereumPreparedTx> => async (dispatch: Dispatch, getState: GetState): Promise<EthereumPreparedTx> => {
|
||||
|
||||
// const instance = await dispatch( initWeb3(tx.network) );
|
||||
// const token = tx.token;
|
||||
// let data: string = `0x${tx.data}`; // TODO: check if already prefixed
|
||||
// let value: string = instance.web3.utils.toHex( EthereumjsUnits.convert(tx.amount, 'ether', 'wei') );
|
||||
// let to: string = tx.to;
|
||||
|
||||
// if (token) {
|
||||
// // smart contract transaction
|
||||
// const contract = instance.erc20.clone();
|
||||
// contract.options.address = token.address;
|
||||
// const tokenAmount: string = new BigNumber(tx.amount).times(Math.pow(10, token.decimals)).toString(10);
|
||||
// data = instance.erc20.methods.transfer(to, tokenAmount).encodeABI();
|
||||
// value = '0x00';
|
||||
// to = token.address;
|
||||
// }
|
||||
|
||||
// return {
|
||||
// to,
|
||||
// value,
|
||||
// data,
|
||||
// chainId: instance.chainId,
|
||||
// nonce: instance.web3.utils.toHex(tx.nonce),
|
||||
// gasLimit: instance.web3.utils.toHex(tx.gasLimit),
|
||||
// gasPrice: instance.web3.utils.toHex( EthereumjsUnits.convert(tx.gasPrice, 'gwei', 'wei') ),
|
||||
// r: '',
|
||||
// s: '',
|
||||
// v: '',
|
||||
// }
|
||||
// };
|
||||
|
||||
// export const pushTx = (network: string, tx: EthereumPreparedTx): PromiseAction<string> => async (dispatch: Dispatch, getState: GetState): Promise<string> => {
|
||||
// const instance = await dispatch( initWeb3(network) );
|
||||
// const ethTx = new EthereumjsTx(tx);
|
||||
// const serializedTx = `0x${ethTx.serialize().toString('hex')}`;
|
||||
|
||||
// return new Promise((resolve, reject) => {
|
||||
// instance.web3.eth.sendSignedTransaction(serializedTx)
|
||||
// .on('error', error => reject(error))
|
||||
// .once('transactionHash', (hash: string) => {
|
||||
// resolve(hash);
|
||||
// });
|
||||
// });
|
||||
// };
|
||||
export const disconnect = (network: string): ThunkAction => (dispatch: Dispatch, getState: GetState): void => {
|
||||
// check if Web3 was already initialized
|
||||
const instance = getState().web3.find(w3 => w3.network === network);
|
||||
if (instance) {
|
||||
// reset current connection
|
||||
instance.web3.currentProvider.reset();
|
||||
instance.web3.currentProvider.connection.close();
|
||||
|
||||
// remove instance from reducer
|
||||
dispatch({
|
||||
type: WEB3.DISCONNECT,
|
||||
instance
|
||||
});
|
||||
}
|
||||
};
|
||||
|
@ -29,7 +29,7 @@ export type Discovery = {
|
||||
interrupted: boolean;
|
||||
completed: boolean;
|
||||
waitingForDevice: boolean;
|
||||
waitingForBackend: boolean;
|
||||
waitingForBlockchain: boolean;
|
||||
}
|
||||
|
||||
export type State = Array<Discovery>;
|
||||
@ -53,7 +53,7 @@ const start = (state: State, action: DiscoveryStartAction): State => {
|
||||
interrupted: false,
|
||||
completed: false,
|
||||
waitingForDevice: false,
|
||||
waitingForBackend: false,
|
||||
waitingForBlockchain: false,
|
||||
};
|
||||
|
||||
const newState: State = [...state];
|
||||
@ -96,6 +96,7 @@ const stop = (state: State, action: DiscoveryStopAction): State => {
|
||||
if (d.deviceState === action.device.state && !d.completed) {
|
||||
d.interrupted = true;
|
||||
d.waitingForDevice = false;
|
||||
d.waitingForBlockchain = false;
|
||||
}
|
||||
return d;
|
||||
});
|
||||
@ -114,7 +115,7 @@ const waitingForDevice = (state: State, action: DiscoveryWaitingAction): State =
|
||||
interrupted: false,
|
||||
completed: false,
|
||||
waitingForDevice: true,
|
||||
waitingForBackend: false,
|
||||
waitingForBlockchain: false,
|
||||
};
|
||||
|
||||
const index: number = findIndex(state, action.network, deviceState);
|
||||
@ -128,7 +129,7 @@ const waitingForDevice = (state: State, action: DiscoveryWaitingAction): State =
|
||||
return newState;
|
||||
};
|
||||
|
||||
const waitingForBackend = (state: State, action: DiscoveryWaitingAction): State => {
|
||||
const waitingForBlockchain = (state: State, action: DiscoveryWaitingAction): State => {
|
||||
const deviceState: string = action.device.state || '0';
|
||||
const instance: Discovery = {
|
||||
network: action.network,
|
||||
@ -141,7 +142,7 @@ const waitingForBackend = (state: State, action: DiscoveryWaitingAction): State
|
||||
interrupted: false,
|
||||
completed: false,
|
||||
waitingForDevice: false,
|
||||
waitingForBackend: true,
|
||||
waitingForBlockchain: true,
|
||||
};
|
||||
|
||||
const index: number = findIndex(state, action.network, deviceState);
|
||||
@ -167,8 +168,8 @@ export default function discovery(state: State = initialState, action: Action):
|
||||
return complete(state, action);
|
||||
case DISCOVERY.WAITING_FOR_DEVICE:
|
||||
return waitingForDevice(state, action);
|
||||
case DISCOVERY.WAITING_FOR_BACKEND:
|
||||
return waitingForBackend(state, action);
|
||||
case DISCOVERY.WAITING_FOR_BLOCKCHAIN:
|
||||
return waitingForBlockchain(state, action);
|
||||
case DISCOVERY.FROM_STORAGE:
|
||||
return action.payload.map((d) => {
|
||||
const hdKey: HDKey = new HDKey();
|
||||
@ -179,7 +180,7 @@ export default function discovery(state: State = initialState, action: Action):
|
||||
hdKey,
|
||||
interrupted: false,
|
||||
waitingForDevice: false,
|
||||
waitingForBackend: false,
|
||||
waitingForBlockchain: false,
|
||||
};
|
||||
});
|
||||
case CONNECT.FORGET:
|
||||
|
@ -22,23 +22,24 @@ export type State = Array<PendingTx>;
|
||||
|
||||
const initialState: State = [];
|
||||
|
||||
// const add01 = (state: State, action: SendTxAction): State => {
|
||||
// const newState = [...state];
|
||||
// newState.push({
|
||||
// id: action.txid,
|
||||
// network: action.account.network,
|
||||
// currency: action.selectedCurrency,
|
||||
// amount: action.amount,
|
||||
// total: action.total,
|
||||
// tx: action.tx,
|
||||
// nonce: action.nonce,
|
||||
// address: action.account.address,
|
||||
// rejected: false,
|
||||
// });
|
||||
// return newState;
|
||||
// };
|
||||
const add = (state: State, action: SendTxAction): State => {
|
||||
const newState = [...state];
|
||||
newState.push({
|
||||
type: 'send',
|
||||
id: action.txid,
|
||||
network: action.account.network,
|
||||
currency: action.selectedCurrency,
|
||||
amount: action.amount,
|
||||
total: action.total,
|
||||
tx: action.tx,
|
||||
nonce: action.nonce,
|
||||
address: action.account.address,
|
||||
rejected: false,
|
||||
});
|
||||
return newState;
|
||||
};
|
||||
|
||||
const add = (state: State, payload: any): State => {
|
||||
const add_NEW = (state: State, payload: any): State => {
|
||||
const newState = [...state];
|
||||
newState.push(payload);
|
||||
return newState;
|
||||
@ -55,11 +56,11 @@ const reject = (state: State, id: string): State => state.map((tx) => {
|
||||
|
||||
export default function pending(state: State = initialState, action: Action): State {
|
||||
switch (action.type) {
|
||||
// case SEND.TX_COMPLETE:
|
||||
// return add(state, action);
|
||||
case SEND.TX_COMPLETE:
|
||||
return add(state, action);
|
||||
|
||||
case PENDING.ADD:
|
||||
return add(state, action.payload);
|
||||
// case PENDING.ADD:
|
||||
// return add(state, action.payload);
|
||||
case PENDING.TX_RESOLVED:
|
||||
return remove(state, action.tx.id);
|
||||
case PENDING.TX_NOT_FOUND:
|
||||
|
@ -51,6 +51,13 @@ const updateGasPrice = (state: State, action: Web3UpdateGasPriceAction): State =
|
||||
return newState;
|
||||
};
|
||||
|
||||
const disconnect = (state: State, instance: Web3Instance): State => {
|
||||
const index: number = state.indexOf(instance);
|
||||
const newState: Array<Web3Instance> = [...state];
|
||||
newState.splice(index, 1);
|
||||
return newState;
|
||||
};
|
||||
|
||||
export default function web3(state: State = initialState, action: Action): State {
|
||||
switch (action.type) {
|
||||
case WEB3.CREATE:
|
||||
@ -59,6 +66,8 @@ export default function web3(state: State = initialState, action: Action): State
|
||||
return updateLatestBlock(state, action);
|
||||
case WEB3.GAS_PRICE_UPDATED:
|
||||
return updateGasPrice(state, action);
|
||||
case WEB3.DISCONNECT:
|
||||
return disconnect(state, action.instance);
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
|
@ -10,6 +10,7 @@ import * as BlockchainActions from 'actions/BlockchainActions';
|
||||
import * as ModalActions from 'actions/ModalActions';
|
||||
import * as STORAGE from 'actions/constants/localStorage';
|
||||
import * as CONNECT from 'actions/constants/TrezorConnect';
|
||||
import { READY as BLOCKCHAIN_READY } from 'actions/constants/blockchain';
|
||||
|
||||
import type {
|
||||
Middleware,
|
||||
@ -32,10 +33,9 @@ const TrezorConnectService: Middleware = (api: MiddlewareAPI) => (next: Middlewa
|
||||
// TODO: check if modal is open
|
||||
// api.dispatch( push('/') );
|
||||
} else if (action.type === TRANSPORT.START) {
|
||||
api.dispatch(BlockchainActions.init());
|
||||
} else if (action.type === BLOCKCHAIN_READY) {
|
||||
api.dispatch(TrezorConnectActions.postInit());
|
||||
|
||||
api.dispatch( BlockchainActions.subscribe('ropsten') );
|
||||
|
||||
} else if (action.type === DEVICE.DISCONNECT) {
|
||||
api.dispatch(TrezorConnectActions.deviceDisconnect(action.device));
|
||||
} else if (action.type === CONNECT.REMEMBER_REQUEST) {
|
||||
@ -64,9 +64,11 @@ const TrezorConnectService: Middleware = (api: MiddlewareAPI) => (next: Middlewa
|
||||
} else if (action.type === CONNECT.COIN_CHANGED) {
|
||||
api.dispatch(TrezorConnectActions.coinChanged(action.payload.network));
|
||||
} else if (action.type === BLOCKCHAIN.BLOCK) {
|
||||
// api.dispatch(BlockchainActions.onBlockMined(action.payload.coin));
|
||||
api.dispatch(BlockchainActions.onBlockMined(action.payload.coin));
|
||||
} else if (action.type === BLOCKCHAIN.NOTIFICATION) {
|
||||
// api.dispatch(BlockchainActions.onNotification(action.payload));
|
||||
} else if (action.type === BLOCKCHAIN.ERROR) {
|
||||
api.dispatch( BlockchainActions.error(action.payload) );
|
||||
}
|
||||
|
||||
return action;
|
||||
|
Loading…
Reference in New Issue
Block a user