Skip to content

Instantly share code, notes, and snippets.

@Deathwing
Created July 18, 2022 19:26
Show Gist options
  • Save Deathwing/b49c03c465c17312f8affc536b5ea64d to your computer and use it in GitHub Desktop.
Save Deathwing/b49c03c465c17312f8affc536b5ea64d to your computer and use it in GitHub Desktop.
Working ERC721Factory + CreateSellOffer for OpenSea
// SPDX-License-Identifier: CC-BY-NC-4.0
pragma solidity 0.8.15;
import "@openzeppelin/contracts/token/ERC721/IERC721.sol";
import "@openzeppelin/contracts/utils/Context.sol";
import "@openzeppelin/contracts/utils/Strings.sol";
contract OwnableDelegateProxy {}
contract ProxyRegistry {
mapping(address => OwnableDelegateProxy) public proxies;
}
interface IERC721Optimized {
function factoryMint(address to, uint64 amount) external;
}
interface IERC721Factory {
function name() external view returns (string memory);
function symbol() external view returns (string memory);
function numOptions() external view returns (uint256);
function canMint(uint256 _optionId) external view returns (bool);
function tokenURI(uint256 _optionId) external view returns (string memory);
function supportsFactoryInterface() external view returns (bool);
function mint(uint256 _optionId, address _toAddress) external;
}
contract ERC721Factory is Context, IERC721, IERC721Factory {
using Strings for uint256;
struct OptionConfig {
uint64[] mintAmount;
}
string private _name;
string private _symbol;
string private _baseURI;
OptionConfig private _optionConfig;
IERC721Optimized private _erc721Optimized;
address private _proxyRegistryAddress;
mapping(address => mapping(address => bool)) private _operatorApprovals;
constructor(string memory name_, string memory symbol_, string memory baseURI_, OptionConfig memory optionConfig_, address payable erc721OptimizedAddress_, address proxyRegistryAddress_) {
require(bytes(name_).length > 0, "ERC721Factory: name can't be empty");
require(bytes(symbol_).length > 0, "ERC721Factory: symbol can't be empty");
require(bytes(baseURI_).length > 0, "ERC721Factory: baseURI can't be empty");
require(optionConfig_.mintAmount.length > 0, "ERC721Factory: optionConfig's mintAmount can't be empty");
require(erc721OptimizedAddress_ != address(0), "ERC721Factory: erc721OptimizedAddress can't be null address");
if (proxyRegistryAddress_ != address(0))
ProxyRegistry(proxyRegistryAddress_).proxies(_msgSender());
_name = name_;
_symbol = symbol_;
_baseURI = baseURI_;
_optionConfig = optionConfig_;
_erc721Optimized = IERC721Optimized(erc721OptimizedAddress_);
_proxyRegistryAddress = proxyRegistryAddress_;
_fireTransferEvents(address(0), owner());
}
function supportsInterface(bytes4 interfaceId) external pure returns (bool) {
return interfaceId == type(IERC721).interfaceId || interfaceId == type(IERC721Factory).interfaceId || interfaceId == type(IERC165).interfaceId;
}
function name() external view returns (string memory) {
return _name;
}
function symbol() external view returns (string memory) {
return _symbol;
}
function baseURI() external view returns (string memory) {
return _baseURI;
}
function optionConfig() external view returns (OptionConfig memory) {
return _optionConfig;
}
function erc721OptimizedAddress() external view returns (address) {
return address(_erc721Optimized);
}
function proxyRegistryAddress() external view returns (address) {
return _proxyRegistryAddress;
}
function numOptions() external view returns (uint256) {
return _optionConfig.mintAmount.length;
}
function setBaseURI(string calldata baseURI_) external onlyOwner {
require(bytes(baseURI_).length > 0, "ERC721Factory: baseURI can't be empty");
_baseURI = baseURI_;
}
function setOptionConfig(OptionConfig memory optionConfig_) external onlyOwner {
require(optionConfig_.mintAmount.length > 0, "ERC721Factory: optionConfig's mintAmount can't be empty");
_fireTransferEvents(owner(), address(0));
_optionConfig = optionConfig_;
_fireTransferEvents(address(0), owner());
}
function setProxyRegistryAddress(address proxyRegistryAddress_) external onlyOwner {
if (proxyRegistryAddress_ != address(0))
ProxyRegistry(proxyRegistryAddress_).proxies(_msgSender());
_proxyRegistryAddress = proxyRegistryAddress_;
}
function contractURI() external view returns (string memory) {
return string(abi.encodePacked(_baseURI, "contract"));
}
function canMint(uint256 _optionId) external view returns (bool) {
return _canMint(_optionId);
}
function tokenURI(uint256 _optionId) external view returns (string memory) {
return string(abi.encodePacked(_baseURI, _optionId.toString()));
}
function supportsFactoryInterface() external pure returns (bool) {
return true;
}
function mint(uint256 _optionId, address _toAddress) external {
_mint(_optionId, _toAddress);
}
function transferOwnership(address newOwner) override public onlyOwner {
address _prevOwner = owner();
super.transferOwnership(newOwner);
_fireTransferEvents(_prevOwner, newOwner);
}
function _canMint(uint256 _optionId) private view returns (bool) {
if (_optionId >= _optionConfig.mintAmount.length)
return false;
return true;
}
function _mint(uint256 _optionId, address _toAddress) private {
assert((_proxyRegistryAddress != address(0) && address(ProxyRegistry(_proxyRegistryAddress).proxies(owner())) == _msgSender()) || owner() == _msgSender());
require(_canMint(_optionId), "ERC721Factory: can't mint");
_erc721Optimized.factoryMint(_toAddress, _optionConfig.mintAmount[_optionId]);
}
function _fireTransferEvents(address _from, address _to) private {
for (uint256 i = 0; i < _optionConfig.mintAmount.length; i++)
emit Transfer(_from, _to, i);
}
/**
* Hack to get things to work automatically on OpenSea.
* Use IERC721 so the frontend doesn't have to worry about different method names.
*/
function approve(address, uint256) external {}
function setApprovalForAll(address _operator, bool _approved) external {
_operatorApprovals[_msgSender()][_operator] = _approved;
}
function transferFrom(address, address _to, uint256 _tokenId) external {
_mint(_tokenId, _to);
}
function safeTransferFrom(address, address _to, uint256 _tokenId) external {
_mint(_tokenId, _to);
}
function safeTransferFrom(address, address _to, uint256 _tokenId, bytes calldata) external {
_mint(_tokenId, _to);
}
function isApprovedForAll(address _owner, address _operator) external view returns (bool) {
if (owner() == _owner && _owner == _operator)
return true;
if (owner() == _owner && (_proxyRegistryAddress != address(0) && address(ProxyRegistry(_proxyRegistryAddress).proxies(_owner)) == _operator))
return true;
return _operatorApprovals[_owner][_operator];
}
function balanceOf(address _owner) external view returns (uint256 _balance) {
if (owner() == _owner)
_balance = _optionConfig.mintAmount.length;
}
function getApproved(uint256) external view returns (address) {
return owner();
}
function ownerOf(uint256) external view returns (address) {
return owner();
}
}
const HDWalletProvider = require("@truffle/hdwallet-provider");
const { Contract, getDefaultProvider, utils } = require('ethers');
const { Network, OpenSeaSDK } = require('opensea-js');
const { exit } = require('process');
const factory_abi = [{
"inputs": [],
"name": "optionConfig",
"outputs": [
{
"components": [
{
"internalType": "uint64[]",
"name": "mintAmount",
"type": "uint64[]"
}
],
"internalType": "struct ERC721Factory.OptionConfig",
"name": "",
"type": "tuple"
}
],
"stateMutability": "view",
"type": "function"
}];
const configs = {
[Network.Main]: {
factoryAddress: '<FACTORY_ADDRESS>',
provider: 'https://mainnet.infura.io/v3/<API_KEY>',
apiKey: '<API_KEY>',
},
[Network.Rinkeby]: {
factoryAddress: '<FACTORY_ADDRESS>',
provider: 'https://rinkeby.infura.io/v3/<API_KEY>',
apiKey: '<API_KEY>'
}
};
async function createFactorySellOrders(network, privateKey, ordersPerOptionId, pricePerTokenInETH) {
const config = configs[network];
const contractProvider = getDefaultProvider(config.provider);
const factoryContract = new Contract(config.factoryAddress, factory_abi, contractProvider);
const openseaProvider = new HDWalletProvider({
privateKeys: [privateKey],
provider: config.provider
});
const openseaSdk = new OpenSeaSDK(
openseaProvider.engine, {
networkName: network,
apiKey: config.apiKey
});
const accountAddress = openseaProvider.getAddress();
async function createSellOrder(optionId, startAmount) {
const sellOrder = {
asset: {
tokenId: optionId.toString(),
tokenAddress: factoryContract.address,
},
accountAddress: accountAddress,
startAmount: startAmount
};
return await openseaSdk.createSellOrder(sellOrder);
}
const optionConfig = await factoryContract.optionConfig();
const postedOffers = {};
try {
for (var i = 0; i < ordersPerOptionId; i++) {
for (var j = 0; j < optionConfig.mintAmount.length; j++) {
const optionId = j;
const amount = optionConfig.mintAmount[optionId].toNumber();
const startAmountString = (amount * pricePerTokenInETH).toString();
await createSellOrder(optionId, startAmountString);
if (!postedOffers[optionId])
postedOffers[optionId] = { amount, startAmountString, posts: 0 };
postedOffers[optionId].posts++;
}
}
} catch (e) {
console.error(e);
}
for (const optionId in postedOffers)
console.log(`Created ${postedOffers[optionId].posts} offers for optionId ${optionId} (amount: ${postedOffers[optionId].amount}) for ${postedOffers[optionId].startAmountString} ETH each.`);
}
(async () => {
const network = process.argv[2];
const privateKey = process.argv[3];
const ordersPerOptionId = Number.parseInt(process.argv[4]);
const pricePerTokenInETH = Number.parseFloat(process.argv[5]);
if (!network || !privateKey)
throw new Error('Usage: \'node opensea.js (main|rinkery) (factory-assets-owner-private-key) (order-count) (price-eth)\', i.e.: \'node opensea.js rinkery 0x123456789 100 0.1\'');
try {
await createFactorySellOrders(network, privateKey, ordersPerOptionId, pricePerTokenInETH);
exit(0);
} catch (e) {
console.error(e);
exit(1);
}
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment