diff --git a/src/Classes/APIBots/OpenSeaEventsPollBot.ts b/src/Classes/APIBots/OpenSeaEventsPollBot.ts index 362f135..4337ea5 100644 --- a/src/Classes/APIBots/OpenSeaEventsPollBot.ts +++ b/src/Classes/APIBots/OpenSeaEventsPollBot.ts @@ -349,8 +349,10 @@ export class OpenSeaEventsPollBot extends APIPollBot { const seller = saleEvent.seller || '' const buyer = saleEvent.buyer || '' + // API poll bot currently only queries Ethereum mainnet contracts const normalized: NormalizedOpenSeaSale = { source: 'api', + chainId: 1, osAssetId: `ethereum/${contractAddress}/${tokenId}`, contractAddress, tokenId, diff --git a/src/Classes/APIBots/OpenSeaListBot.ts b/src/Classes/APIBots/OpenSeaListBot.ts index 88aaa9c..f9d2f65 100644 --- a/src/Classes/APIBots/OpenSeaListBot.ts +++ b/src/Classes/APIBots/OpenSeaListBot.ts @@ -107,11 +107,12 @@ export class OpenSeaListBot { try { // Get Art Blocks metadata response for the item (same as ReservoirListBot) - const tokenApiUrl = getTokenApiUrl(contractAddress, tokenId) + const tokenApiUrl = getTokenApiUrl(nftInfo.chainId, contractAddress, tokenId) const artBlocksResponse = await axios.get(tokenApiUrl) const artBlocksData = artBlocksResponse?.data as ArtBlocksTokenData const tokenUrl = getTokenUrl( artBlocksData.external_url ?? '', + nftInfo.chainId, contractAddress, tokenId ) diff --git a/src/Classes/APIBots/OpenSeaSaleBot.ts b/src/Classes/APIBots/OpenSeaSaleBot.ts index 6185d9f..3d45b62 100644 --- a/src/Classes/APIBots/OpenSeaSaleBot.ts +++ b/src/Classes/APIBots/OpenSeaSaleBot.ts @@ -29,6 +29,7 @@ import { artIndexerBot } from '../..' export interface NormalizedOpenSeaSale { source: 'stream' | 'api' + chainId: number osAssetId: string contractAddress: string tokenId: string @@ -95,6 +96,7 @@ export class OpenSeaSaleBot { ) const normalizedSale: NormalizedOpenSeaSale = { source: 'stream', + chainId: nftInfo.chainId, osAssetId: event.payload.item.nft_id.toLowerCase(), contractAddress: nftInfo.contractAddress, tokenId: nftInfo.tokenId, @@ -183,19 +185,20 @@ export class OpenSeaSaleBot { try { // Get Art Blocks metadata response for the item (same as ReservoirSaleBot) - const tokenApiUrl = getTokenApiUrl(contractAddress, tokenId) + const tokenApiUrl = getTokenApiUrl(sale.chainId, contractAddress, tokenId) const artBlocksResponse = await axios.get(tokenApiUrl) const artBlocksData = artBlocksResponse?.data as ArtBlocksTokenData const tokenUrl = getTokenUrl( artBlocksData.external_url ?? '', + sale.chainId, contractAddress, tokenId ) let sellerText = await ensOrAddress(seller) let buyerText = await ensOrAddress(buyer) - const platformUrl = buildArtBlocksTokenURL(contractAddress, tokenId) + const platformUrl = buildArtBlocksTokenURL(sale.chainId, contractAddress, tokenId) // Add OpenSea usernames if available (same logic as ReservoirSaleBot) if (!sellerText.includes('.eth')) { diff --git a/src/Classes/APIBots/utils.ts b/src/Classes/APIBots/utils.ts index fb9b111..01c49a6 100644 --- a/src/Classes/APIBots/utils.ts +++ b/src/Classes/APIBots/utils.ts @@ -1,7 +1,6 @@ import * as dotenv from 'dotenv' import { - ARBITRUM_CONTRACTS, COLLAB_CONTRACTS, ENGINE_CONTRACTS, STUDIO_CONTRACTS, @@ -28,7 +27,6 @@ const publicClient = createPublicClient({ ]), }) -const STAGING_CONTRACTS = require('../../ProjectConfig/stagingContracts.json') const EXPLORATIONS_CONTRACTS = require('../../ProjectConfig/explorationsContracts.json') const CORE_CONTRACTS = require('../../ProjectConfig/coreContracts.json') @@ -260,22 +258,12 @@ export function getVerticalName(msg: string): string { } export function getTokenApiUrl( + chainId: number, contractAddress: string, tokenId: string ): string { contractAddress = contractAddress.toLowerCase() - if ( - Object.values(CORE_CONTRACTS).includes(contractAddress) || - contractAddress === '' - ) { - return `https://token.artblocks.io/${tokenId}` - } else if (Object.values(STAGING_CONTRACTS).includes(contractAddress)) { - return `https://token.staging.artblocks.io/${contractAddress}/${tokenId}` - } else if (isArbitrumContract(contractAddress)) { - return `https://token.arbitrum.artblocks.io/${contractAddress}/${tokenId}` - } else { - return `https://token.artblocks.io/${contractAddress}/${tokenId}` - } + return `https://token.artblocks.io/${chainId}/${contractAddress}/${tokenId}` } export function isExplorationsContract(contractAddress: string): boolean { @@ -292,10 +280,6 @@ export function isEngineContract(contractAddress: string): boolean { return ENGINE_CONTRACTS.includes(contractAddress.toLowerCase()) } -export function isArbitrumContract(contractAddress: string): boolean { - return ARBITRUM_CONTRACTS.includes(contractAddress.toLowerCase()) -} - export function isCoreContract(contractAddress: string): boolean { return Object.values(CORE_CONTRACTS).includes(contractAddress.toLowerCase()) } @@ -324,28 +308,50 @@ export async function getCollectionType( export function getTokenUrl( external_url: string, + chainId: number, contractAddr: string, tokenId: string ): string { if (external_url && !external_url.includes('generator.artblocks.io')) { return external_url } - return buildArtBlocksTokenURL(contractAddr, tokenId) + return buildArtBlocksTokenURL(chainId, contractAddr, tokenId) } export function getProjectSlugUrl(slug: string): string { return `https://www.artblocks.io/collection/${slug}` } -export function getProjectUrl(contractAddr: string, projectId: string): string { - return `https://www.artblocks.io/collection/${contractAddr}-${projectId}` -} - export function buildArtBlocksTokenURL( + chainId: number, contractAddr: string, tokenId: string ): string { - return `https://www.artblocks.io/token/${contractAddr}/${tokenId}` + return `https://www.artblocks.io/token/${chainId}/${contractAddr}/${tokenId}` +} + +/** + * Build generator (live script) URL from chain + contract + token. + */ +export function buildGeneratorUrl( + chainId: number, + contractAddress: string, + tokenId: string +): string { + contractAddress = contractAddress.toLowerCase() + return `https://generator.artblocks.io/${chainId}/${contractAddress}/${tokenId}` +} + +/** + * Build media/preview image URL from chain + contract + token. + */ +export function buildMediaUrl( + chainId: number, + contractAddress: string, + tokenId: string +): string { + contractAddress = contractAddress.toLowerCase() + return `https://media-proxy.artblocks.io/${chainId}/${contractAddress}/${tokenId}.png` } export function buildOpenseaURL(contractAddr: string, tokenId: string): string { @@ -439,16 +445,29 @@ export const CLEANUP_INTERVAL_MS = 60 * 60 * 1000 * @param nftId - Format: "ethereum/0x2308742aa28cc460522ff855d24a365f99deba7b/7111" * @returns {contractAddress, tokenId} or null if invalid format */ +const CHAIN_NAME_TO_ID: Record = { + ethereum: 1, + arbitrum: 42161, + base: 8453, +} + export function parseNftId( nftId: string -): { contractAddress: string; tokenId: string } | null { +): { chainId: number; contractAddress: string; tokenId: string } | null { const parts = nftId.split('/') - if (parts.length !== 3 || parts[0] !== 'ethereum') { + if (parts.length !== 3) { console.warn('Invalid NFT ID format:', nftId) return null } + const chainId = CHAIN_NAME_TO_ID[parts[0]] + if (chainId === undefined) { + console.warn('Unknown chain in NFT ID:', parts[0], nftId) + return null + } + return { + chainId, contractAddress: parts[1].toLowerCase(), tokenId: parts[2], } diff --git a/src/Classes/ArtIndexerBot.ts b/src/Classes/ArtIndexerBot.ts index 927fb49..19753e1 100644 --- a/src/Classes/ArtIndexerBot.ts +++ b/src/Classes/ArtIndexerBot.ts @@ -28,7 +28,6 @@ import { getVerticalName, isVerticalName, resolveEnsName, - getProjectUrl, getProjectSlugUrl, } from './APIBots/utils' import { randomColor } from '../Utils/smartBotResponse' @@ -138,19 +137,11 @@ export class ArtIndexerBot { async buildContracts() { try { - const arbContractsArr = await getAllContracts(true) - for (let i = 0; i < arbContractsArr.length; i++) { - const name = arbContractsArr[i].name + const contractsArr = await getAllContracts() + for (let i = 0; i < contractsArr.length; i++) { + const name = contractsArr[i].name if (typeof name === 'string') { - this.contracts[name.toLowerCase()] = arbContractsArr[i] - } - } - - const ethContractsArr = await getAllContracts(false) - for (let i = 0; i < ethContractsArr.length; i++) { - const name = ethContractsArr[i].name - if (typeof name === 'string') { - this.contracts[name.toLowerCase()] = ethContractsArr[i] + this.contracts[name.toLowerCase()] = contractsArr[i] } } } catch (e) { @@ -199,8 +190,10 @@ export class ArtIndexerBot { ) const newBot = new ProjectBot({ id: project.id, + chainId: project.chain_id, projectNumber: parseInt(project.project_id), coreContract: project.contract_address, + slug: project.slug, editionSize: project.invocations, maxEditionSize: project.max_invocations, projectName: project.name ?? 'unknown', @@ -997,22 +990,16 @@ export class ArtIndexerBot { const projectPrices: { price: number name: string - contractAddress: string - projectId: string - slug?: string + slug: string }[] = [] const projectsWithoutListings: { name: string - contractAddress: string - projectId: string - slug?: string + slug: string }[] = [] for (const bucket of validBuckets) { if (bucket.project) { const projectName = bucket.project.name || 'Unknown Project' - const contractAddress = bucket.project.contract_address - const projectId = bucket.project.project_id const slug = bucket.project.slug if (bucket.project.lowest_listing) { @@ -1022,16 +1009,11 @@ export class ArtIndexerBot { projectPrices.push({ price, name: projectName, - contractAddress, - projectId, slug, }) } else { - // Project exists but has no listings projectsWithoutListings.push({ name: projectName, - contractAddress, - projectId, slug, }) } @@ -1109,23 +1091,19 @@ export class ArtIndexerBot { // Top 3 cheapest (already sorted ascending) const top3Cheapest = sortedProjectPrices.slice(0, 3) const cheapestText = top3Cheapest - .map((p) => { - const projectUrl = p.slug - ? getProjectSlugUrl(p.slug) - : getProjectUrl(p.contractAddress, p.projectId) - return `— [${p.name}](${projectUrl}) • ${formatPrice(p.price)} Ξ` - }) + .map( + (p) => + `— [${p.name}](${getProjectSlugUrl(p.slug)}) • ${formatPrice(p.price)} Ξ` + ) .join('\n') // Top 3 priciest (reverse order - most expensive first) const top3Priciest = sortedProjectPrices.slice(-3).reverse() const priciestText = top3Priciest - .map((p) => { - const projectUrl = p.slug - ? getProjectSlugUrl(p.slug) - : getProjectUrl(p.contractAddress, p.projectId) - return `— [${p.name}](${projectUrl}) • ${formatPrice(p.price)} Ξ` - }) + .map( + (p) => + `— [${p.name}](${getProjectSlugUrl(p.slug)}) • ${formatPrice(p.price)} Ξ` + ) .join('\n') embedContent.addFields({ @@ -1144,12 +1122,7 @@ export class ArtIndexerBot { const firstFive = projectsWithoutListings.slice(0, 5) const noListingsText = firstFive - .map((p) => { - const projectUrl = p.slug - ? getProjectSlugUrl(p.slug) - : getProjectUrl(p.contractAddress, p.projectId) - return `[${p.name}](${projectUrl})` - }) + .map((p) => `[${p.name}](${getProjectSlugUrl(p.slug)})`) .join(' • ') + (projectsWithoutListings.length > 5 ? ` and ${projectsWithoutListings.length - 5} more` diff --git a/src/Classes/MintBot.ts b/src/Classes/MintBot.ts index 2ae64e5..79211b1 100644 --- a/src/Classes/MintBot.ts +++ b/src/Classes/MintBot.ts @@ -3,9 +3,10 @@ import { artIndexerBot, mintBot, projectConfig } from '../index' import axios from 'axios' import { MINT_UTM, + buildArtBlocksTokenURL, + buildGeneratorUrl, + buildMediaUrl, getCollectionType, - getTokenApiUrl, - getTokenUrl, replaceToPNG, waitForEngineContracts, waitForStudioContracts, @@ -13,6 +14,12 @@ import { import { ensOrAddress } from './APIBots/utils' import { TwitterBot } from './TwitterBot' +// When true, only post mints from mainnet (chain ID 1). Non-mainnet mints are skipped. +// Set to false in a couple weeks to enable L2/other chain mints. +const HIDE_NON_MAINNET_MINTS = true + +const MAINNET_CHAIN_ID = 1 + const MINT_CONFIG: { [id: string]: string[] } = require('../ProjectConfig/mintBotConfig.json') @@ -95,9 +102,8 @@ export class MintBot { this.contractToChannel = contractToChannel } - // Go through all mints in the queue and make sure the image exists - // If it does, report to discord and remove from the queue - // If it doesn't, add it back in the queue to check again later + // Go through all mints in the queue and make sure the media image exists. + // Uses event data only (no token API call). Polls media URL until available, then posts. async checkAndPostMints() { if (Object.keys(this.mintsToPost).length === 0) { return @@ -107,17 +113,9 @@ export class MintBot { await Promise.all( Object.entries(this.mintsToPost).map(async ([id, mint]) => { try { - const tokenUrl = getTokenApiUrl(mint.contractAddress, mint.tokenId) - const artBlocksResponse = await axios.get(tokenUrl) - - const artBlocksData = artBlocksResponse.data - let assetUrl = artBlocksData?.preview_asset_url - if (!assetUrl) { - console.log(`No preview asset URL for mint ${id}`) - return - } - - assetUrl = replaceToPNG(assetUrl) + const assetUrl = replaceToPNG( + buildMediaUrl(mint.chainId, mint.contractAddress, mint.tokenId) + ) // Validate URL format try { @@ -143,14 +141,18 @@ export class MintBot { return } - // If we get here, the image is valid + // If we get here, the image is valid - build embed from event data delete this.mintsToPost[id] mint.image = assetUrl - mint.generatorLink = artBlocksData.generator_url - mint.tokenName = artBlocksData.name - mint.artistName = artBlocksData.artist - mint.artblocksUrl = getTokenUrl( - artBlocksData.external_url, + mint.generatorLink = buildGeneratorUrl( + mint.chainId, + mint.contractAddress, + mint.tokenId + ) + mint.tokenName = `${mint.projectName} #${mint.invocation}` + mint.artistName = '' // Not in mint event; omit from display + mint.artblocksUrl = buildArtBlocksTokenURL( + mint.chainId, mint.contractAddress, mint.tokenId ) @@ -178,9 +180,18 @@ export class MintBot { tokenID: string, owner: string, invocation: string, - projectId: string + projectId: string, + projectName: string, + chainId: number ) { - console.log('NEW MINT', contractAddress, tokenID, owner) + console.log( + 'NEW MINT', + contractAddress, + tokenID, + owner, + 'chainId:', + chainId + ) const id = `${contractAddress}-${tokenID}` if (!this.contractToChannel[contractAddress]) { @@ -188,9 +199,24 @@ export class MintBot { return } + if (HIDE_NON_MAINNET_MINTS && chainId !== MAINNET_CHAIN_ID) { + console.log( + `Skipping mint for non-mainnet chain ${chainId} (HIDE_NON_MAINNET_MINTS=true)` + ) + return + } + artIndexerBot.checkMintedOut(projectId, invocation) - this.mintsToPost[id] = new Mint(this.bot, contractAddress, tokenID, owner) + this.mintsToPost[id] = new Mint( + this.bot, + contractAddress, + tokenID, + owner, + chainId, + projectName, + invocation + ) } // Routine that runs every MINT_REFRESH_TIME_SECONDS seconds and @@ -236,6 +262,9 @@ export class Mint { contractAddress: string tokenId: string owner: string + chainId: number + projectName: string + invocation: string image: string generatorLink: string tokenName: string @@ -245,12 +274,18 @@ export class Mint { bot: Client, contractAddress: string, tokenId: string, - owner: string + owner: string, + chainId: number, + projectName: string, + invocation: string ) { this.bot = bot this.contractAddress = contractAddress this.tokenId = tokenId this.owner = owner + this.chainId = chainId + this.projectName = projectName + this.invocation = invocation this.image = '' this.generatorLink = '' this.tokenName = '' @@ -266,7 +301,7 @@ export class Mint { const baseABProfile = 'https://www.artblocks.io/user/' const ownerProfile = baseABProfile + this.owner + MINT_UTM - embed.setTitle(`Minted: ${this.tokenName} - ${this.artistName}`) + embed.setTitle(`Minted: ${this.tokenName}`) if (this.artblocksUrl) { embed.setURL(this.artblocksUrl + MINT_UTM) } diff --git a/src/Classes/ProjectBot.ts b/src/Classes/ProjectBot.ts index affb140..81e4135 100644 --- a/src/Classes/ProjectBot.ts +++ b/src/Classes/ProjectBot.ts @@ -11,7 +11,7 @@ import { PROJECTBOT_EXPLORE_UTM, PROJECTBOT_UTM, ethFromWeiString, - getProjectUrl, + getProjectSlugUrl, getTokenApiUrl, getTokenUrl, } from './APIBots/utils' @@ -35,8 +35,10 @@ const ONE_MILLION = 1e6 */ export class ProjectBot { id: string + chainId: number projectNumber: number coreContract: string + slug: string editionSize: number maxEditionSize: number projectName: string @@ -50,8 +52,10 @@ export class ProjectBot { constructor({ id, + chainId, projectNumber, coreContract, + slug, editionSize, maxEditionSize, projectName, @@ -63,8 +67,10 @@ export class ProjectBot { description, }: { id: string + chainId: number projectNumber: number coreContract: string + slug: string editionSize: number maxEditionSize: number projectName: string @@ -76,8 +82,10 @@ export class ProjectBot { description?: string }) { this.id = id + this.chainId = chainId this.projectNumber = projectNumber this.coreContract = coreContract + this.slug = slug this.editionSize = editionSize this.maxEditionSize = maxEditionSize this.projectName = projectName @@ -277,7 +285,7 @@ export class ProjectBot { if (tokenMetadata.contract?.name === 'Art Blocks x Pace') { external_url = '' } - const tokenUrl = getTokenUrl(external_url, this.coreContract, tokenID) + const tokenUrl = getTokenUrl(external_url, this.chainId, this.coreContract, tokenID) const titleLink = tokenUrl + PROJECTBOT_UTM let title = `${tokenMetadata.project.name} #${tokenMetadata.invocation} - ${tokenMetadata.project.artist_name}` @@ -354,7 +362,11 @@ export class ProjectBot { console.log('sending birthday message(s) for:', this.projectName) const artBlocksResponse = await axios.get( - getTokenApiUrl(this.coreContract, `${this.projectNumber * ONE_MILLION}`) + getTokenApiUrl( + this.chainId, + this.coreContract, + `${this.projectNumber * ONE_MILLION}` + ) ) const artBlocksData = await artBlocksResponse.data let assetUrl = artBlocksData?.preview_asset_url @@ -382,8 +394,7 @@ export class ProjectBot { What are your favorite outputs from ${this.projectName}? [Explore the full project here](${ - getProjectUrl(this.coreContract, this.projectNumber.toString()) + - PROJECTBOT_UTM + getProjectSlugUrl(this.slug) + PROJECTBOT_UTM }) ` ) @@ -410,7 +421,11 @@ export class ProjectBot { ) as TextChannel const artBlocksResponse = await axios.get( - getTokenApiUrl(this.coreContract, `${this.projectNumber * ONE_MILLION}`) + getTokenApiUrl( + this.chainId, + this.coreContract, + `${this.projectNumber * ONE_MILLION}` + ) ) const artBlocksData = await artBlocksResponse.data const assetUrl = artBlocksData?.preview_asset_url @@ -418,8 +433,7 @@ export class ProjectBot { // Send congratulations message const title = `:tada: ${this.projectName} has minted out! Congratulations ${this.artistName}! :tada:` const description = `Check out the whole collection [here](${ - getProjectUrl(this.coreContract, this.projectNumber.toString()) + - PROJECTBOT_UTM + getProjectSlugUrl(this.slug) + PROJECTBOT_UTM })` const embedContent = new EmbedBuilder() .setColor('#9370DB') @@ -442,10 +456,7 @@ export class ProjectBot { upcomingDetails.auction_start_time || upcomingDetails.start_datetime ) - const projectUrl = getProjectUrl( - this.coreContract, - this.projectNumber.toString() - ) + const projectUrl = getProjectSlugUrl(this.slug) const title = `${upcomingDetails.name} by ${upcomingDetails.artist_name}` const assetUrl = await replaceVideoWithGIF( @@ -528,10 +539,7 @@ export class ProjectBot { if (!msg.channel.isSendable()) { return } - const projectUrl = getProjectUrl( - this.coreContract, - this.projectNumber.toString() - ) + const projectUrl = getProjectSlugUrl(this.slug) const titleLink = projectUrl + PROJECTBOT_EXPLORE_UTM const title = `${this.projectName} by ${this.artistName}` diff --git a/src/Classes/TriviaBot.ts b/src/Classes/TriviaBot.ts index a08a648..54090c1 100644 --- a/src/Classes/TriviaBot.ts +++ b/src/Classes/TriviaBot.ts @@ -181,7 +181,7 @@ Next question:` project.projectNumber * 1e6 const artBlocksResponse = await axios.get( - getTokenApiUrl(project.coreContract, `${tokenNumber}`) + getTokenApiUrl(project.chainId, project.coreContract, `${tokenNumber}`) ) const artBlocksData = artBlocksResponse.data const assetUrl = await replaceVideoWithGIF(artBlocksData.preview_asset_url) @@ -202,7 +202,7 @@ Next question:` project.projectNumber * 1e6 const artBlocksResponse = await axios.get( - getTokenApiUrl(project.coreContract, `${tokenNumber}`) + getTokenApiUrl(project.chainId, project.coreContract, `${tokenNumber}`) ) const artBlocksData = artBlocksResponse.data const assetUrl = await replaceVideoWithGIF(artBlocksData.preview_asset_url) diff --git a/src/Classes/TwitterBot.ts b/src/Classes/TwitterBot.ts index 1541d3d..a9a28cc 100644 --- a/src/Classes/TwitterBot.ts +++ b/src/Classes/TwitterBot.ts @@ -247,7 +247,7 @@ export class TwitterBot { } const { data: artBlocksData } = await axios.get( - getTokenApiUrl(projectBot.coreContract, tokenId) + getTokenApiUrl(projectBot.chainId, projectBot.coreContract, tokenId) ) const assetUrl = artBlocksData.preview_asset_url @@ -263,6 +263,7 @@ export class TwitterBot { const tokenUrl = getTokenUrl( artBlocksData.external_url, + projectBot.chainId, projectBot.coreContract, tokenId ) + TWITTER_PROJECTBOT_UTM diff --git a/src/Data/graphql/artbot-hasura-queries.graphql b/src/Data/graphql/artbot-hasura-queries.graphql index de05ee6..7bf081b 100644 --- a/src/Data/graphql/artbot-hasura-queries.graphql +++ b/src/Data/graphql/artbot-hasura-queries.graphql @@ -1,6 +1,8 @@ fragment ProjectDetail on projects_metadata { id project_id + slug + chain_id name description invocations @@ -21,11 +23,15 @@ fragment ProjectDetail on projects_metadata { } contract { name + partner { + name + } } } fragment TokenDetail on tokens_metadata { invocation + chain_id project { name artist_name @@ -48,11 +54,16 @@ fragment ProjectTokenDetail on tokens_metadata { id project_id invocation + chain_id } fragment ContractDetail on contracts_metadata { address name + chain_id + partner { + name + } } query getAllProjects($first: Int!, $skip: Int, $blocked_addresses: [String!]) { diff --git a/src/Data/queryGraphQL.ts b/src/Data/queryGraphQL.ts index 597f22d..d4cd052 100644 --- a/src/Data/queryGraphQL.ts +++ b/src/Data/queryGraphQL.ts @@ -1,5 +1,5 @@ import * as dotenv from 'dotenv' -import { Client, createClient } from 'urql/core' +import { createClient } from 'urql/core' import * as fs from 'fs' import * as path from 'path' import { sanitizeTwitterHandle } from '../Utils/twitterUtils' @@ -35,18 +35,13 @@ import { SetDataFragment, GetLowestPricedTokenByProjectDocument, } from '../../generated/graphql' -import { - isArbitrumContract, - waitForStudioContracts, -} from '../Classes/APIBots/utils' -import { ARBITRUM_CONTRACTS, ENGINE_CONTRACTS } from '..' +import { waitForStudioContracts } from '../Classes/APIBots/utils' +import { ENGINE_CONTRACTS } from '..' dotenv.config() const fetch = require('node-fetch') export const PUBLIC_HASURA_ENDPOINT = 'https://data.artblocks.io/v1/graphql' -export const PUBLIC_ARB_HASURA_ENDPOINT = - 'https://ab-prod-arbitrum.hasura.app/v1/graphql' const CORE_CONTRACTS: { [id: string]: string } = require('../ProjectConfig/coreContracts.json') @@ -86,29 +81,14 @@ const client = createClient({ requestPolicy: 'network-only', }) -const arbitrumClient = createClient({ - url: PUBLIC_ARB_HASURA_ENDPOINT, - fetch: fetch, - requestPolicy: 'network-only', -}) - -const getClientForContract = (contract: string) => { - if (isArbitrumContract(contract)) { - return arbitrumClient - } - return client -} - const maxProjectsPerQuery = 1000 -export async function getAllProjectsForClient( - hasuraClient: Client -): Promise { +export async function getAllProjects(): Promise { try { const allProjects: ProjectDetailFragment[] = [] let loop = true while (loop) { - const { data } = await hasuraClient + const { data } = await client .query(GetAllProjectsDocument, { first: maxProjectsPerQuery, skip: allProjects.length, @@ -124,43 +104,12 @@ export async function getAllProjectsForClient( } } return allProjects - } catch (err) { - console.error('Error in getAllProjectsForClient:', err) - return [] - } -} - -export async function getAllProjects(): Promise { - try { - const mainnet = await getAllProjectsForClient(client) - const arb = await getAllProjectsForClient(arbitrumClient) - return mainnet.concat(arb) } catch (err) { console.error('Error in getAllProjects:', err) return [] } } -export async function getArbitrumContracts() { - const nonPBABContracts = getNonEngineContracts() - try { - const { data: arbData } = await arbitrumClient - .query(GetEngineContractsDocument, { - ids: nonPBABContracts, - }) - .toPromise() - - if (!arbData) { - throw Error('No data returned from getEngineContracts arbitrum query') - } - - return arbData.contracts_metadata.map(({ address }) => address) - } catch (err) { - console.error('Error in getArbitrumContracts:', err) - return undefined - } -} - export async function getStudioContracts() { try { const { data } = await client @@ -194,33 +143,28 @@ export async function getEngineContracts() { throw Error('No data returned from getEngineContracts Hasura query') } - const arbContracts = await getArbitrumContracts() - - const allContracts = data.contracts_metadata - .map(({ address }) => address) - .concat(arbContracts ?? []) - - return allContracts + return data.contracts_metadata.map(({ address }) => address) } catch (err) { console.error('Error in getEngineContracts:', err) return undefined } } -async function getAllWalletTokensClient( - hasuraClient: Client, - wallet: string, - contracts: string[] -) { +export async function getAllTokensInWallet(walletAddress: string) { + const addresses = Object.values(CORE_CONTRACTS) + .concat(Object.values(COLLAB_CONTRACTS)) + .concat(Object.values(EXPLORATION_CONTRACTS)) + .concat(Object.values(ENGINE_CONTRACTS)) + const maxTokensPerQuery = 1000 try { const allTokens: TokenDetailFragment[] = [] let loop = true while (loop) { - const { data } = await hasuraClient + const { data } = await client .query(GetWalletTokensDocument, { - wallet, - contracts, + wallet: walletAddress, + contracts: addresses, first: maxTokensPerQuery, skip: allTokens.length, }) @@ -236,36 +180,13 @@ async function getAllWalletTokensClient( return allTokens } catch (err) { console.error( - `Error in getAllWalletTokensClient for wallet ${wallet}:`, + `Error in getAllTokensInWallet for wallet ${walletAddress}:`, err ) return [] } } -export async function getAllTokensInWallet(walletAddress: string) { - const engineContracts = ENGINE_CONTRACTS - const arbContracts = ARBITRUM_CONTRACTS - const addresses = Object.values(CORE_CONTRACTS) - .concat(Object.values(COLLAB_CONTRACTS)) - .concat(Object.values(EXPLORATION_CONTRACTS)) - .concat(Object.values(engineContracts)) - .concat(Object.values(arbContracts)) - - const mainnetTokens = await getAllWalletTokensClient( - client, - walletAddress, - addresses - ) - const arbTokens = await getAllWalletTokensClient( - arbitrumClient, - walletAddress, - addresses - ) - - return mainnetTokens.concat(arbTokens) -} - export async function getArtblocksOpenProjects(): Promise< ProjectDetailFragment[] > { @@ -324,8 +245,7 @@ export async function getContractProject( projectId: number, contractAddress: string ): Promise { - const hasuraClient = getClientForContract(contractAddress) - const { data } = await hasuraClient + const { data } = await client .query(GetProjectDocument, { id: `${contractAddress}-${projectId}`, }) @@ -377,12 +297,11 @@ export async function getProject( async function getContractProjects( contractAddress: string ): Promise { - const hasuraClient = getClientForContract(contractAddress) const maxTokensPerQuery = 1000 const allProjects: ProjectDetailFragment[] = [] let loop = true while (loop) { - const { data } = await hasuraClient + const { data } = await client .query(GetContractProjectsDocument, { contract: contractAddress, first: maxTokensPerQuery, @@ -418,9 +337,8 @@ async function getContractsProjects( } export async function getProjectInvocations(projectId: string) { - const hasuraClient = getClientForContract(projectId.split('-')[0]) try { - const { data } = await hasuraClient + const { data } = await client .query(GetProjectInvocationsDocument, { id: projectId, }) @@ -439,9 +357,7 @@ export async function getProjectInvocations(projectId: string) { } export async function getToken(tokenId: string): Promise { - const hasuraClient = getClientForContract(tokenId.split('-')[0]) - - const { data, error } = await hasuraClient + const { data, error } = await client .query(GetTokenDocument, { token_id: tokenId, }) @@ -461,9 +377,7 @@ export async function getToken(tokenId: string): Promise { export async function getMostRecentMintedTokenByContracts( contracts: string[] ): Promise { - const isArb = contracts.length > 0 && isArbitrumContract(contracts[0]) - const c = isArb ? arbitrumClient : client - const { data } = await c + const { data } = await client .query(GetMostRecentMintedTokenByContractDocument, { contracts: contracts, }) @@ -491,11 +405,8 @@ export async function getMostRecentMintedFlagshipToken(): Promise { - const c = isArb ? arbitrumClient : client - const { data } = await c.query(GetAllContractsDocument, {}).toPromise() +export async function getAllContracts(): Promise { + const { data } = await client.query(GetAllContractsDocument, {}).toPromise() if (!data || !data.contracts_metadata.length) { throw Error('No data returned from getAllContracts Hasura query') @@ -702,9 +613,7 @@ export async function getSetByName( export async function getLowestPricedTokenByProject( projectId: string ): Promise<{ id: string; invocation: number } | null> { - const hasuraClient = getClientForContract(projectId.split('-')[0]) - - const { data } = await hasuraClient + const { data } = await client .query(GetLowestPricedTokenByProjectDocument, { project_id: projectId, }) diff --git a/src/index.ts b/src/index.ts index 04f9434..a9753fa 100644 --- a/src/index.ts +++ b/src/index.ts @@ -6,7 +6,6 @@ const bodyParser = require('body-parser') import { ProjectConfig } from './ProjectConfig/projectConfig' export let STUDIO_CONTRACTS: string[] = [] export let ENGINE_CONTRACTS: string[] = [] -export let ARBITRUM_CONTRACTS: string[] = [] export let ARTIST_TWITTER_HANDLES: Map = new Map() export const projectConfig = new ProjectConfig() @@ -14,7 +13,6 @@ import { ArtIndexerBot } from './Classes/ArtIndexerBot' import { MintBot } from './Classes/MintBot' import { - getArbitrumContracts, getArtBlocksXBMProjects, getArtBlocksXPaceProjects, getEngineContracts, @@ -59,9 +57,6 @@ getStudioContracts().then((contracts) => { getEngineContracts().then((contracts) => { ENGINE_CONTRACTS = contracts ?? [] }) -getArbitrumContracts().then((contracts) => { - ARBITRUM_CONTRACTS = contracts ?? [] -}) getArtistsTwitterHandles().then((handles) => { ARTIST_TWITTER_HANDLES = handles }) @@ -141,17 +136,19 @@ app.get( } ) -type MintEvent = { +// Hasura event trigger payload format (tokens_metadata_image_id_update) +type MintEventPayload = { event: { data: { new: { + chain_id: number contract_address: string owner_address: string project_name: string + project_id: string token_id: string + invocation: number minted_at: string - invocation: string - project_id: string } } } @@ -160,15 +157,14 @@ type MintEvent = { app.post( '/new-mint', function ( - req: { body: MintEvent; headers: Record }, + req: { body: MintEventPayload; headers: Record }, res: { setHeader: (key: string, value: string) => void json: (data: unknown) => void status: (code: number) => { json: (data: unknown) => void } } ) { - const mintEvent = req.body - const mintData = mintEvent.event.data.new + const mintData = req.body.event.data.new if (req.headers.webhook_secret !== process.env.MINT_WEBHOOK_SECRET) { console.log('Invalid mint webhook secret') @@ -178,10 +174,12 @@ app.post( mintBot.addMint( mintData.contract_address, - mintData.token_id, + String(mintData.token_id), mintData.owner_address, - mintData.invocation, - mintData.project_id + String(mintData.invocation), + mintData.project_id, + mintData.project_name, + mintData.chain_id ) res.setHeader('Content-Type', 'application/json') res.json({