Skip to content
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
2 changes: 2 additions & 0 deletions src/Classes/APIBots/OpenSeaEventsPollBot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
3 changes: 2 additions & 1 deletion src/Classes/APIBots/OpenSeaListBot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
Expand Down
7 changes: 5 additions & 2 deletions src/Classes/APIBots/OpenSeaSaleBot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import { artIndexerBot } from '../..'

export interface NormalizedOpenSeaSale {
source: 'stream' | 'api'
chainId: number
osAssetId: string
contractAddress: string
tokenId: string
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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')) {
Expand Down
71 changes: 45 additions & 26 deletions src/Classes/APIBots/utils.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import * as dotenv from 'dotenv'

import {
ARBITRUM_CONTRACTS,
COLLAB_CONTRACTS,
ENGINE_CONTRACTS,
STUDIO_CONTRACTS,
Expand All @@ -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')
Expand Down Expand Up @@ -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 {
Expand All @@ -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())
}
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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<string, number> = {
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],
}
Expand Down
61 changes: 17 additions & 44 deletions src/Classes/ArtIndexerBot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ import {
getVerticalName,
isVerticalName,
resolveEnsName,
getProjectUrl,
getProjectSlugUrl,
} from './APIBots/utils'
import { randomColor } from '../Utils/smartBotResponse'
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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',
Expand Down Expand Up @@ -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) {
Expand All @@ -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,
})
}
Expand Down Expand Up @@ -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({
Expand All @@ -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`
Expand Down
Loading