Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ingestor highlight #29

Merged
merged 7 commits into from
Jul 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions src/ingestors/highlight/abi.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
export const MINT_CONTRACT_ABI = [
{
inputs: [{ internalType: 'uint256', name: 'vectorId', type: 'uint256' }],
name: 'getAbridgedVector',
outputs: [
{
components: [
{ internalType: 'address', name: 'contractAddress', type: 'address' },
{ internalType: 'uint48', name: 'startTimestamp', type: 'uint48' },
{ internalType: 'uint48', name: 'endTimestamp', type: 'uint48' },
{ internalType: 'address', name: 'paymentRecipient', type: 'address' },
{ internalType: 'uint48', name: 'maxTotalClaimableViaVector', type: 'uint48' },
{ internalType: 'uint48', name: 'totalClaimedViaVector', type: 'uint48' },
{ internalType: 'address', name: 'currency', type: 'address' },
{ internalType: 'uint48', name: 'tokenLimitPerTx', type: 'uint48' },
{ internalType: 'uint48', name: 'maxUserClaimableViaVector', type: 'uint48' },
{ internalType: 'uint192', name: 'pricePerToken', type: 'uint192' },
{ internalType: 'uint48', name: 'editionId', type: 'uint48' },
{ internalType: 'bool', name: 'editionBasedCollection', type: 'bool' },
{ internalType: 'bool', name: 'requireDirectEOA', type: 'bool' },
{ internalType: 'bytes32', name: 'allowlistRoot', type: 'bytes32' },
],
internalType: 'struct IAbridgedMintVector.AbridgedVector',
name: '',
type: 'tuple',
},
],
stateMutability: 'view',
type: 'function',
},
{
inputs: [
{ internalType: 'uint256', name: 'vectorId', type: 'uint256' },
{ internalType: 'uint48', name: 'numTokensToMint', type: 'uint48' },
{ internalType: 'address', name: 'mintRecipient', type: 'address' },
],
name: 'vectorMint721',
outputs: [],
stateMutability: 'payable',
type: 'function',
},
];
156 changes: 156 additions & 0 deletions src/ingestors/highlight/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
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 { getHighlightMetadata, getHighlightMintPriceInWei } from './onchain-metadata';
import {
getHighlightCollectionByAddress,
getHighlightCollectionById,
getHighlightCollectionOwnerDetails,
getHighlightVectorId,
} from './offchain-metadata';
import { MINT_CONTRACT_ABI } from './abi';

const CONTRACT_ADDRESS = '0x8087039152c472Fa74F47398628fF002994056EA';

export class HighlightIngestor implements MintIngestor {
async supportsUrl(resources: MintIngestorResources, url: string): Promise<boolean> {
const id = url.split('/').pop();
if (!id) {
return false;
}

const collection = await getHighlightCollectionById(resources, id);

if (!collection || collection.chainId !== 8453) {
return false;
}

const urlPattern = /^https:\/\/highlight\.xyz\/mint\/[a-f0-9]{24}$/;
return (
new URL(url).hostname === 'www.highlight.xyz' || new URL(url).hostname === 'highlight.xyz' || urlPattern.test(url)
);
}

async supportsContract(resources: MintIngestorResources, contractOptions: MintContractOptions): Promise<boolean> {
if (contractOptions.chainId !== 8453) {
return false;
}
const collection = await getHighlightCollectionByAddress(resources, contractOptions);
if (!collection) {
return false;
}
return true;
}

async createMintForContract(
resources: MintIngestorResources,
contractOptions: MintContractOptions,
): Promise<MintTemplate> {
const mintBuilder = new MintTemplateBuilder()
.setMintInstructionType(MintInstructionType.EVM_MINT)
.setPartnerName('Highlight');

if (contractOptions.url) {
mintBuilder.setMarketingUrl(contractOptions.url);
}

const collection = await getHighlightCollectionByAddress(resources, contractOptions);

if (!collection) {
throw new MintIngestorError(MintIngestionErrorName.CouldNotResolveMint, 'Collection not found');
}

const contractAddress = collection.contract;
const description = collection?.description;

mintBuilder
.setName(collection.name)
.setDescription(description)
.setFeaturedImageUrl(collection.image.split('?')[0]);
mintBuilder.setMintOutputContract({ chainId: 8453, address: contractAddress });

if (collection.sampleImages.length) {
collection.sampleImages.forEach((url, index) => {
mintBuilder.addImage(url, `Sample image #${index}`);
});
}

if (!collection.creator) {
throw new MintIngestorError(MintIngestionErrorName.MissingRequiredData, 'Error finding creator');
}

const creator = await getHighlightCollectionOwnerDetails(resources, collection.highlightCollection.id);

mintBuilder.setCreator({
name: creator.creatorAccountSettings.displayName,
walletAddress: collection.creator,
imageUrl: creator.creatorAccountSettings.displayAvatar,
});

const vectorId = await getHighlightVectorId(resources, collection.highlightCollection.id);

if (!vectorId) {
throw new MintIngestorError(MintIngestionErrorName.MissingRequiredData, 'Id not available');
}

const totalPriceWei = await getHighlightMintPriceInWei(+vectorId, resources.alchemy);

if (!totalPriceWei) {
throw new MintIngestorError(MintIngestionErrorName.MissingRequiredData, 'Price not available');
}

mintBuilder.setMintInstructions({
chainId: 8453,
contractAddress: CONTRACT_ADDRESS,
contractMethod: 'vectorMint721',
contractParams: `[${vectorId}, 1, address]`,
abi: MINT_CONTRACT_ABI,
priceWei: totalPriceWei,
});

const metadata = await getHighlightMetadata(+vectorId, resources.alchemy);

if (!metadata) {
throw new MintIngestorError(MintIngestionErrorName.MissingRequiredData, 'Missing timestamps');
}

const { startTimestamp, endTimestamp } = metadata;

const liveDate = +new Date() > startTimestamp * 1000 ? new Date() : new Date(startTimestamp * 1000);
mintBuilder
.setAvailableForPurchaseStart(new Date(startTimestamp * 1000 || Date.now()))
.setAvailableForPurchaseEnd(new Date(endTimestamp * 1000 || '2030-01-01'))
.setLiveDate(liveDate);

return mintBuilder.build();
}

async createMintTemplateForUrl(resources: MintIngestorResources, url: string): Promise<MintTemplate> {
const isCompatible = await this.supportsUrl(resources, url);
if (!isCompatible) {
throw new MintIngestorError(MintIngestionErrorName.IncompatibleUrl, 'Incompatible URL');
}

// Example URL: https://highlight.xyz/mint/665fa33f07b3436991e55632
const splits = url.split('/');
const id = splits.pop();
const chain = splits.pop();

if (!id) {
throw new MintIngestorError(MintIngestionErrorName.CouldNotResolveMint, 'Url error');
}

const collection = await getHighlightCollectionById(resources, id);

if (!collection) {
throw new MintIngestorError(MintIngestionErrorName.CouldNotResolveMint, 'No such collection');
}

return this.createMintForContract(resources, {
chainId: collection.chainId,
contractAddress: collection.address,
url,
});
}
}
160 changes: 160 additions & 0 deletions src/ingestors/highlight/offchain-metadata.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
import { MintContractOptions, MintIngestorResources } from 'src/lib';
import { Collection, CollectionByAddress, CollectionByAddress1 } from './types';

export const getHighlightCollectionById = async (
resources: MintIngestorResources,
id: string,
): Promise<Collection | undefined> => {
const url = 'https://api.highlight.xyz:8080/';

const headers = {
accept: 'application/json',
'content-type': 'application/json',
};

const data = {
operationName: 'GetCollectionDetails',
variables: {
collectionId: id,
},
query: `
query GetCollectionDetails($collectionId: String!) {
getPublicCollectionDetails(collectionId: $collectionId) {
id
name
description
collectionImage
marketplaceId
accountId
address
symbol
chainId
status
baseUri
onChainBaseUri
}
}
`,
};

try {
const resp = await resources.fetcher.post(url, data, { headers });
if (!resp.data.data) {
throw new Error('Empty response');
}
return resp.data.data.getPublicCollectionDetails;
} catch (error) {}
};

export const getHighlightCollectionByAddress = async (
resources: MintIngestorResources,
contractOptioons: MintContractOptions,
): Promise<CollectionByAddress | undefined> => {
try {
const resp = await resources.fetcher(
`https://marketplace.highlight.xyz/reservoir/base/collections/v7?id=${contractOptioons.contractAddress}&normalizeRoyalties=false`,
);
const collection1: CollectionByAddress1 = resp.data.collections.find((c: CollectionByAddress) => {
return c.id.toLowerCase() === contractOptioons.contractAddress.toLowerCase() && c.chainId === 8453;
});

const resp2 = await resources.fetcher(
`https://marketplace.highlight.xyz/reservoir/base/tokens/v7?collection=${contractOptioons.contractAddress}&limit=1&normalizeRoyalties=false`,
);
const collection2 = resp2.data.tokens[0];
return {
...collection1,
...collection2,
};
} catch (error) {}
};

export const getHighlightVectorId = async (resources: MintIngestorResources, id: string): Promise<string | undefined> => {
const url = 'https://api.highlight.xyz:8080/';

const headers = {
accept: 'application/json',
'content-type': 'application/json',
};

const data = {
operationName: 'GetCollectionSaleDetails',
variables: {
collectionId: id,
},
query: `
query GetCollectionSaleDetails($collectionId: String!) {
getPublicCollectionDetails(collectionId: $collectionId) {
size
mintVectors {
name
start
end
paused
price
currency
chainId
paymentCurrency {
address
decimals
symbol
type
mintFee
}
onchainMintVectorId
}
}
}
`,
};

try {
const resp = await resources.fetcher.post(url, data, { headers });
const vectorString = resp.data.data.getPublicCollectionDetails.mintVectors.find(
(c: { chainId: number }) => c.chainId === 8453,
).onchainMintVectorId;
const vectorId = vectorString.split(':').pop();
return vectorId;
} catch (error) {}
};

export const getHighlightCollectionOwnerDetails = async (resources: MintIngestorResources, id: string) => {
const url = 'https://api.highlight.xyz:8080/';
const data = {
operationName: 'GetCollectionCreatorDetails',
variables: {
withEns: true,
collectionId: id,
},
query: `query GetCollectionCreatorDetails($collectionId: String!, $withEns: Boolean) {
getPublicCollectionDetails(collectionId: $collectionId) {
id
creatorAddresses {
address
name
}
creatorEns
creatorAccountSettings(withEns: $withEns) {
verified
imported
displayAvatar
displayName
walletAddresses
}
}
}`,
};

const headers = {
accept: 'application/json',
'content-type': 'application/json',
};

try {
const resp = await resources.fetcher.post(url, data, { headers });
if (resp.data.errors) {
throw new Error("Error fetching owner");
}
return resp.data.data.getPublicCollectionDetails;
} catch (error) {}
};
37 changes: 37 additions & 0 deletions src/ingestors/highlight/onchain-metadata.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { Alchemy, Contract } from 'alchemy-sdk';
import { MINT_CONTRACT_ABI } from './abi';

const CONTRACT_ADDRESS = '0x8087039152c472Fa74F47398628fF002994056EA';

const getContract = async (alchemy: Alchemy): Promise<Contract> => {
const ethersProvider = await alchemy.config.getProvider();
const contract = new Contract(CONTRACT_ADDRESS, MINT_CONTRACT_ABI, ethersProvider);
return contract;
};

export const getHighlightMintPriceInWei = async (vectorId: number, alchemy: Alchemy): Promise<string | undefined> => {
try {
const contract = await getContract(alchemy);
const data = await contract.functions.getAbridgedVector(vectorId);
const { pricePerToken } = data[0];

const fee = 800000000000000;
const totalFee = parseInt(pricePerToken.toString()) + fee;

return `${totalFee}`;
} catch (error) {
console.log(error);
}
};

export const getHighlightMetadata = async (
vectorId: number,
alchemy: Alchemy,
): Promise<{ startTimestamp: number; endTimestamp: number } | undefined> => {
try {
const contract = await getContract(alchemy);
const metadata = await contract.functions.getAbridgedVector(vectorId);
const { startTimestamp, endTimestamp } = metadata[0];
return { startTimestamp, endTimestamp };
} catch (error) {}
};
Loading