-
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.
Merge pull request #36 from Myestery/rodeo-ingestor
Rodeo ingestion
- Loading branch information
Showing
5 changed files
with
509 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
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', | ||
}, | ||
]; |
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,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; | ||
} | ||
} |
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,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; | ||
}; |
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,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(); | ||
}; |
Oops, something went wrong.