-
Notifications
You must be signed in to change notification settings - Fork 8
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add OpenSea Base Mint Ingestor
- Loading branch information
Showing
6 changed files
with
384 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,113 @@ | ||
export const OPENSEA_DROPS_ABI = [ | ||
{ | ||
"inputs": [ | ||
{ | ||
"internalType": "address", | ||
"name": "nftContract", | ||
"type": "address" | ||
} | ||
], | ||
"name": "getPublicDrop", | ||
"outputs": [ | ||
{ | ||
"components": [ | ||
{ | ||
"internalType": "uint80", | ||
"name": "mintPrice", | ||
"type": "uint80" | ||
}, | ||
{ | ||
"internalType": "uint48", | ||
"name": "startTime", | ||
"type": "uint48" | ||
}, | ||
{ | ||
"internalType": "uint48", | ||
"name": "endTime", | ||
"type": "uint48" | ||
}, | ||
{ | ||
"internalType": "uint16", | ||
"name": "maxTotalMintableByWallet", | ||
"type": "uint16" | ||
}, | ||
{ | ||
"internalType": "uint16", | ||
"name": "feeBps", | ||
"type": "uint16" | ||
}, | ||
{ | ||
"internalType": "bool", | ||
"name": "restrictFeeRecipients", | ||
"type": "bool" | ||
} | ||
], | ||
"internalType": "struct PublicDrop", | ||
"name": "", | ||
"type": "tuple" | ||
} | ||
], | ||
"stateMutability": "view", | ||
"type": "function" | ||
} | ||
]; | ||
|
||
export const OPENSEA_PROXY_ABI = [ | ||
{ | ||
"inputs": [], | ||
"name": "contractURI", | ||
"outputs": [ | ||
{ | ||
"internalType": "string", | ||
"name": "", | ||
"type": "string" | ||
} | ||
], | ||
"stateMutability": "view", | ||
"type": "function" | ||
}, | ||
{ | ||
"inputs": [], | ||
"name": "baseURI", | ||
"outputs": [ | ||
{ | ||
"internalType": "string", | ||
"name": "", | ||
"type": "string" | ||
} | ||
], | ||
"stateMutability": "view", | ||
"type": "function" | ||
}, | ||
{ | ||
"inputs": [], | ||
"name": "name", | ||
"outputs": [ | ||
{ | ||
"internalType": "string", | ||
"name": "", | ||
"type": "string" | ||
} | ||
], | ||
"stateMutability": "view", | ||
"type": "function" | ||
}, | ||
{ | ||
"inputs": [ | ||
{ | ||
"internalType": "address", | ||
"name": "minter", | ||
"type": "address" | ||
}, | ||
{ | ||
"internalType": "uint256", | ||
"name": "quantity", | ||
"type": "uint256" | ||
} | ||
], | ||
"name": "mintSeaDrop", | ||
"outputs": [], | ||
"stateMutability": "nonpayable", | ||
"type": "function" | ||
} | ||
]; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,84 @@ | ||
import { MintContractOptions, MintIngestor, MintIngestorResources } from '../../lib/types/mint-ingestor'; | ||
import { MintIngestionErrorName, MintIngestorError } from '../../lib/types/mint-ingestor-error'; | ||
import { MintInstructionType, MintTemplate } from '../../lib/types/mint-template'; | ||
import { MintTemplateBuilder } from '../../lib/builder/mint-template-builder'; | ||
import { OPENSEA_PROXY_ABI } from './abi'; | ||
import { getOpenSeaDropContractMetadata, getOpenSeaDropPriceInEth, urlForValidOpenSeaDropContract } from './onchain-metadata'; | ||
|
||
export class OpenSeaIngestor implements MintIngestor { | ||
|
||
configuration = { | ||
supportsContractIsExpensive: true, | ||
}; | ||
|
||
async supportsUrl(resources: MintIngestorResources, url: string): Promise<boolean> { | ||
return false; // This ingestor does not support ingesting via URL | ||
} | ||
|
||
async supportsContract(resources: MintIngestorResources, contract: MintContractOptions): Promise<boolean> { | ||
const { chainId, contractAddress } = contract; | ||
if (!chainId || !contractAddress) { | ||
return false; | ||
} | ||
|
||
const url = await urlForValidOpenSeaDropContract(chainId, contractAddress, resources.alchemy); | ||
|
||
return !!url; | ||
} | ||
|
||
async createMintTemplateForUrl(resources: MintIngestorResources, url: string): Promise<any> { | ||
const isCompatible = await this.supportsUrl(resources, url); | ||
if (!isCompatible) { | ||
throw new MintIngestorError(MintIngestionErrorName.IncompatibleUrl, 'Incompatible URL'); | ||
} | ||
|
||
return false; // This ingestor does not support ingesting via URL | ||
} | ||
|
||
async createMintForContract(resources: MintIngestorResources, contract: MintContractOptions): Promise<MintTemplate> { | ||
|
||
const { chainId, contractAddress } = contract; | ||
if (!chainId || !contractAddress) { | ||
throw new MintIngestorError(MintIngestionErrorName.MissingRequiredData, 'Missing required data'); | ||
} | ||
|
||
const mintBuilder = new MintTemplateBuilder() | ||
.setMintInstructionType(MintInstructionType.EVM_MINT) | ||
.setPartnerName('OpenSea'); | ||
|
||
|
||
const url = await urlForValidOpenSeaDropContract(chainId, contractAddress, resources.alchemy); | ||
|
||
if (url) { | ||
mintBuilder.setMarketingUrl(url); | ||
} | ||
|
||
const { name, description, image, startDate, endDate } = await getOpenSeaDropContractMetadata( | ||
chainId, | ||
contractAddress, | ||
resources.alchemy, | ||
); | ||
|
||
mintBuilder.setName(name).setDescription(description).setFeaturedImageUrl(image); | ||
|
||
const totalPriceWei = await getOpenSeaDropPriceInEth(chainId, contractAddress, resources.alchemy); | ||
|
||
mintBuilder.setMintInstructions({ | ||
chainId, | ||
contractAddress, | ||
contractMethod: 'mintSeaDrop', | ||
contractParams: '[address, 1]', | ||
abi: OPENSEA_PROXY_ABI, | ||
priceWei: totalPriceWei, | ||
}); | ||
|
||
const liveDate = new Date() > startDate ? new Date() : startDate; | ||
mintBuilder | ||
.setAvailableForPurchaseEnd(endDate || new Date('2030-01-01')) | ||
.setAvailableForPurchaseStart(startDate || new Date()) | ||
.setLiveDate(liveDate); | ||
|
||
const output = mintBuilder.build(); | ||
return output; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
import { Axios } from "axios"; | ||
|
||
const ipfsGateway = 'https://ipfs.io/ipfs/'; // IPFS gateway URL (replace if needed) | ||
|
||
export const openSeaOnchainDataFromIpfsUrl = async (baseuri: string, fetcher: Axios): Promise<{ name: string, description: string; image: string } | undefined> => { | ||
const ipfsHash = new URL(baseuri).href.split('/').slice(-1)[0]; | ||
const url = `${ipfsGateway}${ipfsHash}`; | ||
|
||
try { | ||
const response = await fetcher.get(url); | ||
|
||
if (response.status !== 200) { | ||
return undefined; | ||
} | ||
|
||
const data = response.data; | ||
|
||
// Ensure data has the expected structure | ||
if (!data.hasOwnProperty('name') || !data.hasOwnProperty('description') || !data.hasOwnProperty('image')) { | ||
return undefined; | ||
} | ||
|
||
const imageHash = new URL(data.image).href.split('/').slice(-1)[0]; | ||
|
||
return { | ||
name: data.name, | ||
description: data.description, | ||
image: `${ipfsGateway}${imageHash}`, | ||
}; | ||
} catch (error) { | ||
return undefined; | ||
} | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,96 @@ | ||
import { Alchemy, Contract } from 'alchemy-sdk'; | ||
import { OPENSEA_DROPS_ABI, OPENSEA_PROXY_ABI } from './abi'; | ||
import { openSeaOnchainDataFromIpfsUrl } from './offchain-metadata'; | ||
import axios from "axios"; | ||
|
||
// Declare global constant for OpenSea Drops implementation contract address | ||
// Proxy contracts which are the mint contracts will be defined as nftContract | ||
const CONTRACT_ADDRESS = "0x00005ea00ac477b1030ce78506496e8c2de24bf5"; | ||
|
||
// Get the contract from contractAddress | ||
const getContract = async ( | ||
contractAddress: string, | ||
abi: any, | ||
alchemy: Alchemy): Promise<Contract> => { | ||
const ethersProvider = await alchemy.config.getProvider(); | ||
const contract = new Contract(contractAddress, abi, ethersProvider); | ||
return contract; | ||
}; | ||
|
||
export const getOpenSeaDropContractMetadata = async ( | ||
chainId: number, | ||
nftContractAddress: string, | ||
alchemy: Alchemy, | ||
): Promise<any> => { | ||
const nftContract = await getContract(nftContractAddress, OPENSEA_PROXY_ABI, alchemy); | ||
const contract = await getContract(CONTRACT_ADDRESS, OPENSEA_DROPS_ABI, alchemy); | ||
|
||
const baseuri = await nftContract.functions.baseURI(); | ||
const { name, description, image } = await openSeaOnchainDataFromIpfsUrl(baseuri, axios) || {}; | ||
const metadata = await contract.functions.getPublicDrop(nftContractAddress); | ||
|
||
const {startTime, endTime} = metadata[0]; | ||
|
||
return { | ||
name, | ||
description, | ||
image, | ||
startDate: new Date(startTime * 1000), | ||
endDate: new Date(endTime * 1000), | ||
}; | ||
}; | ||
|
||
export const getOpenSeaDropPriceInEth = async ( | ||
chainId: number, | ||
nftContract: string, | ||
alchemy: Alchemy, | ||
): Promise<any> => { | ||
const contract = await getContract(CONTRACT_ADDRESS, OPENSEA_DROPS_ABI, alchemy); | ||
const metadata = await contract.functions.getPublicDrop(nftContract); | ||
|
||
const {mintPrice, feeBps} = metadata[0]; | ||
// Convert basis points to decimal value | ||
const feeRatio = feeBps / 10000; | ||
|
||
// Calculate the fee amount | ||
const feePrice = feeRatio * mintPrice; | ||
|
||
const totalFee = parseInt(feePrice.toString()) + parseInt(mintPrice.toString()); | ||
return `${totalFee}`; | ||
}; | ||
|
||
// Function to get the URL for a valid OpenSea Drop contract | ||
export const urlForValidOpenSeaDropContract = async ( | ||
chainId: number, | ||
contractAddress: string, | ||
alchemy: Alchemy, | ||
): Promise<string | undefined> => { | ||
try { | ||
// Get the contract | ||
const contract = await getContract(contractAddress, OPENSEA_PROXY_ABI, alchemy); | ||
if (!contract) { | ||
return undefined; | ||
} | ||
|
||
// Fetch the contract name | ||
let response; | ||
try { | ||
response = await contract.functions.name(); | ||
} catch { | ||
return undefined; | ||
} | ||
|
||
// Clean and format the name for the URL | ||
const formattedName = response[0] | ||
.replace(/[^a-zA-Z0-9]+/g, "-") // Replace sequences of non-alphanumeric characters with a single hyphen | ||
.toLowerCase(); // Convert to lowercase | ||
|
||
// Construct the URL | ||
const url = `https://opensea.io/collection/${formattedName}/overview`; | ||
|
||
return url; | ||
} catch (error) { | ||
return undefined; | ||
} | ||
}; | ||
|
Oops, something went wrong.