Skip to content

Commit

Permalink
Merge pull request #36 from Myestery/rodeo-ingestor
Browse files Browse the repository at this point in the history
Rodeo ingestion
  • Loading branch information
chrismaddern authored Aug 22, 2024
2 parents 2d3019a + 8a2c3db commit b3e3bd4
Show file tree
Hide file tree
Showing 5 changed files with 509 additions and 0 deletions.
43 changes: 43 additions & 0 deletions src/ingestors/rodeo/abi.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
export const RODEO_ABI = [
{
inputs: [
{ internalType: 'uint256', name: 'saleTermsId', type: 'uint256' },
{ internalType: 'address payable', name: 'referrer', type: 'address' },
],
name: 'getFixedPriceSale',
outputs: [
{
components: [
{ internalType: 'address', name: 'multiTokenContract', type: 'address' },
{ internalType: 'uint256', name: 'tokenId', type: 'uint256' },
{ internalType: 'uint256', name: 'pricePerQuantity', type: 'uint256' },
{ internalType: 'uint256', name: 'quantityAvailableToMint', type: 'uint256' },
{ internalType: 'address payable', name: 'creatorPaymentAddress', type: 'address' },
{ internalType: 'uint256', name: 'generalAvailabilityStartTime', type: 'uint256' },
{ internalType: 'uint256', name: 'mintEndTime', type: 'uint256' },
{ internalType: 'uint256', name: 'creatorRevenuePerQuantity', type: 'uint256' },
{ internalType: 'uint256', name: 'referrerRewardPerQuantity', type: 'uint256' },
{ internalType: 'uint256', name: 'worldCuratorRevenuePerQuantity', type: 'uint256' },
{ internalType: 'uint256', name: 'protocolFeePerQuantity', type: 'uint256' },
],
internalType: 'struct MultiTokenDropMarketFixedPriceSale.GetFixedPriceSaleResults',
name: 'results',
type: 'tuple',
},
],
stateMutability: 'view',
type: 'function',
},
{
inputs: [
{ internalType: 'uint256', name: 'saleTermsId', type: 'uint256' },
{ internalType: 'uint256', name: 'tokenQuantity', type: 'uint256' },
{ internalType: 'address', name: 'tokenRecipient', type: 'address' },
{ internalType: 'address payable', name: 'referrer', type: 'address' },
],
name: 'mintFromFixedPriceSale',
outputs: [],
stateMutability: 'payable',
type: 'function',
},
];
108 changes: 108 additions & 0 deletions src/ingestors/rodeo/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
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 { getRodeoMintByAddressAndChain, getRodeoMintByURL, rodeoSupports } from './offchain-metadata';

import { BigNumber } from 'alchemy-sdk';
import { MintTemplateBuilder } from '../../lib/builder/mint-template-builder';
import { RODEO_ABI } from './abi';
import { getRodeoFeeInEth } from './onchain-metadata';

export class RodeoIngestor implements MintIngestor {
configuration = {
supportsContractIsExpensive: true,
};

async supportsUrl(resources: MintIngestorResources, url: string): Promise<boolean> {
if (new URL(url).hostname !== 'rodeo.club') {
return false;
}
try {
const { chainId, contractAddress, tokenId } = await getRodeoMintByURL(resources, url);
return !!chainId && !!contractAddress && !!tokenId;
} catch (error) {
return false;
}
}

async supportsContract(resources: MintIngestorResources, contract: MintContractOptions): Promise<boolean> {
const { chainId, contractAddress, tokenId } = contract;
if (!chainId || !contractAddress || !tokenId) {
return false;
}
return await rodeoSupports(contract, resources);
}

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

const { chainId, contractAddress, tokenId } = await getRodeoMintByURL(resources, url);

if (!chainId || !contractAddress || !tokenId) {
throw new MintIngestorError(MintIngestionErrorName.MissingRequiredData, 'Missing required data');
}

return this.createMintForContract(resources, { chainId, contractAddress, url, tokenId });
}

async createMintForContract(resources: MintIngestorResources, contract: MintContractOptions): Promise<MintTemplate> {
const { chainId, contractAddress, tokenId } = contract;
if (!chainId || !contractAddress || !tokenId) {
throw new MintIngestorError(MintIngestionErrorName.MissingRequiredData, 'Missing required data');
}

const mintBuilder = new MintTemplateBuilder()
.setMintInstructionType(MintInstructionType.EVM_MINT)
.setPartnerName('RodeoClub');

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

// asides name and image no other metadata we need is onchain so we can just use the offchain metadata
const { name, image, description, mintAddress, public_sale_start_at, public_sale_end_at, sale_terms_id, user } =
await getRodeoMintByAddressAndChain(
resources,
contract.chainId,
contract.contractAddress,
contract.tokenId as string,
);

mintBuilder.setName(name).setDescription(description).setFeaturedImageUrl(image);
const totalPrice = await getRodeoFeeInEth(sale_terms_id, user.address, mintAddress, resources.alchemy);

mintBuilder.setMintOutputContract({
chainId,
address: contractAddress,
});

mintBuilder.setCreator({
name: user.name,
imageUrl: user.image,
});

mintBuilder.setMintInstructions({
chainId,
contractAddress: mintAddress,
contractMethod: 'mintFromFixedPriceSale',
contractParams: `[${sale_terms_id}, 1, address, "${user.address}"]`,
abi: RODEO_ABI,
priceWei: totalPrice,
});

const startDate = public_sale_start_at ? new Date(public_sale_start_at) : new Date();
const endDate = public_sale_end_at ? new Date(public_sale_end_at) : null;

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;
}
}
197 changes: 197 additions & 0 deletions src/ingestors/rodeo/offchain-metadata.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
import { AxiosInstance, AxiosResponse } from 'axios';
import { MintContractOptions, MintIngestionErrorName, MintIngestorError, MintIngestorResources } from '../../lib';

/**
* @param resources MintIngestorResources
* @param url string ie 'https://rodeo.club/post/0x98E9116a26E1cf014770122b2f5b7EE4Cad067bA/1?utm_source=twitter&utm_medium=tweet&utm_campaign=hot_ones'
* @returns { chainId: number, contractAddress: string, image: string , name: string}
* @throws MintIngestorError
*/
export const getRodeoMintByURL = async (
resources: MintIngestorResources,
url: string,
): Promise<{
chainId: number;
contractAddress: string;
image: string;
name: string;
mintAddress: string;
description: string;
tokenId: string;
}> => {
const urlParts = new URL(url);
const contractAddress = urlParts.pathname.split('/')[2];
const tokenId = urlParts.pathname.split('/')[3];
if (!contractAddress || contractAddress.length !== 42) {
throw new MintIngestorError(MintIngestionErrorName.CouldNotResolveMint, 'Mint not found');
}
if (!tokenId || tokenId.length === 0) {
throw new MintIngestorError(MintIngestionErrorName.CouldNotResolveMint, 'Token not found');
}
return await getRodeoMintByAddressAndChain(resources, 8453, contractAddress, tokenId);
};

export const getRodeoMintByAddressAndChain = async (
resources: MintIngestorResources,
chainId: number,
contractAddress: string,
tokenId: string,
) => {
let response: AxiosResponse;
try {
const url = 'https://api-v2.foundation.app/electric/v2/graphql';

const headers = {
Accept: '*/*',
};

const data = {
query: `
query ShopPage($tokenFilter: TokenInput!, $page: Int, $perPage: Limit) {
token(by: {token: $tokenFilter}, filters: {existenceStatus: ANY}) {
...Token
isDeleted
}
tokenHolders(by: {token: $tokenFilter}, page: $page, perPage: $perPage) {
tokenHolderBalances {
items {
...TokenHolder
}
page
totalItems
totalPages
}
firstMinter {
...UserWallet
}
}
}
fragment Token on ERC1155Token {
chainId
contractAddress
name
description
mintedCount
uniqueMintersCount
commentCount
tokenId
saleConfiguration {
...SaleConfiguration
}
creator {
...UserWallet
}
media {
...Media
}
}
fragment SaleConfiguration on TokenTimedSaleConfiguration {
... on TokenTimedSaleConfiguration {
saleType
status
startTime
endTime
mintPrice
saleTermsId
}
}
fragment UserWallet on UserWallet {
user {
...User
}
wallet {
address
}
}
fragment User on User {
id
displayName
username
imageUrl
}
fragment Media on Media {
__typename
... on ImageMedia {
processingStatus
sourceUrl
url
width
height
blurHash
imageMimeType: mimeType
}
... on VideoMedia {
previewUrl
processingStatus
sourceUrl
staticUrl
url
width
height
videoMimeType: mimeType
}
}
fragment TokenHolder on TokenHolderBalance {
count: tokenCount
holder {
...UserWallet
}
}
`,
variables: {
tokenFilter: {
chainId,
contractAddress,
tokenId: parseInt(tokenId),
},
},
operationName: 'ShopPage',
};

response = await resources.fetcher.post(url, data, { headers });
} catch (error) {
throw new MintIngestorError(MintIngestionErrorName.CouldNotResolveMint, 'Could not query mint from Transient API');
}
const data = response.data.data;
if (!data || !data.token) {
throw new MintIngestorError(MintIngestionErrorName.CouldNotResolveMint, 'Project not found');
}

return {
chainId: data.token.chainId,
contractAddress: data.token.contractAddress,
image: data.token.media.url,
name: data.token.name,
mintAddress: '0x132363a3bbf47E06CF642dd18E9173E364546C99',
description: data.token.description,
public_sale_start_at: data.token.saleConfiguration.startTime,
public_sale_end_at: data.token.saleConfiguration.endTime,
tokenId: data.token.tokenId,
sale_terms_id: data.token.saleConfiguration.saleTermsId as number,
user: {
name: data.token.creator.user.displayName,
image: data.token.creator.user.imageUrl,
address: data.token.creator.wallet.address,
},
};
};

export const rodeoSupports = async (
contract: MintContractOptions,
resources: MintIngestorResources,
): Promise<boolean> => {
const { chainId, contractAddress, tokenId } = contract;
const exists = await getRodeoMintByAddressAndChain(resources, chainId, contractAddress, tokenId as string);
return !!exists;
};
40 changes: 40 additions & 0 deletions src/ingestors/rodeo/onchain-metadata.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { Alchemy, Contract } from 'alchemy-sdk';

import { BigNumber } from 'alchemy-sdk';
import { RODEO_ABI } from './abi';

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

export const getRodeoFeeInEth = async (
salesTermId: number,
referrer: string,
mintContractAddress: string,
alchemy: Alchemy,
): Promise<string> => {
const contract = await getContract(mintContractAddress, alchemy);
const feeConfig = await contract.functions.getFixedPriceSale(salesTermId, referrer);

// Destructure the results from the feeConfig
// We're using the structure from the ABI you provided earlier
const [results] = feeConfig;
const {
creatorRevenuePerQuantity,
referrerRewardPerQuantity,
worldCuratorRevenuePerQuantity,
protocolFeePerQuantity,
pricePerQuantity,
} = results;

// Sum up all the fees
const totalFee = BigNumber.from(creatorRevenuePerQuantity)
.add(referrerRewardPerQuantity)
.add(worldCuratorRevenuePerQuantity)
.add(protocolFeePerQuantity)
.add(pricePerQuantity);

return totalFee.toString();
};
Loading

0 comments on commit b3e3bd4

Please sign in to comment.