1import {EthUniqueHelper, usingEthPlaygrounds} from '../../eth/util';2import {readFile} from 'fs/promises';3import {ContractImports, TokenPermissionField} from '../../eth/util/playgrounds/types';4import {5 ICrossAccountId,6 ITokenPropertyPermission,7} from '../../util/playgrounds/types';8import {IKeyringPair} from '@polkadot/types/types';9import {UniqueNFTCollection} from '../../util/playgrounds/unique';10import {Contract} from 'web3-eth-contract';11import {createObjectCsvWriter} from 'csv-writer';1213const CONTRACT_IMPORT: ContractImports[] = [14 {15 fsPath: `${__dirname}/../../eth/api/CollectionHelpers.sol`,16 solPath: 'eth/api/CollectionHelpers.sol',17 },18 {19 fsPath: `${__dirname}/../../eth/api/ContractHelpers.sol`,20 solPath: 'eth/api/ContractHelpers.sol',21 },22 {23 fsPath: `${__dirname}/../../eth/api/UniqueRefungibleToken.sol`,24 solPath: 'eth/api/UniqueRefungibleToken.sol',25 },26 {27 fsPath: `${__dirname}/../../eth/api/UniqueRefungible.sol`,28 solPath: 'eth/api/UniqueRefungible.sol',29 },30 {31 fsPath: `${__dirname}/../../eth/api/UniqueNFT.sol`,32 solPath: 'eth/api/UniqueNFT.sol',33 },34];3536const PROPERTIES = Array(40)37 .fill(0)38 .map((_, i) => {39 return {40 key: `key_${i}`,41 value: Uint8Array.from(Buffer.from(`value_${i}`)),42 };43 });4445const PERMISSIONS: ITokenPropertyPermission[] = PROPERTIES.map((p) => {46 return {47 key: p.key,48 permission: {49 tokenOwner: true,50 collectionAdmin: true,51 mutable: true,52 },53 };54});5556interface IBenchmarkResultForProp {57 propertiesNumber: number;58 substrateFee: number;59 ethFee: number;60 ethBulkFee: number;61 ethMintCrossFee: number;62 evmProxyContractFee: number;63 evmProxyContractBulkFee: number;64}65interface IBenchmarkCollection {66 createFee: number,67 mintFee: number,68 transferFee: number,69}70const main = async () => {71 const benchmarks = [72 'substrateFee',73 'ethFee',74 'ethBulkFee',75 'ethMintCrossFee',76 'evmProxyContractFee',77 'evmProxyContractBulkFee',78 ];79 const headers = [80 'propertiesNumber',81 ...benchmarks,82 ];838485 const csvWriter = createObjectCsvWriter({86 path: 'properties.csv',87 header: headers,88 });8990 await usingEthPlaygrounds(async (helper, privateKey) => {91 const NOMINAL = helper.balance.getOneTokenNominal();92 const CONTRACT_SOURCE = (93 await readFile(`${__dirname}/proxyContract.sol`)94 ).toString();95 const ZEPPELIN_OBJECT = '0x' + (await readFile(`${__dirname}/openZeppelin/bin/ZeppelinContract.bin`)).toString();96 const ZEPPELIN_ABI = JSON.parse((await readFile(`${__dirname}/openZeppelin/bin/ZeppelinContract.abi`)).toString());9798 const donor = await privateKey('//Alice'); 99 const [substrateReceiver] = await helper.arrange.createAccounts([10n], donor);100101 const ethSigner = await helper.eth.createAccountWithBalance(donor, 100n);102 const ethReceiver = await helper.eth.createAccountWithBalance(donor, 5n);103 104 let susbtrateCollection: UniqueNFTCollection | null;105 const createCollectionSubstrateFee = convertToTokens(106 await helper.arrange.calculcateFee({Substrate: donor.address}, async () => {107 susbtrateCollection = await helper.nft.mintCollection(donor, {tokenPropertyPermissions: [108 {109 key: 'url',110 permission: {111 tokenOwner: true,112 collectionAdmin: true,113 mutable: true,114 },115 },116 ]});117 }),118 NOMINAL,119 );120121 const susbstrateMintFee = convertToTokens(122 await helper.arrange.calculcateFee(123 {Substrate: donor.address},124 () => susbtrateCollection!.mintToken(donor, {Substrate: substrateReceiver.address}, [{key: 'url', value: 'test'}]),125 ),126 NOMINAL,127 );128 const susbstrateTransferFee = convertToTokens(129 await helper.arrange.calculcateFee(130 {Substrate: substrateReceiver.address},131 () => susbtrateCollection!.transferToken(substrateReceiver, 1, {Substrate: donor.address}),132 ),133 NOMINAL,134 );135 const substrateFee: IBenchmarkCollection = {136 createFee: createCollectionSubstrateFee,137 mintFee: susbstrateMintFee,138 transferFee: susbstrateTransferFee,139 };140141 const helperContract = await helper.ethNativeContract.collectionHelpers(ethSigner);142143 let ethContract: Contract | null = null;144145 const createCollectionEthFee = convertToTokens(146 await helper.arrange.calculcateFee({Ethereum: ethSigner}, async () => {147 const result = await helperContract.methods.createNFTCollection('a', 'b', 'c').send({from: ethSigner, value: Number(2n * helper.balance.getOneTokenNominal())});148 ethContract = await helper.ethNativeContract.collection(helper.ethAddress.normalizeAddress(result.events.CollectionCreated.returnValues.collectionId), 'nft', ethSigner);149 }),150 NOMINAL,151 );152153 await ethContract!.methods.setTokenPropertyPermissions([154 ['url', [155 [TokenPermissionField.Mutable, true],156 [TokenPermissionField.TokenOwner, true],157 [TokenPermissionField.CollectionAdmin, true]],158 ],159 ]).send({from: ethSigner});160161 const ethMintFee = convertToTokens(await helper.arrange.calculcateFee(162 {Ethereum: ethSigner},163 () => ethContract!.methods.mintCross(164 helper.ethCrossAccount.fromAddress(ethReceiver),165 [{key: 'url', value: Buffer.from('test')}],166 )167 .send({from: ethSigner}),168 ), NOMINAL);169170 const ethTransferFee = convertToTokens(await helper.arrange.calculcateFee(171 {Ethereum: ethReceiver},172 () => ethContract!.methods.transferFrom(ethReceiver, ethSigner, 1)173 .send({from: ethReceiver}),174 ), NOMINAL);175176 const ethFee: IBenchmarkCollection = {177 createFee: createCollectionEthFee,178 mintFee: ethMintFee,179 transferFee: ethTransferFee,180 };181182 let zeppelelinContract: Contract | null = null;183184 const zeppelinDeployFee = convertToTokens(185 await helper.arrange.calculcateFee({Ethereum: ethSigner}, async () => {186 zeppelelinContract = await helper.ethContract.deployByAbi(187 ethSigner,188 ZEPPELIN_ABI,189 ZEPPELIN_OBJECT,190 );191 }),192 NOMINAL,193 );194195 const zeppelinMintFee = convertToTokens(await helper.arrange.calculcateFee(196 {Ethereum: ethSigner},197 () => zeppelelinContract!.methods.safeMint(ethReceiver, 'test').send({from: ethSigner}),198 ), NOMINAL);199200201 const zeppelinTransferFee = convertToTokens(await helper.arrange.calculcateFee(202 {Ethereum: ethReceiver},203 () => zeppelelinContract!.methods.safeTransferFrom(ethReceiver, ethSigner, 0).send({from: ethReceiver}),204 ), NOMINAL);205206 const zeppelinFee: IBenchmarkCollection = {207 createFee: zeppelinDeployFee,208 mintFee: zeppelinMintFee,209 transferFee: zeppelinTransferFee,210 };211212 console.log('collection ops fees');213 const results = {substrate: substrateFee, eth: ethFee, zeppelin: zeppelinFee};214 console.table(results);215216 const contract = await helper.ethContract.deployByCode(217 ethSigner,218 'ProxyMint',219 CONTRACT_SOURCE,220 CONTRACT_IMPORT,221 );222223 const fees = await benchMintFee(helper, privateKey, contract);224 console.log('Minting without properties');225 console.table(fees);226227 const result: IBenchmarkResultForProp[] = [];228 const csvResult: IBenchmarkResultForProp[] = [];229230 for (let i = 1; i <= 20; i++) {231 const benchResult = await benchMintWithProperties(helper, privateKey, contract, {232 propertiesNumber: i,233 }) as any;234235 csvResult.push(benchResult);236237 const minFee = Math.min(...(benchmarks.map(x => benchResult[x])));238 for(const key of benchmarks) {239 const keyPercent = Math.round((benchResult[key] / minFee) * 100);240 benchResult[key] = `${benchResult[key]} (${keyPercent}%)`;241 }242243 result.push(benchResult);244 }245246 await csvWriter.writeRecords(csvResult);247248 console.log('Minting with properties');249 console.table(result, headers);250 });251};252253main()254 .then(() => process.exit(0))255 .catch((e) => {256 console.log(e);257 process.exit(1);258 });259260async function createCollectionForBenchmarks(261 helper: EthUniqueHelper,262 privateKey: (seed: string) => Promise<IKeyringPair>,263 ethSigner: string,264 proxyContract: string,265 permissions: ITokenPropertyPermission[],266) {267 const donor = await privateKey('//Alice');268269 const collection = await helper.nft.mintCollection(donor, {270 name: 'test mintToSubstrate',271 description: 'EVMHelpers',272 tokenPrefix: 'ap',273 tokenPropertyPermissions: [274 {275 key: 'url',276 permission: {277 tokenOwner: true,278 collectionAdmin: true,279 mutable: true,280 },281 },282 ],283 limits: {sponsorTransferTimeout: 0, sponsorApproveTimeout: 0},284 permissions: {mintMode: true},285 });286287 await collection.addToAllowList(donor, {288 Ethereum: helper.address.substrateToEth(donor.address),289 });290 await collection.addToAllowList(donor, {Substrate: donor.address});291 await collection.addAdmin(donor, {Ethereum: ethSigner});292 await collection.addAdmin(donor, {293 Ethereum: helper.address.substrateToEth(donor.address),294 });295 await collection.addToAllowList(donor, {Ethereum: proxyContract});296 await collection.addAdmin(donor, {Ethereum: proxyContract});297 await collection.setTokenPropertyPermissions(donor, permissions);298299 return collection;300}301302async function benchMintFee(303 helper: EthUniqueHelper,304 privateKey: (seed: string) => Promise<IKeyringPair>,305 proxyContract: Contract,306): Promise<{307 substrateFee: number;308 ethFee: number;309 evmProxyContractFee: number;310}> {311 const donor = await privateKey('//Alice');312 const substrateReceiver = await privateKey('//Bob');313 const ethSigner = await helper.eth.createAccountWithBalance(donor, 100n);314315 const nominal = helper.balance.getOneTokenNominal();316317 await helper.eth.transferBalanceFromSubstrate(318 donor,319 proxyContract.options.address,320 100n,321 );322323 const collection = await createCollectionForBenchmarks(324 helper,325 privateKey,326 ethSigner,327 proxyContract.options.address,328 PERMISSIONS,329 );330331 const substrateFee = await helper.arrange.calculcateFee(332 {Substrate: donor.address},333 () => collection.mintToken(donor, {Substrate: substrateReceiver.address}),334 );335336 const collectionEthAddress = helper.ethAddress.fromCollectionId(collection.collectionId);337 const collectionContract = await helper.ethNativeContract.collection(338 collectionEthAddress,339 'nft',340 );341342 const receiverEthAddress = helper.address.substrateToEth(substrateReceiver.address);343344 const encodedCall = collectionContract.methods345 .mint(receiverEthAddress)346 .encodeABI();347348 const ethFee = await helper.arrange.calculcateFee(349 {Substrate: donor.address},350 async () => {351 await helper.eth.sendEVM(352 donor,353 collectionContract.options.address,354 encodedCall,355 '0',356 );357 },358 );359360 const evmProxyContractFee = await helper.arrange.calculcateFee(361 {Ethereum: ethSigner},362 async () => {363 await proxyContract.methods364 .mintToSubstrate(365 helper.ethAddress.fromCollectionId(collection.collectionId),366 substrateReceiver.addressRaw,367 )368 .send({from: ethSigner});369 },370 );371372 return {373 substrateFee: convertToTokens(substrateFee, nominal),374 ethFee: convertToTokens(ethFee, nominal),375 evmProxyContractFee: convertToTokens(evmProxyContractFee, nominal),376 };377}378379async function benchMintWithProperties(380 helper: EthUniqueHelper,381 privateKey: (seed: string) => Promise<IKeyringPair>,382 proxyContract: Contract,383 setup: { propertiesNumber: number },384): Promise<IBenchmarkResultForProp> {385 const donor = await privateKey('//Alice'); 386 const ethSigner = await helper.eth.createAccountWithBalance(donor, 100n);387388 const susbstrateReceiver = await privateKey('//Bob');389 const receiverEthAddress = helper.address.substrateToEth(susbstrateReceiver.address);390391 const nominal = helper.balance.getOneTokenNominal();392393 const substrateFee = await calculateFeeNftMintWithProperties(394 helper,395 privateKey,396 {Substrate: donor.address},397 ethSigner,398 proxyContract.options.address,399 async (collection) => {400 await collection.mintToken(401 donor,402 {Substrate: susbstrateReceiver.address},403 PROPERTIES.slice(0, setup.propertiesNumber).map((p) => {404 return {key: p.key, value: Buffer.from(p.value).toString()};405 }),406 );407 },408 );409410 const ethFee = await calculateFeeNftMintWithProperties(411 helper,412 privateKey,413 {Substrate: donor.address},414 ethSigner,415 proxyContract.options.address,416 async (collection) => {417 const evmContract = await helper.ethNativeContract.collection(418 helper.ethAddress.fromCollectionId(collection.collectionId),419 'nft',420 undefined,421 true,422 );423424 const subTokenId = await evmContract.methods.nextTokenId().call();425426 let encodedCall = evmContract.methods427 .mint(receiverEthAddress)428 .encodeABI();429430 await helper.eth.sendEVM(431 donor,432 evmContract.options.address,433 encodedCall,434 '0',435 );436437 for (const val of PROPERTIES.slice(0, setup.propertiesNumber)) {438 encodedCall = await evmContract.methods439 .setProperty(subTokenId, val.key, Buffer.from(val.value))440 .encodeABI();441442 await helper.eth.sendEVM(443 donor,444 evmContract.options.address,445 encodedCall,446 '0',447 );448 }449 },450 );451452 const ethBulkFee = await calculateFeeNftMintWithProperties(453 helper,454 privateKey,455 {Substrate: donor.address},456 ethSigner,457 proxyContract.options.address,458 async (collection) => {459 const evmContract = await helper.ethNativeContract.collection(460 helper.ethAddress.fromCollectionId(collection.collectionId),461 'nft',462 );463464 const subTokenId = await evmContract.methods.nextTokenId().call();465466 let encodedCall = evmContract.methods467 .mint(receiverEthAddress)468 .encodeABI();469470 await helper.eth.sendEVM(471 donor,472 evmContract.options.address,473 encodedCall,474 '0',475 );476477 encodedCall = await evmContract.methods478 .setProperties(479 subTokenId,480 PROPERTIES.slice(0, setup.propertiesNumber),481 )482 .encodeABI();483484 await helper.eth.sendEVM(485 donor,486 evmContract.options.address,487 encodedCall,488 '0',489 );490 },491 );492493 const ethMintCrossFee = await calculateFeeNftMintWithProperties(494 helper,495 privateKey,496 {Ethereum: ethSigner},497 ethSigner,498 proxyContract.options.address,499 async (collection) => {500 const evmContract = await helper.ethNativeContract.collection(501 helper.ethAddress.fromCollectionId(collection.collectionId),502 'nft',503 );504505 await evmContract.methods.mintCross(506 helper.ethCrossAccount.fromAddress(receiverEthAddress),507 PROPERTIES.slice(0, setup.propertiesNumber),508 )509 .send({from: ethSigner});510 },511 );512513 const proxyContractFee = await calculateFeeNftMintWithProperties(514 helper,515 privateKey,516 {Ethereum: ethSigner},517 ethSigner,518 proxyContract.options.address,519 async (collection) => {520 await proxyContract.methods521 .mintToSubstrateWithProperty(522 helper.ethAddress.fromCollectionId(collection.collectionId),523 susbstrateReceiver.addressRaw,524 PROPERTIES.slice(0, setup.propertiesNumber),525 )526 .send({from: ethSigner});527 },528 );529530 const proxyContractBulkFee = await calculateFeeNftMintWithProperties(531 helper,532 privateKey,533 {Ethereum: ethSigner},534 ethSigner,535 proxyContract.options.address,536 async (collection) => {537 await proxyContract.methods538 .mintToSubstrateBulkProperty(539 helper.ethAddress.fromCollectionId(collection.collectionId),540 susbstrateReceiver.addressRaw,541 PROPERTIES.slice(0, setup.propertiesNumber),542 )543 .send({from: ethSigner, gas: 25_000_000});544 },545 );546547 return {548 propertiesNumber: setup.propertiesNumber,549 substrateFee: convertToTokens(substrateFee, nominal),550 ethFee: convertToTokens(ethFee, nominal),551 ethBulkFee: convertToTokens(ethBulkFee, nominal),552 ethMintCrossFee: convertToTokens(ethMintCrossFee, nominal),553 evmProxyContractFee: convertToTokens(proxyContractFee, nominal),554 evmProxyContractBulkFee: convertToTokens(proxyContractBulkFee, nominal),555 };556}557558async function calculateFeeNftMintWithProperties(559 helper: EthUniqueHelper,560 privateKey: (seed: string) => Promise<IKeyringPair>,561 payer: ICrossAccountId,562 ethSigner: string,563 proxyContractAddress: string,564 calculatedCall: (collection: UniqueNFTCollection) => Promise<any>,565): Promise<bigint> {566 const collection = await createCollectionForBenchmarks(567 helper,568 privateKey,569 ethSigner,570 proxyContractAddress,571 PERMISSIONS,572 );573 return helper.arrange.calculcateFee(payer, async () => {574 await calculatedCall(collection);575 });576}577function convertToTokens(value: bigint, nominal: bigint): number {578 return Number((value * 1000n) / nominal) / 1000;579}