import { BigNumber, ethers, utils } from 'ethers'; import { Axelar } from './Axelar'; import { IOmniteAxelarBridge } from './config/abi/OmniteAxelarBridgeSender'; import { IOmniteLayerZeroBridge } from './config/abi/OmniteLayerZeroBridgeSender'; import config from './config/config'; import { ContractFactory } from './contracts/ContractFactory'; import { ERC721Native } from './contracts/ERC721Native'; import { LayerZeroUltraLightNode } from './contracts/LayerZeroUltraLightNode'; import { OmniteAxelarBridgeSender } from './contracts/OmniteAxelarBridgeSender'; import { OmniteLayerZeroBridgeSender } from './contracts/OmniteLayerZeroBridgeSender'; import { LayerZero } from './LayerZero'; import { BridgeBroker, DeployBlueprintName, DeployData, DeployType, NetworkDistribution, NetworkDistributionWithIds, } from './types'; import { getWalletConfigByChainId } from './utils/chainUtils'; export class CollectionCreator { public contractUri: string = ''; public baseTokenUri: string = ''; public originalTokenAddress: string = ethers.constants.AddressZero; private _collectionId: string | undefined; private readonly networks: Array; /** * Creates a new collection creator instance. * * @param {string} sourceChainId - source chain id * @param {ethers.providers.JsonRpcSigner} signer - ethers signer * @param {DeployType} deploymentType - deployment type * @param {DeployData} deployData - data for deployment * @param {Array} bridgeBrokers - supported bridge brokers * @param {BridgeBroker} deployBroker - broker should be used for deployment * @param {Array} networks - init networks for collection */ constructor( private readonly sourceChainId: string, private readonly signer: ethers.providers.JsonRpcSigner, private readonly deploymentType: DeployType, private readonly deployData: DeployData, private readonly bridgeBrokers: Array, private readonly deployBroker: BridgeBroker, networks: Array ) { this.networks = CollectionCreator.assignRangeToNetworks(networks); } get isNative() { return this.deploymentType === DeployType.native; } get blueprintName() { let blueprintName = DeployBlueprintName.ERC721Native; if ( this.deploymentType === DeployType.notNative || this.deploymentType === DeployType.notNativeNewChains ) { blueprintName = DeployBlueprintName.ERC721NonNative; } return blueprintName; } get callParams() { return { baseTokenUri: this.baseTokenUri, contractUri: this.contractUri, }; } estimatedFee() { return CollectionCreator.estimateDeploymentFees( this.sourceChainId, this.bridgeBrokers, this.deployBroker, this.networks, this.deployData, this.isNative ); } get collectionId(): string { if (this._collectionId) { throw new Error('Collection id is not defined'); } return this.collectionId; } set collectionId(value: string) { this._collectionId = value; } private prepareLayerZeroNetworkConstructorParams = async (): Promise< Array > => { return Promise.all( this.networks.map(async (network) => { const targetChainConfig = getWalletConfigByChainId( network.chainId ); const deploymentParams: IOmniteLayerZeroBridge.DeploymentParamsStruct = { chainId: targetChainConfig.layerZeroChainId, bridgeAddress: ethers.utils.solidityPack( ['address'], [ targetChainConfig?.contractAddress .omniteLayerZeroBridgeReceiver, ] ), ctorParams: await this.encodeCtorParams( network.chainId, { slotStart: network.startId, slotEnd: network.lastId, originalContractAddress: this.originalTokenAddress, baseTokenUri: !this.isNative ? undefined : this.baseTokenUri, contractUri: this.contractUri, } ), value: (await this.estimatedFee()).gasPerNetwork[ network.chainId ], originalContractAddress: this.originalTokenAddress, }; return deploymentParams; }) ); }; private prepareAxelarNetworkConstructorParams = async (): Promise< Array > => { return Promise.all( this.networks.map(async (network) => { const targetChainConfig = getWalletConfigByChainId( network.chainId ); const deploymentParams: IOmniteAxelarBridge.DeploymentParamsStruct = { chain: targetChainConfig.axelarChainId, // bridgeAddress: // targetChainConfig.contractAddress // .omniteAxelarBridgeReceiver, bridgeAddress: ethers.utils.solidityPack( ['address'], [ targetChainConfig.contractAddress .omniteAxelarBridgeReceiver, ] ), originalContractAddress: this.originalTokenAddress, value: (await this.estimatedFee()).gasPerNetwork[ network.chainId ], ctorParams: await this.encodeCtorParams( network.chainId, { slotStart: network.startId, slotEnd: network.lastId, originalContractAddress: this.originalTokenAddress, baseTokenUri: !this.isNative ? undefined : this.baseTokenUri, contractUri: this.contractUri, } ), }; return deploymentParams; }) ); }; /** * Creates a new collection on selected networks (in the constructor). */ deploy = async () => { const ownerAddress = await this.signer.getAddress(); const estimatedFee = await this.estimatedFee(); const chainIdGas = estimatedFee.gasLimitPerNetwork[this.sourceChainId] ? this.sourceChainId : Object.keys(estimatedFee.gasLimitPerNetwork).pop(); if (!chainIdGas) { throw new Error('No gas estimation'); } const gasAmount = estimatedFee.gasLimitPerNetwork[chainIdGas].toNumber(); let receipt: ethers.ContractTransaction | undefined; if (this.deployBroker === BridgeBroker.LayerZero) { const omniteLayerZeroBridgeSender = await OmniteLayerZeroBridgeSender.create(this.signer); switch (this.deploymentType) { case DeployType.native: receipt = await omniteLayerZeroBridgeSender.deployNativeCollection( await this.prepareLayerZeroNetworkConstructorParams(), { collectionName: this.deployData.collectionName, refundAddress: ownerAddress, gasAmount, owner: ownerAddress, }, estimatedFee.totalGas ); break; default: throw new Error(`Deployment type is not supported`); } } else { const omniteAxelarBridgeSender = await OmniteAxelarBridgeSender.create(this.signer); switch (this.deploymentType) { case DeployType.native: receipt = await omniteAxelarBridgeSender.deployNativeCollection( await this.prepareAxelarNetworkConstructorParams(), { collectionName: this.deployData.collectionName, refundAddress: ownerAddress, gasAmount, owner: ownerAddress, }, estimatedFee.totalGas ); break; default: throw new Error(`Deployment type is not supported`); } } return receipt; }; prepareDeployTransaction = async () => { const ownerAddress = await this.signer.getAddress(); const estimatedFee = await this.estimatedFee(); const chainIdGas = estimatedFee.gasLimitPerNetwork[this.sourceChainId] ? this.sourceChainId : Object.keys(estimatedFee.gasLimitPerNetwork).pop(); if (!chainIdGas) { throw new Error('No gas estimation'); } const gasAmount = estimatedFee.gasLimitPerNetwork[chainIdGas].toNumber(); let tx: ethers.PopulatedTransaction | undefined; if (this.deployBroker === BridgeBroker.LayerZero) { const omniteLayerZeroBridgeSender = await OmniteLayerZeroBridgeSender.create(this.signer); switch (this.deploymentType) { case DeployType.native: tx = await omniteLayerZeroBridgeSender.populateDeployNativeCollection( await this.prepareLayerZeroNetworkConstructorParams(), { collectionName: this.deployData.collectionName, refundAddress: ownerAddress, gasAmount, owner: ownerAddress, }, estimatedFee.totalGas ); break; default: throw new Error(`Deployment type is not supported`); } } else { const omniteAxelarBridgeSender = await OmniteAxelarBridgeSender.create(this.signer); switch (this.deploymentType) { case DeployType.native: tx = await omniteAxelarBridgeSender.populateDeployNativeCollection( await this.prepareAxelarNetworkConstructorParams(), { collectionName: this.deployData.collectionName, refundAddress: ownerAddress, gasAmount, owner: ownerAddress, }, estimatedFee.totalGas ); break; default: throw new Error(`Deployment type is not supported`); } } return tx; }; private encodeCtorParams = async ( chainId: string, callParams: { slotStart?: number; slotEnd?: number; originalContractAddress?: string; baseTokenUri?: string; contractUri?: string; } ) => { return CollectionCreator.encodeCtorParams( this.blueprintName, chainId, this.bridgeBrokers, this.deployData, callParams ); }; static encodeCtorParams = async ( blueprintName: DeployBlueprintName, sourceChainId: string, bridgeProviders: Array, deployData: DeployData, callParams: { slotStart?: number; slotEnd?: number; originalContractAddress?: string; baseTokenUri?: string; contractUri?: string; } ) => { const blueprintAddress = await ContractFactory.getLatestBlueprint( sourceChainId, blueprintName ); switch (blueprintName) { case DeployBlueprintName.ERC721Native: if ( callParams.slotStart === undefined || callParams.slotEnd === undefined || callParams.baseTokenUri === undefined ) { throw new Error( 'slotStart, slotEnd, and baseTokenUri must be specified' ); } return ERC721Native.encode( sourceChainId, blueprintAddress, bridgeProviders, deployData.collectionName, deployData.collectionTicker, deployData.userAddress, callParams.slotStart, callParams.slotEnd, callParams.baseTokenUri, callParams.contractUri || '' ); // case DeployBlueprintName.ERC721NonNative: // return ERC721NonNative.encodeInitializer( // sourceChainId, // blueprintAddress, // deployData.collectionName, // deployData.collectionTicker, // deployData.userAddress, // callParams.contractUri || '' // ); // case DeployBlueprintName.NonNativeWrapper: // if (callParams.originalContractAddress === undefined) { // throw new Error( // 'originalContractAddress must be specified' // ); // } // return NonNativeWrapper.encodeInitializer( // sourceChainId, // blueprintAddress, // callParams.originalContractAddress // ); default: throw new Error(`Unsupported blueprint: "${blueprintName}"`); } }; static generateDeployPayload = async ( chainId: string, network: NetworkDistributionWithIds, bridgeProviders: Array, deployBroker: BridgeBroker, deployData: DeployData, isNative: boolean = true, baseTokenUri?: string ) => { const chainConfig = getWalletConfigByChainId(chainId); if (!chainConfig) { throw new Error('Invalid chain config'); } const blueprintName = isNative ? DeployBlueprintName.ERC721Native : DeployBlueprintName.ERC721NonNative; const constructorParams = await CollectionCreator.encodeCtorParams( blueprintName, chainId, bridgeProviders, deployData, isNative ? { slotStart: network.startId, slotEnd: network.lastId, baseTokenUri, } : {} ); const collectionId = utils.solidityKeccak256( ['string'], [Math.random().toString()] ); switch (deployBroker) { case BridgeBroker.LayerZero: return OmniteLayerZeroBridgeSender.deployTokenContractEncode( chainConfig.chainId, blueprintName, collectionId, // unused by contracts constructorParams, deployData.collectionName, deployData.userAddress ); case BridgeBroker.Axelar: return OmniteAxelarBridgeSender.deployTokenContractEncode( chainConfig.chainId, blueprintName, collectionId, // unused by contracts constructorParams, deployData.collectionName, deployData.userAddress ); default: throw new Error(`Unsupported broker: "${deployBroker}"`); } }; static estimateDeploymentFees = async ( sourceChainId: string, availableBridgeProviders: Array, bridgeBroker: BridgeBroker, networks: Array, deployData: DeployData, isNative: boolean = true ) => { let totalGas: BigNumber = ethers.utils.parseUnits('0.0', 'ether'); const gasPerNetwork: { [chainId: string]: BigNumber } = {}; const gasLimitPerNetwork: { [chainId: string]: BigNumber } = {}; const networksWithRange = CollectionCreator.assignRangeToNetworks(networks); let payload: string = ''; for (const network of networksWithRange) { try { const targetChainConfig = getWalletConfigByChainId( network.chainId ); if (!targetChainConfig) { throw new Error('No bridge address'); } payload = await CollectionCreator.generateDeployPayload( targetChainConfig.chainId, network, availableBridgeProviders, bridgeBroker, deployData, isNative, 'testtokenbaseuri' ); const estimatedGasLimit = bridgeBroker === BridgeBroker.LayerZero ? await LayerZero.estimateReceiveGas( sourceChainId, deployData.userAddress, network.chainId, payload ) : await Axelar.estimateReceiveGas( sourceChainId, deployData.userAddress, network.chainId, payload ); if (network.chainId === sourceChainId) { gasPerNetwork[sourceChainId] = BigNumber.from('0'); gasLimitPerNetwork[sourceChainId] = estimatedGasLimit; continue; } const nativeFee = bridgeBroker === BridgeBroker.LayerZero ? await LayerZero.estimateFees( sourceChainId, targetChainConfig.chainId, targetChainConfig.contractAddress .omniteLayerZeroBridgeSender, payload, estimatedGasLimit ) : await Axelar.estimateFees( sourceChainId, targetChainConfig.chainId, estimatedGasLimit ); gasPerNetwork[targetChainConfig.chainId] = nativeFee; gasLimitPerNetwork[targetChainConfig.chainId] = estimatedGasLimit; totalGas = totalGas.add( gasPerNetwork[targetChainConfig.chainId] ); } catch (err) { console.error(err); totalGas = totalGas.add( ethers.utils.parseEther(config.defaults.bridgeFee) ); } } return { totalGas, gasPerNetwork, gasLimitPerNetwork, payload }; }; static assignRangeToNetworks = (networks: Array) => { let startId = 1; const networksWithIds: Array = []; for (const network of networks) { const lastId = startId + Number(network.amount); networksWithIds.push({ ...network, startId, lastId: lastId - 1 }); startId = lastId; } return networksWithIds; }; }