Skip to content

Commit

Permalink
Merge branch 'opensea'
Browse files Browse the repository at this point in the history
Add OpenSea Base Mint Ingestor
  • Loading branch information
chainparser committed Jul 6, 2024
2 parents 3948da4 + 0fb201c commit 88c8f09
Show file tree
Hide file tree
Showing 6 changed files with 384 additions and 0 deletions.
2 changes: 2 additions & 0 deletions src/ingestors/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { MintIngestor } from '../lib/types/mint-ingestor';
import { ProhibitionDailyIngestor } from './prohibition-daily';
import { FxHashIngestor } from './fxhash';
import { OpenSeaIngestor } from './opensea';

export type MintIngestionMap = {
[key: string]: MintIngestor;
Expand All @@ -9,6 +10,7 @@ export type MintIngestionMap = {
export const ALL_MINT_INGESTORS: MintIngestionMap = {
'prohibition-daily': new ProhibitionDailyIngestor(),
fxhash: new FxHashIngestor(),
opensea: new OpenSeaIngestor(),
};

export * from './';
113 changes: 113 additions & 0 deletions src/ingestors/opensea/abi.ts
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"
}
];
84 changes: 84 additions & 0 deletions src/ingestors/opensea/index.ts
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;
}
}
33 changes: 33 additions & 0 deletions src/ingestors/opensea/offchain-metadata.ts
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;
}
};
96 changes: 96 additions & 0 deletions src/ingestors/opensea/onchain-metadata.ts
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;
}
};

Loading

0 comments on commit 88c8f09

Please sign in to comment.