Refactor contracts-* exports so none in test dir so npmignore works as intended
This commit is contained in:
		
							
								
								
									
										423
									
								
								contracts/asset-proxy/src/erc1155_proxy_wrapper.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										423
									
								
								contracts/asset-proxy/src/erc1155_proxy_wrapper.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,423 @@ | ||||
| import { DevUtilsContract } from '@0x/contracts-dev-utils'; | ||||
| import { artifacts as erc1155Artifacts, ERC1155MintableContract, Erc1155Wrapper } from '@0x/contracts-erc1155'; | ||||
| import { | ||||
|     constants, | ||||
|     ERC1155FungibleHoldingsByOwner, | ||||
|     ERC1155HoldingsByOwner, | ||||
|     ERC1155NonFungibleHoldingsByOwner, | ||||
|     LogDecoder, | ||||
|     txDefaults, | ||||
| } from '@0x/contracts-test-utils'; | ||||
| import { BigNumber } from '@0x/utils'; | ||||
| import { Web3Wrapper } from '@0x/web3-wrapper'; | ||||
| import { Provider, TransactionReceiptWithDecodedLogs } from 'ethereum-types'; | ||||
| import * as _ from 'lodash'; | ||||
|  | ||||
| import { artifacts } from './artifacts'; | ||||
|  | ||||
| import { ERC1155ProxyContract, IAssetProxyContract } from './wrappers'; | ||||
|  | ||||
| export class ERC1155ProxyWrapper { | ||||
|     private readonly _tokenOwnerAddresses: string[]; | ||||
|     private readonly _fungibleTokenIds: string[]; | ||||
|     private readonly _nonFungibleTokenIds: string[]; | ||||
|     private readonly _nfts: Array<{ id: BigNumber; tokenId: BigNumber }>; | ||||
|     private readonly _contractOwnerAddress: string; | ||||
|     private readonly _web3Wrapper: Web3Wrapper; | ||||
|     private readonly _provider: Provider; | ||||
|     private readonly _logDecoder: LogDecoder; | ||||
|     private readonly _dummyTokenWrappers: Erc1155Wrapper[]; | ||||
|     private readonly _assetProxyInterface: IAssetProxyContract; | ||||
|     private readonly _devUtils: DevUtilsContract; | ||||
|     private _proxyContract?: ERC1155ProxyContract; | ||||
|     private _proxyIdIfExists?: string; | ||||
|     private _initialTokenIdsByOwner: ERC1155HoldingsByOwner = { fungible: {}, nonFungible: {} }; | ||||
|  | ||||
|     constructor(provider: Provider, tokenOwnerAddresses: string[], contractOwnerAddress: string) { | ||||
|         this._web3Wrapper = new Web3Wrapper(provider); | ||||
|         this._provider = provider; | ||||
|         const allArtifacts = _.merge(artifacts, erc1155Artifacts); | ||||
|         this._logDecoder = new LogDecoder(this._web3Wrapper, allArtifacts); | ||||
|         this._dummyTokenWrappers = []; | ||||
|         this._assetProxyInterface = new IAssetProxyContract(constants.NULL_ADDRESS, provider); | ||||
|         this._devUtils = new DevUtilsContract(constants.NULL_ADDRESS, provider); | ||||
|         this._tokenOwnerAddresses = tokenOwnerAddresses; | ||||
|         this._contractOwnerAddress = contractOwnerAddress; | ||||
|         this._fungibleTokenIds = []; | ||||
|         this._nonFungibleTokenIds = []; | ||||
|         this._nfts = []; | ||||
|     } | ||||
|     /** | ||||
|      * @dev Deploys dummy ERC1155 contracts | ||||
|      * @return An array of ERC1155 wrappers; one for each deployed contract. | ||||
|      */ | ||||
|     public async deployDummyContractsAsync(): Promise<Erc1155Wrapper[]> { | ||||
|         // tslint:disable-next-line:no-unused-variable | ||||
|         for (const i of _.times(constants.NUM_DUMMY_ERC1155_CONTRACTS_TO_DEPLOY)) { | ||||
|             const erc1155Contract = await ERC1155MintableContract.deployFrom0xArtifactAsync( | ||||
|                 erc1155Artifacts.ERC1155Mintable, | ||||
|                 this._provider, | ||||
|                 txDefaults, | ||||
|                 artifacts, | ||||
|             ); | ||||
|             const erc1155Wrapper = new Erc1155Wrapper(erc1155Contract, this._provider, this._contractOwnerAddress); | ||||
|             this._dummyTokenWrappers.push(erc1155Wrapper); | ||||
|         } | ||||
|         return this._dummyTokenWrappers; | ||||
|     } | ||||
|     /** | ||||
|      * @dev Deploys the ERC1155 proxy | ||||
|      * @return Deployed ERC1155 proxy contract instance | ||||
|      */ | ||||
|     public async deployProxyAsync(): Promise<ERC1155ProxyContract> { | ||||
|         this._proxyContract = await ERC1155ProxyContract.deployFrom0xArtifactAsync( | ||||
|             artifacts.ERC1155Proxy, | ||||
|             this._provider, | ||||
|             txDefaults, | ||||
|             artifacts, | ||||
|         ); | ||||
|         this._proxyIdIfExists = await this._proxyContract.getProxyId.callAsync(); | ||||
|         return this._proxyContract; | ||||
|     } | ||||
|     /** | ||||
|      * @dev Gets the ERC1155 proxy id | ||||
|      */ | ||||
|     public getProxyId(): string { | ||||
|         this._validateProxyContractExistsOrThrow(); | ||||
|         return this._proxyIdIfExists as string; | ||||
|     } | ||||
|     /** | ||||
|      * @dev generates abi-encoded tx data for transferring erc1155 fungible/non-fungible tokens. | ||||
|      * @param from source address | ||||
|      * @param to destination address | ||||
|      * @param contractAddress address of erc155 contract | ||||
|      * @param tokensToTransfer array of erc1155 tokens to transfer | ||||
|      * @param valuesToTransfer array of corresponding values for each erc1155 token to transfer | ||||
|      * @param valueMultiplier each value in `valuesToTransfer` is multiplied by this | ||||
|      * @param receiverCallbackData callback data if `to` is a contract | ||||
|      * @param authorizedSender sender of `transferFrom` transaction | ||||
|      * @param extraData extra data to append to `transferFrom` transaction. Optional. | ||||
|      * @return abi encoded tx data. | ||||
|      */ | ||||
|     public async getTransferFromAbiEncodedTxDataAsync( | ||||
|         from: string, | ||||
|         to: string, | ||||
|         contractAddress: string, | ||||
|         tokensToTransfer: BigNumber[], | ||||
|         valuesToTransfer: BigNumber[], | ||||
|         valueMultiplier: BigNumber, | ||||
|         receiverCallbackData: string, | ||||
|         authorizedSender: string, | ||||
|         assetData_?: string, | ||||
|     ): Promise<string> { | ||||
|         this._validateProxyContractExistsOrThrow(); | ||||
|         const assetData = | ||||
|             assetData_ === undefined | ||||
|                 ? await this._devUtils.encodeERC1155AssetData.callAsync( | ||||
|                       contractAddress, | ||||
|                       tokensToTransfer, | ||||
|                       valuesToTransfer, | ||||
|                       receiverCallbackData, | ||||
|                   ) | ||||
|                 : assetData_; | ||||
|         const data = this._assetProxyInterface.transferFrom.getABIEncodedTransactionData( | ||||
|             assetData, | ||||
|             from, | ||||
|             to, | ||||
|             valueMultiplier, | ||||
|         ); | ||||
|         return data; | ||||
|     } | ||||
|     /** | ||||
|      * @dev transfers erc1155 fungible/non-fungible tokens. | ||||
|      * @param txData: abi-encoded tx data | ||||
|      * @param authorizedSender sender of `transferFrom` transaction | ||||
|      */ | ||||
|     public async transferFromRawAsync( | ||||
|         txData: string, | ||||
|         authorizedSender: string, | ||||
|     ): Promise<TransactionReceiptWithDecodedLogs> { | ||||
|         const txHash = await this._web3Wrapper.sendTransactionAsync({ | ||||
|             to: (this._proxyContract as ERC1155ProxyContract).address, | ||||
|             data: txData, | ||||
|             from: authorizedSender, | ||||
|             gas: 300000, | ||||
|         }); | ||||
|         const txReceipt = await this._logDecoder.getTxWithDecodedLogsAsync(txHash); | ||||
|         return txReceipt; | ||||
|     } | ||||
|     /** | ||||
|      * @dev transfers erc1155 fungible/non-fungible tokens. | ||||
|      * @param from source address | ||||
|      * @param to destination address | ||||
|      * @param contractAddress address of erc155 contract | ||||
|      * @param tokensToTransfer array of erc1155 tokens to transfer | ||||
|      * @param valuesToTransfer array of corresponding values for each erc1155 token to transfer | ||||
|      * @param valueMultiplier each value in `valuesToTransfer` is multiplied by this | ||||
|      * @param receiverCallbackData callback data if `to` is a contract | ||||
|      * @param authorizedSender sender of `transferFrom` transaction | ||||
|      * @param extraData extra data to append to `transferFrom` transaction. Optional. | ||||
|      * @return tranasction hash. | ||||
|      */ | ||||
|     public async transferFromAsync( | ||||
|         from: string, | ||||
|         to: string, | ||||
|         contractAddress: string, | ||||
|         tokensToTransfer: BigNumber[], | ||||
|         valuesToTransfer: BigNumber[], | ||||
|         valueMultiplier: BigNumber, | ||||
|         receiverCallbackData: string, | ||||
|         authorizedSender: string, | ||||
|         assetData_?: string, | ||||
|     ): Promise<TransactionReceiptWithDecodedLogs> { | ||||
|         this._validateProxyContractExistsOrThrow(); | ||||
|         const assetData = | ||||
|             assetData_ === undefined | ||||
|                 ? await this._devUtils.encodeERC1155AssetData.callAsync( | ||||
|                       contractAddress, | ||||
|                       tokensToTransfer, | ||||
|                       valuesToTransfer, | ||||
|                       receiverCallbackData, | ||||
|                   ) | ||||
|                 : assetData_; | ||||
|         const data = this._assetProxyInterface.transferFrom.getABIEncodedTransactionData( | ||||
|             assetData, | ||||
|             from, | ||||
|             to, | ||||
|             valueMultiplier, | ||||
|         ); | ||||
|         const txHash = await this._web3Wrapper.sendTransactionAsync({ | ||||
|             to: (this._proxyContract as ERC1155ProxyContract).address, | ||||
|             data, | ||||
|             from: authorizedSender, | ||||
|             gas: 300000, | ||||
|         }); | ||||
|         const txReceipt = await this._logDecoder.getTxWithDecodedLogsAsync(txHash); | ||||
|         return txReceipt; | ||||
|     } | ||||
|     /** | ||||
|      * @dev For each deployed ERC1155 contract, this function mints a set of fungible/non-fungible | ||||
|      *      tokens for each token owner address (`_tokenOwnerAddresses`). | ||||
|      * @return Balances of each token owner, across all ERC1155 contracts and tokens. | ||||
|      */ | ||||
|     public async setBalancesAndAllowancesAsync(): Promise<ERC1155HoldingsByOwner> { | ||||
|         this._validateDummyTokenContractsExistOrThrow(); | ||||
|         this._validateProxyContractExistsOrThrow(); | ||||
|         this._initialTokenIdsByOwner = { | ||||
|             fungible: {}, | ||||
|             nonFungible: {}, | ||||
|         }; | ||||
|         const fungibleHoldingsByOwner: ERC1155FungibleHoldingsByOwner = {}; | ||||
|         const nonFungibleHoldingsByOwner: ERC1155NonFungibleHoldingsByOwner = {}; | ||||
|         // Set balances accordingly | ||||
|         for (const dummyWrapper of this._dummyTokenWrappers) { | ||||
|             const dummyAddress = dummyWrapper.getContract().address; | ||||
|             // tslint:disable-next-line:no-unused-variable | ||||
|             for (const i of _.times(constants.NUM_ERC1155_FUNGIBLE_TOKENS_MINT)) { | ||||
|                 // Create a fungible token | ||||
|                 const tokenId = await dummyWrapper.mintFungibleTokensAsync( | ||||
|                     this._tokenOwnerAddresses, | ||||
|                     constants.INITIAL_ERC1155_FUNGIBLE_BALANCE, | ||||
|                 ); | ||||
|                 const tokenIdAsString = tokenId.toString(); | ||||
|                 this._fungibleTokenIds.push(tokenIdAsString); | ||||
|                 // Mint tokens for each owner for this token | ||||
|                 for (const tokenOwnerAddress of this._tokenOwnerAddresses) { | ||||
|                     // tslint:disable-next-line:no-unused-variable | ||||
|                     if (fungibleHoldingsByOwner[tokenOwnerAddress] === undefined) { | ||||
|                         fungibleHoldingsByOwner[tokenOwnerAddress] = {}; | ||||
|                     } | ||||
|                     if (fungibleHoldingsByOwner[tokenOwnerAddress][dummyAddress] === undefined) { | ||||
|                         fungibleHoldingsByOwner[tokenOwnerAddress][dummyAddress] = {}; | ||||
|                     } | ||||
|                     fungibleHoldingsByOwner[tokenOwnerAddress][dummyAddress][tokenIdAsString] = | ||||
|                         constants.INITIAL_ERC1155_FUNGIBLE_BALANCE; | ||||
|                     await dummyWrapper.setApprovalForAllAsync( | ||||
|                         tokenOwnerAddress, | ||||
|                         (this._proxyContract as ERC1155ProxyContract).address, | ||||
|                         true, | ||||
|                     ); | ||||
|                 } | ||||
|             } | ||||
|             // Non-fungible tokens | ||||
|             // tslint:disable-next-line:no-unused-variable | ||||
|             for (const j of _.times(constants.NUM_ERC1155_NONFUNGIBLE_TOKENS_MINT)) { | ||||
|                 const [tokenId, nftIds] = await dummyWrapper.mintNonFungibleTokensAsync(this._tokenOwnerAddresses); | ||||
|                 const tokenIdAsString = tokenId.toString(); | ||||
|                 this._nonFungibleTokenIds.push(tokenIdAsString); | ||||
|                 _.each(this._tokenOwnerAddresses, async (tokenOwnerAddress: string, i: number) => { | ||||
|                     if (nonFungibleHoldingsByOwner[tokenOwnerAddress] === undefined) { | ||||
|                         nonFungibleHoldingsByOwner[tokenOwnerAddress] = {}; | ||||
|                     } | ||||
|                     if (nonFungibleHoldingsByOwner[tokenOwnerAddress][dummyAddress] === undefined) { | ||||
|                         nonFungibleHoldingsByOwner[tokenOwnerAddress][dummyAddress] = {}; | ||||
|                     } | ||||
|                     if (nonFungibleHoldingsByOwner[tokenOwnerAddress][dummyAddress][tokenIdAsString] === undefined) { | ||||
|                         nonFungibleHoldingsByOwner[tokenOwnerAddress][dummyAddress][tokenIdAsString] = []; | ||||
|                     } | ||||
|                     this._nfts.push({ id: nftIds[i], tokenId }); | ||||
|                     nonFungibleHoldingsByOwner[tokenOwnerAddress][dummyAddress][tokenIdAsString].push(nftIds[i]); | ||||
|                     await dummyWrapper.setApprovalForAllAsync( | ||||
|                         tokenOwnerAddress, | ||||
|                         (this._proxyContract as ERC1155ProxyContract).address, | ||||
|                         true, | ||||
|                     ); | ||||
|                 }); | ||||
|             } | ||||
|         } | ||||
|         this._initialTokenIdsByOwner = { | ||||
|             fungible: fungibleHoldingsByOwner, | ||||
|             nonFungible: nonFungibleHoldingsByOwner, | ||||
|         }; | ||||
|         return this._initialTokenIdsByOwner; | ||||
|     } | ||||
|     /** | ||||
|      * @dev For each deployed ERC1155 contract, this function quieries the set of fungible/non-fungible | ||||
|      *      tokens for each token owner address (`_tokenOwnerAddresses`). | ||||
|      * @return Balances of each token owner, across all ERC1155 contracts and tokens. | ||||
|      */ | ||||
|     public async getBalancesAsync(): Promise<ERC1155HoldingsByOwner> { | ||||
|         this._validateDummyTokenContractsExistOrThrow(); | ||||
|         this._validateBalancesAndAllowancesSetOrThrow(); | ||||
|         const tokenHoldingsByOwner: ERC1155FungibleHoldingsByOwner = {}; | ||||
|         const nonFungibleHoldingsByOwner: ERC1155NonFungibleHoldingsByOwner = {}; | ||||
|         for (const dummyTokenWrapper of this._dummyTokenWrappers) { | ||||
|             const tokenContract = dummyTokenWrapper.getContract(); | ||||
|             const tokenAddress = tokenContract.address; | ||||
|             // Construct batch balance call | ||||
|             const tokenOwners: string[] = []; | ||||
|             const tokenIds: BigNumber[] = []; | ||||
|             for (const tokenOwnerAddress of this._tokenOwnerAddresses) { | ||||
|                 for (const tokenId of this._fungibleTokenIds) { | ||||
|                     tokenOwners.push(tokenOwnerAddress); | ||||
|                     tokenIds.push(new BigNumber(tokenId)); | ||||
|                 } | ||||
|                 for (const nft of this._nfts) { | ||||
|                     tokenOwners.push(tokenOwnerAddress); | ||||
|                     tokenIds.push(nft.id); | ||||
|                 } | ||||
|             } | ||||
|             const balances = await dummyTokenWrapper.getBalancesAsync(tokenOwners, tokenIds); | ||||
|             // Parse out balances into fungible / non-fungible token holdings | ||||
|             let i = 0; | ||||
|             for (const tokenOwnerAddress of this._tokenOwnerAddresses) { | ||||
|                 // Fungible tokens | ||||
|                 for (const tokenId of this._fungibleTokenIds) { | ||||
|                     if (tokenHoldingsByOwner[tokenOwnerAddress] === undefined) { | ||||
|                         tokenHoldingsByOwner[tokenOwnerAddress] = {}; | ||||
|                     } | ||||
|                     if (tokenHoldingsByOwner[tokenOwnerAddress][tokenAddress] === undefined) { | ||||
|                         tokenHoldingsByOwner[tokenOwnerAddress][tokenAddress] = {}; | ||||
|                     } | ||||
|                     tokenHoldingsByOwner[tokenOwnerAddress][tokenAddress][tokenId] = balances[i++]; | ||||
|                 } | ||||
|                 // Non-fungible tokens | ||||
|                 for (const nft of this._nfts) { | ||||
|                     if (nonFungibleHoldingsByOwner[tokenOwnerAddress] === undefined) { | ||||
|                         nonFungibleHoldingsByOwner[tokenOwnerAddress] = {}; | ||||
|                     } | ||||
|                     if (nonFungibleHoldingsByOwner[tokenOwnerAddress][tokenAddress] === undefined) { | ||||
|                         nonFungibleHoldingsByOwner[tokenOwnerAddress][tokenAddress] = {}; | ||||
|                     } | ||||
|                     if ( | ||||
|                         nonFungibleHoldingsByOwner[tokenOwnerAddress][tokenAddress][nft.tokenId.toString()] === | ||||
|                         undefined | ||||
|                     ) { | ||||
|                         nonFungibleHoldingsByOwner[tokenOwnerAddress][tokenAddress][nft.tokenId.toString()] = []; | ||||
|                     } | ||||
|                     const isOwner = balances[i++]; | ||||
|                     if (isOwner.isEqualTo(1)) { | ||||
|                         nonFungibleHoldingsByOwner[tokenOwnerAddress][tokenAddress][nft.tokenId.toString()].push( | ||||
|                             nft.id, | ||||
|                         ); | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|         const holdingsByOwner = { | ||||
|             fungible: tokenHoldingsByOwner, | ||||
|             nonFungible: nonFungibleHoldingsByOwner, | ||||
|         }; | ||||
|         return holdingsByOwner; | ||||
|     } | ||||
|     /** | ||||
|      * @dev Set the approval for the proxy on behalf of `userAddress` . | ||||
|      * @param userAddress owner of ERC1155 tokens. | ||||
|      * @param contractAddress address of ERC1155 contract. | ||||
|      * @param isApproved Whether to approve the proxy for all or not. | ||||
|      */ | ||||
|     public async setProxyAllowanceForAllAsync( | ||||
|         userAddress: string, | ||||
|         contractAddress: string, | ||||
|         isApproved: boolean, | ||||
|     ): Promise<void> { | ||||
|         this._validateProxyContractExistsOrThrow(); | ||||
|         const tokenWrapper = this.getContractWrapper(contractAddress); | ||||
|         const operator = (this._proxyContract as ERC1155ProxyContract).address; | ||||
|         await tokenWrapper.setApprovalForAllAsync(userAddress, operator, isApproved); | ||||
|     } | ||||
|     /** | ||||
|      * @dev Checks if proxy is approved to transfer tokens on behalf of `userAddress`. | ||||
|      * @param userAddress owner of ERC1155 tokens. | ||||
|      * @param contractAddress address of ERC1155 contract. | ||||
|      * @return True iff the proxy is approved for all. False otherwise. | ||||
|      */ | ||||
|     public async isProxyApprovedForAllAsync(userAddress: string, contractAddress: string): Promise<boolean> { | ||||
|         this._validateProxyContractExistsOrThrow(); | ||||
|         const tokenContract = this._getContractFromAddress(contractAddress); | ||||
|         const operator = (this._proxyContract as ERC1155ProxyContract).address; | ||||
|         const didApproveAll = await tokenContract.isApprovedForAll.callAsync(userAddress, operator); | ||||
|         return didApproveAll; | ||||
|     } | ||||
|     public getFungibleTokenIds(): BigNumber[] { | ||||
|         const fungibleTokenIds = _.map(this._fungibleTokenIds, (tokenIdAsString: string) => { | ||||
|             return new BigNumber(tokenIdAsString); | ||||
|         }); | ||||
|         return fungibleTokenIds; | ||||
|     } | ||||
|     public getNonFungibleTokenIds(): BigNumber[] { | ||||
|         const nonFungibleTokenIds = _.map(this._nonFungibleTokenIds, (tokenIdAsString: string) => { | ||||
|             return new BigNumber(tokenIdAsString); | ||||
|         }); | ||||
|         return nonFungibleTokenIds; | ||||
|     } | ||||
|     public getTokenOwnerAddresses(): string[] { | ||||
|         return this._tokenOwnerAddresses; | ||||
|     } | ||||
|     public getContractWrapper(contractAddress: string): Erc1155Wrapper { | ||||
|         const tokenWrapper = _.find(this._dummyTokenWrappers, (wrapper: Erc1155Wrapper) => { | ||||
|             return wrapper.getContract().address === contractAddress; | ||||
|         }); | ||||
|         if (tokenWrapper === undefined) { | ||||
|             throw new Error(`Contract: ${contractAddress} was not deployed through ERC1155ProxyWrapper`); | ||||
|         } | ||||
|         return tokenWrapper; | ||||
|     } | ||||
|     private _getContractFromAddress(tokenAddress: string): ERC1155MintableContract { | ||||
|         const tokenContractIfExists = _.find(this._dummyTokenWrappers, c => c.getContract().address === tokenAddress); | ||||
|         if (tokenContractIfExists === undefined) { | ||||
|             throw new Error(`Token: ${tokenAddress} was not deployed through ERC1155ProxyWrapper`); | ||||
|         } | ||||
|         return tokenContractIfExists.getContract(); | ||||
|     } | ||||
|     private _validateDummyTokenContractsExistOrThrow(): void { | ||||
|         if (this._dummyTokenWrappers === undefined) { | ||||
|             throw new Error('Dummy ERC1155 tokens not yet deployed, please call "deployDummyTokensAsync"'); | ||||
|         } | ||||
|     } | ||||
|     private _validateProxyContractExistsOrThrow(): void { | ||||
|         if (this._proxyContract === undefined) { | ||||
|             throw new Error('ERC1155 proxy contract not yet deployed, please call "deployProxyAsync"'); | ||||
|         } | ||||
|     } | ||||
|     private _validateBalancesAndAllowancesSetOrThrow(): void { | ||||
|         if ( | ||||
|             _.keys(this._initialTokenIdsByOwner.fungible).length === 0 || | ||||
|             _.keys(this._initialTokenIdsByOwner.nonFungible).length === 0 | ||||
|         ) { | ||||
|             throw new Error( | ||||
|                 'Dummy ERC1155 balances and allowances not yet set, please call "setBalancesAndAllowancesAsync"', | ||||
|             ); | ||||
|         } | ||||
|     } | ||||
| } | ||||
		Reference in New Issue
	
	Block a user