mirror of
https://github.com/oceanprotocol/ocean.js.git
synced 2024-11-26 20:39:05 +01:00
wip v4 contract integration NFT creation flow
This commit is contained in:
parent
a6ce03439f
commit
252eca12e4
204
src/data/words.json
Normal file
204
src/data/words.json
Normal file
@ -0,0 +1,204 @@
|
|||||||
|
{
|
||||||
|
"nouns": [
|
||||||
|
"Crab",
|
||||||
|
"Fish",
|
||||||
|
"Seal",
|
||||||
|
"Octopus",
|
||||||
|
"Shark",
|
||||||
|
"Seahorse",
|
||||||
|
"Walrus",
|
||||||
|
"Starfish",
|
||||||
|
"Whale",
|
||||||
|
"Orca",
|
||||||
|
"Penguin",
|
||||||
|
"Jellyfish",
|
||||||
|
"Squid",
|
||||||
|
"Lobster",
|
||||||
|
"Pelican",
|
||||||
|
"Shrimp",
|
||||||
|
"Oyster",
|
||||||
|
"Clam",
|
||||||
|
"Seagull",
|
||||||
|
"Dolphin",
|
||||||
|
"Shell",
|
||||||
|
"Cormorant",
|
||||||
|
"Otter",
|
||||||
|
"Anemone",
|
||||||
|
"Turtle",
|
||||||
|
"Coral",
|
||||||
|
"Ray",
|
||||||
|
"Barracuda",
|
||||||
|
"Krill",
|
||||||
|
"Anchovy",
|
||||||
|
"Angelfish",
|
||||||
|
"Barnacle",
|
||||||
|
"Clownfish",
|
||||||
|
"Cod",
|
||||||
|
"Cuttlefish",
|
||||||
|
"Eel",
|
||||||
|
"Fugu",
|
||||||
|
"Herring",
|
||||||
|
"Haddock",
|
||||||
|
"Ling",
|
||||||
|
"Mackerel",
|
||||||
|
"Manatee",
|
||||||
|
"Narwhal",
|
||||||
|
"Nautilus",
|
||||||
|
"Plankton",
|
||||||
|
"Porpoise",
|
||||||
|
"Prawn",
|
||||||
|
"Pufferfish",
|
||||||
|
"Swordfish",
|
||||||
|
"Tuna"
|
||||||
|
],
|
||||||
|
"adjectives": [
|
||||||
|
"adamant",
|
||||||
|
"adroit",
|
||||||
|
"amatory",
|
||||||
|
"ambitious",
|
||||||
|
"amused",
|
||||||
|
"animistic",
|
||||||
|
"antic",
|
||||||
|
"arcadian",
|
||||||
|
"artistic",
|
||||||
|
"astonishing",
|
||||||
|
"astounding",
|
||||||
|
"baleful",
|
||||||
|
"bellicose",
|
||||||
|
"bilious",
|
||||||
|
"blissful",
|
||||||
|
"boorish",
|
||||||
|
"brave",
|
||||||
|
"breathtaking",
|
||||||
|
"brilliant",
|
||||||
|
"calamitous",
|
||||||
|
"caustic",
|
||||||
|
"cerulean",
|
||||||
|
"clever",
|
||||||
|
"charming",
|
||||||
|
"comely",
|
||||||
|
"competent",
|
||||||
|
"concomitant",
|
||||||
|
"confident",
|
||||||
|
"contumacious",
|
||||||
|
"corpulent",
|
||||||
|
"crapulous",
|
||||||
|
"creative",
|
||||||
|
"dazzling",
|
||||||
|
"dedicated",
|
||||||
|
"defamatory",
|
||||||
|
"delighted",
|
||||||
|
"delightful",
|
||||||
|
"determined",
|
||||||
|
"didactic",
|
||||||
|
"dilatory",
|
||||||
|
"dowdy",
|
||||||
|
"efficacious",
|
||||||
|
"effulgent",
|
||||||
|
"egregious",
|
||||||
|
"empowered",
|
||||||
|
"endemic",
|
||||||
|
"enthusiastic",
|
||||||
|
"equanimous",
|
||||||
|
"exceptional",
|
||||||
|
"execrable",
|
||||||
|
"fabulous",
|
||||||
|
"fantastic",
|
||||||
|
"fastidious",
|
||||||
|
"feckless",
|
||||||
|
"fecund",
|
||||||
|
"friable",
|
||||||
|
"fulsome",
|
||||||
|
"garrulous",
|
||||||
|
"generous",
|
||||||
|
"gentle",
|
||||||
|
"guileless",
|
||||||
|
"gustatory",
|
||||||
|
"heuristic",
|
||||||
|
"histrionic",
|
||||||
|
"hubristic",
|
||||||
|
"incendiary",
|
||||||
|
"incredible",
|
||||||
|
"insidious",
|
||||||
|
"insolent",
|
||||||
|
"inspired",
|
||||||
|
"intransigent",
|
||||||
|
"inveterate",
|
||||||
|
"invidious",
|
||||||
|
"invigorated",
|
||||||
|
"irksome",
|
||||||
|
"jejune",
|
||||||
|
"juicy",
|
||||||
|
"jocular",
|
||||||
|
"joyful",
|
||||||
|
"judicious",
|
||||||
|
"kind",
|
||||||
|
"lachrymose",
|
||||||
|
"limpid",
|
||||||
|
"loquacious",
|
||||||
|
"lovely",
|
||||||
|
"luminous",
|
||||||
|
"mannered",
|
||||||
|
"marvelous",
|
||||||
|
"mendacious",
|
||||||
|
"meretricious",
|
||||||
|
"minatory",
|
||||||
|
"mordant",
|
||||||
|
"motivated",
|
||||||
|
"munificent",
|
||||||
|
"nefarious",
|
||||||
|
"noxious",
|
||||||
|
"obtuse",
|
||||||
|
"optimistic",
|
||||||
|
"parsimonious",
|
||||||
|
"pendulous",
|
||||||
|
"pernicious",
|
||||||
|
"pervasive",
|
||||||
|
"petulant",
|
||||||
|
"passionate",
|
||||||
|
"phenomenal",
|
||||||
|
"platitudinous",
|
||||||
|
"pleasant",
|
||||||
|
"powerful",
|
||||||
|
"precipitate",
|
||||||
|
"propitious",
|
||||||
|
"puckish",
|
||||||
|
"querulous",
|
||||||
|
"quiescent",
|
||||||
|
"rebarbative",
|
||||||
|
"recalcitant",
|
||||||
|
"redolent",
|
||||||
|
"rhadamanthine",
|
||||||
|
"risible",
|
||||||
|
"ruminative",
|
||||||
|
"sagacious",
|
||||||
|
"salubrious",
|
||||||
|
"sartorial",
|
||||||
|
"sclerotic",
|
||||||
|
"serpentine",
|
||||||
|
"smart",
|
||||||
|
"spasmodic",
|
||||||
|
"strident",
|
||||||
|
"stunning",
|
||||||
|
"stupendous",
|
||||||
|
"taciturn",
|
||||||
|
"tactful",
|
||||||
|
"tasty",
|
||||||
|
"tenacious",
|
||||||
|
"tremendous",
|
||||||
|
"tremulous",
|
||||||
|
"trenchant",
|
||||||
|
"turbulent",
|
||||||
|
"turgid",
|
||||||
|
"ubiquitous",
|
||||||
|
"uxorious",
|
||||||
|
"verdant",
|
||||||
|
"vibrant",
|
||||||
|
"voluble",
|
||||||
|
"voracious",
|
||||||
|
"wheedling",
|
||||||
|
"withering",
|
||||||
|
"wonderful",
|
||||||
|
"zealous"
|
||||||
|
]
|
||||||
|
}
|
107
src/datatokens/NFTDatatoken.ts
Normal file
107
src/datatokens/NFTDatatoken.ts
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
import Web3 from 'web3'
|
||||||
|
import { AbiItem } from 'web3-utils'
|
||||||
|
import defaultNFTDatatokenABI from '@oceanprotocol/contracts/artifacts/contracts/templates/ERC721Template.sol/ERC721Template.json'
|
||||||
|
import { Logger, getFairGasPrice, generateDtName } from '../utils'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ERC721 ROLES
|
||||||
|
*/
|
||||||
|
interface Roles {
|
||||||
|
manager: boolean
|
||||||
|
deployERC20: boolean
|
||||||
|
updateMetadata: boolean
|
||||||
|
store: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export class NFTDataToken {
|
||||||
|
public GASLIMIT_DEFAULT = 1000000
|
||||||
|
public factory721Address: string
|
||||||
|
public factory721ABI: AbiItem | AbiItem[]
|
||||||
|
public nftDatatokenABI: AbiItem | AbiItem[]
|
||||||
|
public web3: Web3
|
||||||
|
private logger: Logger
|
||||||
|
public startBlock: number
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
web3: Web3,
|
||||||
|
logger: Logger,
|
||||||
|
nftDatatokenABI?: AbiItem | AbiItem[],
|
||||||
|
startBlock?: number
|
||||||
|
) {
|
||||||
|
this.nftDatatokenABI = nftDatatokenABI || (defaultNFTDatatokenABI.abi as AbiItem[])
|
||||||
|
this.web3 = web3
|
||||||
|
this.logger = logger
|
||||||
|
this.startBlock = startBlock || 0
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create new ERC20 datatoken - only user with ERC20Deployer permission can succeed
|
||||||
|
* @param {String} address
|
||||||
|
* @param {String} nftAddress
|
||||||
|
* @param {String} minter User set as initial minter for the ERC20
|
||||||
|
* @param {String} name Token name
|
||||||
|
* @param {String} symbol Token symbol
|
||||||
|
* @param {String} cap Maximum cap (Number) - will be converted to wei
|
||||||
|
* @param {Number} templateIndex NFT template index
|
||||||
|
* @return {Promise<string>} ERC20 datatoken address
|
||||||
|
*/
|
||||||
|
public async createERC20(
|
||||||
|
nftAddress: string,
|
||||||
|
address: string,
|
||||||
|
minter: string,
|
||||||
|
cap: string,
|
||||||
|
name?: string,
|
||||||
|
symbol?: string,
|
||||||
|
templateIndex?: number
|
||||||
|
): Promise<string> {
|
||||||
|
if (!templateIndex) templateIndex = 1
|
||||||
|
|
||||||
|
// Generate name & symbol if not present
|
||||||
|
if (!name || !symbol) {
|
||||||
|
;({ name, symbol } = generateDtName())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create 721contract object
|
||||||
|
const contract721 = new this.web3.eth.Contract(this.nftDatatokenABI, nftAddress)
|
||||||
|
|
||||||
|
// Estimate gas for ERC20 token creation
|
||||||
|
const gasLimitDefault = this.GASLIMIT_DEFAULT
|
||||||
|
let estGas
|
||||||
|
try {
|
||||||
|
estGas = await contract721.methods
|
||||||
|
.createERC20(
|
||||||
|
templateIndex,
|
||||||
|
[name, symbol],
|
||||||
|
[minter],
|
||||||
|
[this.web3.utils.toWei(cap)],
|
||||||
|
null
|
||||||
|
)
|
||||||
|
.estimateGas({ from: address }, (err, estGas) => (err ? gasLimitDefault : estGas))
|
||||||
|
} catch (e) {
|
||||||
|
estGas = gasLimitDefault
|
||||||
|
}
|
||||||
|
|
||||||
|
// Invoke createERC20 token function of the contract
|
||||||
|
const trxReceipt = await contract721.methods
|
||||||
|
.createERC20(
|
||||||
|
templateIndex,
|
||||||
|
[name, symbol],
|
||||||
|
[minter],
|
||||||
|
[this.web3.utils.toWei(cap)],
|
||||||
|
null
|
||||||
|
)
|
||||||
|
.send({
|
||||||
|
from: address,
|
||||||
|
gas: estGas + 1,
|
||||||
|
gasPrice: await getFairGasPrice(this.web3)
|
||||||
|
})
|
||||||
|
|
||||||
|
let tokenAddress = null
|
||||||
|
try {
|
||||||
|
tokenAddress = trxReceipt.events.ERC20Created.returnValues[0]
|
||||||
|
} catch (e) {
|
||||||
|
this.logger.error(`ERROR: Failed to create datatoken : ${e.message}`)
|
||||||
|
}
|
||||||
|
return tokenAddress
|
||||||
|
}
|
||||||
|
}
|
1
src/datatokens/index.ts
Normal file
1
src/datatokens/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from './NFTDatatoken'
|
91
src/factories/NFTFactory.ts
Normal file
91
src/factories/NFTFactory.ts
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
import { Contract } from 'web3-eth-contract'
|
||||||
|
import Web3 from 'web3'
|
||||||
|
import { AbiItem } from 'web3-utils'
|
||||||
|
import defaultFactory721ABI from '@oceanprotocol/contracts/artifacts/contracts/ERC721Factory.sol/ERC721Factory.json'
|
||||||
|
import { Logger, getFairGasPrice, generateDtName } from '../utils'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides an interface for NFT DataTokens
|
||||||
|
*/
|
||||||
|
export class NFTFactory {
|
||||||
|
public GASLIMIT_DEFAULT = 1000000
|
||||||
|
public factory721Address: string
|
||||||
|
public factory721ABI: AbiItem | AbiItem[]
|
||||||
|
public web3: Web3
|
||||||
|
private logger: Logger
|
||||||
|
public startBlock: number
|
||||||
|
public factory721: Contract
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Instantiate DataTokens.
|
||||||
|
* @param {String} factory721Address
|
||||||
|
* @param {AbiItem | AbiItem[]} factory721ABI
|
||||||
|
* @param {Web3} web3
|
||||||
|
*/
|
||||||
|
constructor(
|
||||||
|
factory721Address: string,
|
||||||
|
web3: Web3,
|
||||||
|
logger: Logger,
|
||||||
|
factory721ABI?: AbiItem | AbiItem[],
|
||||||
|
startBlock?: number
|
||||||
|
) {
|
||||||
|
this.factory721Address = factory721Address
|
||||||
|
this.factory721ABI = factory721ABI || (defaultFactory721ABI.abi as AbiItem[])
|
||||||
|
this.web3 = web3
|
||||||
|
this.logger = logger
|
||||||
|
this.startBlock = startBlock || 0
|
||||||
|
this.factory721 = new this.web3.eth.Contract(
|
||||||
|
this.factory721ABI,
|
||||||
|
this.factory721Address
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create new NFT
|
||||||
|
* @param {String} address
|
||||||
|
* @param {String} name Token name
|
||||||
|
* @param {String} symbol Token symbol
|
||||||
|
* @param {Number} templateIndex NFT template index
|
||||||
|
* @return {Promise<string>} NFT datatoken address
|
||||||
|
*/
|
||||||
|
public async createNFT(
|
||||||
|
address: string,
|
||||||
|
name?: string,
|
||||||
|
symbol?: string,
|
||||||
|
templateIndex?: number
|
||||||
|
): Promise<string> {
|
||||||
|
if (!templateIndex) templateIndex = 1
|
||||||
|
// Generate name & symbol if not present
|
||||||
|
if (!name || !symbol) {
|
||||||
|
;({ name, symbol } = generateDtName())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get estimated gas value
|
||||||
|
const gasLimitDefault = this.GASLIMIT_DEFAULT
|
||||||
|
let estGas
|
||||||
|
try {
|
||||||
|
estGas = await this.factory721.methods
|
||||||
|
.deployERC721Contract(name, symbol, templateIndex, null)
|
||||||
|
.estimateGas({ from: address }, (err, estGas) => (err ? gasLimitDefault : estGas))
|
||||||
|
} catch (e) {
|
||||||
|
estGas = gasLimitDefault
|
||||||
|
}
|
||||||
|
|
||||||
|
// Invoke createToken function of the contract
|
||||||
|
const trxReceipt = await this.factory721.methods
|
||||||
|
.deployERC721Contract(name, symbol, templateIndex, null)
|
||||||
|
.send({
|
||||||
|
from: address,
|
||||||
|
gas: estGas + 1,
|
||||||
|
gasPrice: await getFairGasPrice(this.web3)
|
||||||
|
})
|
||||||
|
|
||||||
|
let tokenAddress = null
|
||||||
|
try {
|
||||||
|
tokenAddress = trxReceipt.events.TokenCreated.returnValues[0]
|
||||||
|
} catch (e) {
|
||||||
|
this.logger.error(`ERROR: Failed to create datatoken : ${e.message}`)
|
||||||
|
}
|
||||||
|
return tokenAddress
|
||||||
|
}
|
||||||
|
}
|
1
src/factories/index.ts
Normal file
1
src/factories/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from './NFTFactory'
|
38
src/pools/balancer/OceanPool.ts
Normal file
38
src/pools/balancer/OceanPool.ts
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
import Web3 from 'web3'
|
||||||
|
import { AbiItem } from 'web3-utils'
|
||||||
|
import { Contract } from 'web3-eth-contract'
|
||||||
|
import defaultPoolABI from '@oceanprotocol/contracts/artifacts/contracts/interfaces/IPool.sol/IPool.json'
|
||||||
|
import defaultERC20ABI from '@oceanprotocol/contracts/artifacts/contracts/interfaces/IERC20.sol/IERC20.json'
|
||||||
|
import { PoolFactory } from './PoolFactory'
|
||||||
|
import { Logger } from '../../utils'
|
||||||
|
|
||||||
|
export class OceanPoolV4 extends PoolFactory {
|
||||||
|
public oceanAddress: string = null
|
||||||
|
public dtAddress: string = null
|
||||||
|
public startBlock: number
|
||||||
|
public vaultABI: AbiItem | AbiItem[]
|
||||||
|
public vaultAddress: string
|
||||||
|
public vault: Contract
|
||||||
|
public poolABI: AbiItem | AbiItem[]
|
||||||
|
public erc20ABI: AbiItem | AbiItem[]
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
web3: Web3,
|
||||||
|
logger: Logger,
|
||||||
|
routerAddress: string = null,
|
||||||
|
oceanAddress: string = null,
|
||||||
|
startBlock?: number
|
||||||
|
) {
|
||||||
|
super(web3, logger, routerAddress)
|
||||||
|
|
||||||
|
this.poolABI = defaultPoolABI.abi as AbiItem[]
|
||||||
|
this.erc20ABI = defaultERC20ABI.abi as AbiItem[]
|
||||||
|
this.vault = new this.web3.eth.Contract(this.vaultABI, this.vaultAddress)
|
||||||
|
|
||||||
|
// if (oceanAddress) {
|
||||||
|
// this.oceanAddress = oceanAddress
|
||||||
|
// }
|
||||||
|
if (startBlock) this.startBlock = startBlock
|
||||||
|
else this.startBlock = 0
|
||||||
|
}
|
||||||
|
}
|
57
src/pools/balancer/PoolFactory.ts
Normal file
57
src/pools/balancer/PoolFactory.ts
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
import Web3 from 'web3'
|
||||||
|
import { AbiItem } from 'web3-utils'
|
||||||
|
import { Contract } from 'web3-eth-contract'
|
||||||
|
import defaultRouterABI from '@oceanprotocol/contracts/artifacts/contracts/interfaces/IFactoryRouter.sol/IFactoryRouter.json'
|
||||||
|
import { Logger } from '../../utils'
|
||||||
|
import { TransactionReceipt } from 'web3-eth'
|
||||||
|
|
||||||
|
export class PoolFactory {
|
||||||
|
public GASLIMIT_DEFAULT = 1000000
|
||||||
|
public web3: Web3 = null
|
||||||
|
public routerABI: AbiItem | AbiItem[]
|
||||||
|
|
||||||
|
public routerAddress: string
|
||||||
|
|
||||||
|
public logger: Logger
|
||||||
|
public router: Contract
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Instantiate PoolFactory.
|
||||||
|
* @param {String} routerAddress
|
||||||
|
* @param {AbiItem | AbiItem[]} routerABI
|
||||||
|
* @param {Web3} web3
|
||||||
|
*/
|
||||||
|
constructor(
|
||||||
|
web3: Web3,
|
||||||
|
logger: Logger,
|
||||||
|
routerAddress: string,
|
||||||
|
routerABI?: AbiItem | AbiItem[]
|
||||||
|
) {
|
||||||
|
this.web3 = web3
|
||||||
|
this.routerAddress = routerAddress
|
||||||
|
this.routerABI = routerABI || (defaultRouterABI.abi as AbiItem[])
|
||||||
|
this.logger = logger
|
||||||
|
this.router = new this.web3.eth.Contract(this.routerABI, this.routerAddress)
|
||||||
|
}
|
||||||
|
|
||||||
|
public async deployPool(
|
||||||
|
account: string,
|
||||||
|
tokens: string[],
|
||||||
|
weights: string[],
|
||||||
|
swapFeePercentage: number,
|
||||||
|
swapMarketFee: number,
|
||||||
|
owner: string
|
||||||
|
): Promise<TransactionReceipt> {
|
||||||
|
const gasLimitDefault = this.GASLIMIT_DEFAULT
|
||||||
|
let estGas
|
||||||
|
try {
|
||||||
|
estGas = await this.router.methods
|
||||||
|
.deployPool(tokens, weightsInWei, swapFeePercentage, swapMarketFee, owner)
|
||||||
|
.estimateGas({ from: account }, (err, estGas) => (err ? gasLimitDefault : estGas))
|
||||||
|
} catch (e) {
|
||||||
|
this.logger.log('Error estimate gas deployPool')
|
||||||
|
this.logger.log(e)
|
||||||
|
estGas = gasLimitDefault
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,2 @@
|
|||||||
|
export * from './PoolFactory'
|
||||||
|
export * from './OceanPool'
|
27
src/utils/DatatokenName.ts
Normal file
27
src/utils/DatatokenName.ts
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import wordListDefault from '../data/words.json'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate new datatoken name & symbol from a word list
|
||||||
|
* @return {<{ name: String; symbol: String }>} datatoken name & symbol. Produces e.g. "Endemic Jellyfish Token" & "ENDJEL-45"
|
||||||
|
*/
|
||||||
|
export function generateDtName(wordList?: { nouns: string[]; adjectives: string[] }): {
|
||||||
|
name: string
|
||||||
|
symbol: string
|
||||||
|
} {
|
||||||
|
const list = wordList || wordListDefault
|
||||||
|
const random1 = Math.floor(Math.random() * list.adjectives.length)
|
||||||
|
const random2 = Math.floor(Math.random() * list.nouns.length)
|
||||||
|
const indexNumber = Math.floor(Math.random() * 100)
|
||||||
|
|
||||||
|
// Capitalized adjective & noun
|
||||||
|
const adjective = list.adjectives[random1].replace(/^\w/, (c) => c.toUpperCase())
|
||||||
|
const noun = list.nouns[random2].replace(/^\w/, (c) => c.toUpperCase())
|
||||||
|
|
||||||
|
const name = `${adjective} ${noun} Token`
|
||||||
|
// use first 3 letters of name, uppercase it, and add random number
|
||||||
|
const symbol = `${(
|
||||||
|
adjective.substring(0, 3) + noun.substring(0, 3)
|
||||||
|
).toUpperCase()}-${indexNumber}`
|
||||||
|
|
||||||
|
return { name, symbol }
|
||||||
|
}
|
@ -1,3 +1,4 @@
|
|||||||
export * from './Logger'
|
export * from './Logger'
|
||||||
export * from './GasUtils'
|
export * from './GasUtils'
|
||||||
export * from './Logger'
|
export * from './Logger'
|
||||||
|
export * from './DatatokenName'
|
||||||
|
Loading…
x
Reference in New Issue
Block a user