From 117ca0725ec3e9c6292ba9745cfea990f6c47140 Mon Sep 17 00:00:00 2001 From: Chris Maddern Date: Thu, 29 Aug 2024 11:05:05 -0400 Subject: [PATCH 1/2] Multi-chain Alchemy instance. Add ETH support for Manifold --- .../coinbase-wallet/onchain-metadata.ts | 24 +- src/ingestors/foundation/onchain-metadata.ts | 16 +- src/ingestors/highlight/onchain-metadata.ts | 14 +- src/ingestors/manifold/index.ts | 10 +- src/ingestors/manifold/offchain-metadata.ts | 59 ++-- src/ingestors/manifold/onchain-metadata.ts | 24 +- .../prohibition-daily/onchain-metadata.ts | 18 +- src/ingestors/rarible/offchain-metadata.ts | 212 +++++++-------- src/ingestors/rarible/onchain-metadata.ts | 256 +++++++++--------- src/ingestors/rodeo/onchain-metadata.ts | 10 +- .../transient-base/onchain-metadata.ts | 13 +- src/lib/resources.ts | 32 ++- src/lib/rpc/alchemy-multichain.ts | 64 +++++ src/lib/types/mint-ingestor.ts | 6 +- test/ingestors/manifold.test.ts | 2 +- 15 files changed, 437 insertions(+), 323 deletions(-) create mode 100644 src/lib/rpc/alchemy-multichain.ts diff --git a/src/ingestors/coinbase-wallet/onchain-metadata.ts b/src/ingestors/coinbase-wallet/onchain-metadata.ts index 618b926..529b0a8 100644 --- a/src/ingestors/coinbase-wallet/onchain-metadata.ts +++ b/src/ingestors/coinbase-wallet/onchain-metadata.ts @@ -1,9 +1,15 @@ -import { Alchemy, Contract } from 'alchemy-sdk'; +import { Contract } from 'alchemy-sdk'; import { MINT_CONTRACT_ABI } from './abi'; import { CollectionMetadata } from './types'; +import { AlchemyMultichainClient } from '../../../src/lib/rpc/alchemy-multichain'; +import { NETWORKS } from '../../../src/lib/simulation/simulation'; -const getContract = async (chainId: number, contractAddress: string, alchemy: Alchemy): Promise => { - const ethersProvider = await alchemy.config.getProvider(); +const getContract = async ( + chainId: number, + contractAddress: string, + alchemy: AlchemyMultichainClient, +): Promise => { + const ethersProvider = await alchemy.forNetwork(NETWORKS[chainId]).config.getProvider(); const contract = new Contract(contractAddress, MINT_CONTRACT_ABI, ethersProvider); return contract; }; @@ -11,7 +17,7 @@ const getContract = async (chainId: number, contractAddress: string, alchemy: Al export const getCoinbaseWalletMetadata = async ( chainId: number, contractAddress: string, - alchemy: Alchemy, + alchemy: AlchemyMultichainClient, ): Promise => { try { const contract = await getContract(chainId, contractAddress, alchemy); @@ -21,26 +27,24 @@ export const getCoinbaseWalletMetadata = async ( ...metadata, cost: parseInt(String(metadata.cost)), startTime: parseInt(String(metadata.startTime)), - endTime: parseInt(String(metadata.endTime)) + endTime: parseInt(String(metadata.endTime)), }; } catch (error) { - console.log(error) + console.log(error); } }; export const getCoinbaseWalletPriceInWei = async ( chainId: number, contractAddress: string, - alchemy: Alchemy, + alchemy: AlchemyMultichainClient, ): Promise => { try { const contract = await getContract(chainId, contractAddress, alchemy); const price = await contract.functions.cost(1); return `${parseInt(String(price))}`; - } catch (error) { - console.log(error) + console.log(error); } }; - diff --git a/src/ingestors/foundation/onchain-metadata.ts b/src/ingestors/foundation/onchain-metadata.ts index dfffd8c..2511fb4 100644 --- a/src/ingestors/foundation/onchain-metadata.ts +++ b/src/ingestors/foundation/onchain-metadata.ts @@ -1,8 +1,14 @@ import { Alchemy, Contract } from 'alchemy-sdk'; import { FOUNDATION_MINT_ABI } from './abi'; +import { AlchemyMultichainClient } from 'src/lib/rpc/alchemy-multichain'; +import { NETWORKS } from '../../../src/lib/simulation/simulation'; -const getContract = async (chainId: number, contractAddress: string, alchemy: Alchemy): Promise => { - const ethersProvider = await alchemy.config.getProvider(); +const getContract = async ( + chainId: number, + contractAddress: string, + alchemy: AlchemyMultichainClient, +): Promise => { + const ethersProvider = await alchemy.forNetwork(NETWORKS[chainId]).config.getProvider(); const contract = new Contract(contractAddress, FOUNDATION_MINT_ABI, ethersProvider); return contract; }; @@ -11,10 +17,9 @@ export const getFoundationMintPriceInWei = async ( chainId: number, contractAddress: string, dropAddress: string, - alchemy: Alchemy, + alchemy: AlchemyMultichainClient, saleType: 'FIXED_PRICE_DROP' | string, ): Promise => { - try { const contract = await getContract(chainId, contractAddress, alchemy); const saleData = @@ -26,6 +31,5 @@ export const getFoundationMintPriceInWei = async ( const totalFee = parseInt(tokenPrice.toString()) + parseInt(saleData.mintFeePerNftInWei.toString()); return `${totalFee}`; - } catch (error) { - } + } catch (error) {} }; diff --git a/src/ingestors/highlight/onchain-metadata.ts b/src/ingestors/highlight/onchain-metadata.ts index 4ce5524..a81c65b 100644 --- a/src/ingestors/highlight/onchain-metadata.ts +++ b/src/ingestors/highlight/onchain-metadata.ts @@ -1,15 +1,19 @@ -import { Alchemy, Contract } from 'alchemy-sdk'; +import { Alchemy, Contract, Network } from 'alchemy-sdk'; import { MINT_CONTRACT_ABI } from './abi'; +import { AlchemyMultichainClient } from 'src/lib/rpc/alchemy-multichain'; const CONTRACT_ADDRESS = '0x8087039152c472Fa74F47398628fF002994056EA'; -const getContract = async (alchemy: Alchemy): Promise => { - const ethersProvider = await alchemy.config.getProvider(); +const getContract = async (alchemy: AlchemyMultichainClient): Promise => { + const ethersProvider = await alchemy.forNetwork(Network.BASE_MAINNET).config.getProvider(); const contract = new Contract(CONTRACT_ADDRESS, MINT_CONTRACT_ABI, ethersProvider); return contract; }; -export const getHighlightMintPriceInWei = async (vectorId: number, alchemy: Alchemy): Promise => { +export const getHighlightMintPriceInWei = async ( + vectorId: number, + alchemy: AlchemyMultichainClient, +): Promise => { try { const contract = await getContract(alchemy); const data = await contract.functions.getAbridgedVector(vectorId); @@ -26,7 +30,7 @@ export const getHighlightMintPriceInWei = async (vectorId: number, alchemy: Alch export const getHighlightMetadata = async ( vectorId: number, - alchemy: Alchemy, + alchemy: AlchemyMultichainClient, ): Promise<{ startTimestamp: number; endTimestamp: number } | undefined> => { try { const contract = await getContract(alchemy); diff --git a/src/ingestors/manifold/index.ts b/src/ingestors/manifold/index.ts index 0a7f950..2f9e218 100644 --- a/src/ingestors/manifold/index.ts +++ b/src/ingestors/manifold/index.ts @@ -10,6 +10,7 @@ export class ManifoldIngestor implements MintIngestor { configuration = { supportsContractIsExpensive: true, }; + supportedChainIds = [8453, 1]; async supportsUrl(resources: MintIngestorResources, url: string): Promise { if (new URL(url).hostname !== 'app.manifold.xyz') { @@ -25,7 +26,7 @@ export class ManifoldIngestor implements MintIngestor { const { publicData } = data || {}; const { network: chainId, contract: contractAddress } = publicData || {}; - if (chainId !== 8453) { + if (!this.supportedChainIds.includes(chainId)) { return false; } @@ -42,7 +43,7 @@ export class ManifoldIngestor implements MintIngestor { return false; } - if (chainId !== 8453) { + if (!this.supportedChainIds.includes(chainId)) { return false; } @@ -67,7 +68,10 @@ export class ManifoldIngestor implements MintIngestor { const { chainId, contractAddress } = await manifoldOnchainDataFromUrl(url, resources.alchemy, resources.fetcher); if (!chainId || !contractAddress) { - throw new MintIngestorError(MintIngestionErrorName.MissingRequiredData, 'Missing required data, mint expired, or sold out'); + throw new MintIngestorError( + MintIngestionErrorName.MissingRequiredData, + 'Missing required data, mint expired, or sold out', + ); } return this.createMintForContract(resources, { chainId, contractAddress, url }); diff --git a/src/ingestors/manifold/offchain-metadata.ts b/src/ingestors/manifold/offchain-metadata.ts index f15b117..9241452 100644 --- a/src/ingestors/manifold/offchain-metadata.ts +++ b/src/ingestors/manifold/offchain-metadata.ts @@ -1,16 +1,15 @@ import { Axios } from 'axios'; -import { Alchemy } from 'alchemy-sdk'; import { getContract } from './onchain-metadata'; import { MANIFOLD_CLAIMS_ERC115_SPECIFIC_ABI, MANIFOLD_CLAIMS_ERC721_SPECIFIC_ABI } from './abi'; - +import { AlchemyMultichainClient } from '../../../src/lib/rpc/alchemy-multichain'; const MANIFOLD_LAZY_PAYABLE_CLAIM_CONTRACT_1155 = '0x26BBEA7803DcAc346D5F5f135b57Cf2c752A02bE'; const MANIFOLD_LAZY_PAYABLE_CLAIM_CONTRACT_721 = '0x23aA05a271DEBFFAA3D75739aF5581f744b326E4'; export const manifoldOnchainDataFromUrl = async ( - url: any, - alchemy: Alchemy, - fetcher: Axios + url: any, + alchemy: AlchemyMultichainClient, + fetcher: Axios, ): Promise => { const slug = url.match(/\/c\/([^\/]+)/)?.[1]; if (!slug) return false; @@ -20,6 +19,7 @@ export const manifoldOnchainDataFromUrl = async ( `https://apps.api.manifoldxyz.dev/public/instance/data?appId=2522713783&instanceSlug=${slug}`, ); const { id, creator, publicData } = data || {}; + const { asset, network: chainId, @@ -64,35 +64,42 @@ export const manifoldOnchainDataFromUrl = async ( // Check if mint sold out switch (spec) { - case 'ERC721': - { - const contract = await getContract(chainId, MANIFOLD_LAZY_PAYABLE_CLAIM_CONTRACT_721, alchemy, MANIFOLD_CLAIMS_ERC721_SPECIFIC_ABI); - const response = await contract.functions.getClaim(contractAddress, id); - const total = response[0][0]; - const totalMax = response[0][1]; + case 'ERC721': { + const contract = await getContract( + chainId, + MANIFOLD_LAZY_PAYABLE_CLAIM_CONTRACT_721, + alchemy, + MANIFOLD_CLAIMS_ERC721_SPECIFIC_ABI, + ); + const response = await contract.functions.getClaim(contractAddress, id); + const total = response[0][0]; + const totalMax = response[0][1]; - if (total == totalMax) return false; + if (total == totalMax) return false; - break; - } - - case 'ERC1155': - { - const contract = await getContract(chainId, MANIFOLD_LAZY_PAYABLE_CLAIM_CONTRACT_1155, alchemy, MANIFOLD_CLAIMS_ERC115_SPECIFIC_ABI); - const response = await contract.functions.getClaim(contractAddress, id); - const total = response[0][0]; - const totalMax = response[0][1]; + break; + } - if (total == totalMax) return false; - - break; - } + case 'ERC1155': { + const contract = await getContract( + chainId, + MANIFOLD_LAZY_PAYABLE_CLAIM_CONTRACT_1155, + alchemy, + MANIFOLD_CLAIMS_ERC115_SPECIFIC_ABI, + ); + const response = await contract.functions.getClaim(contractAddress, id); + const total = response[0][0]; + const totalMax = response[0][1]; + + if (total == totalMax) return false; + + break; + } default: return false; // Unknown spec } - return { instanceId: id, creatorName: publicData.asset.created_by, diff --git a/src/ingestors/manifold/onchain-metadata.ts b/src/ingestors/manifold/onchain-metadata.ts index 1afc836..a2cc521 100644 --- a/src/ingestors/manifold/onchain-metadata.ts +++ b/src/ingestors/manifold/onchain-metadata.ts @@ -1,10 +1,17 @@ -import { Alchemy, Contract } from 'alchemy-sdk'; +import { Contract } from 'alchemy-sdk'; import axios, { AxiosInstance } from 'axios'; import { MANIFOLD_CLAIMS_ABI } from './abi'; +import { AlchemyMultichainClient } from '../../../src/lib/rpc/alchemy-multichain'; +import { NETWORKS } from '../../../src/lib/simulation/simulation'; // fetch contract instance -export const getContract = async (chainId: number, contractAddress: string, alchemy: Alchemy, abi: any): Promise => { - const ethersProvider = await alchemy.config.getProvider(); +export const getContract = async ( + chainId: number, + contractAddress: string, + alchemy: AlchemyMultichainClient, + abi: any, +): Promise => { + const ethersProvider = await alchemy.forNetwork(NETWORKS[chainId]).config.getProvider(); const contract = new Contract(contractAddress, abi, ethersProvider); return contract; }; @@ -18,10 +25,9 @@ const convertNameToUrl = (name: string): string => { export const urlForValidManifoldContract = async ( chainId: number, contractAddress: string, - alchemy: Alchemy, + alchemy: AlchemyMultichainClient, fetcher: AxiosInstance = axios, ): Promise => { - let name: string; try { const contract = await getContract(chainId, contractAddress, alchemy, MANIFOLD_CLAIMS_ABI); @@ -49,9 +55,7 @@ export const urlForValidManifoldContract = async ( } }; -export const getManifoldMintPriceInEth = async ( - mintPrice: number -): Promise => { +export const getManifoldMintPriceInEth = async (mintPrice: number): Promise => { // fee amount is standard const feePrice = 500000000000000; @@ -62,11 +66,11 @@ export const getManifoldMintPriceInEth = async ( export const getManifoldMintOwner = async ( chainId: number, contractAddress: string, - alchemy: Alchemy, + alchemy: AlchemyMultichainClient, ): Promise => { const contract = await getContract(chainId, contractAddress, alchemy, MANIFOLD_CLAIMS_ABI); const response = await contract.functions.owner(); const owner = response[0]; - + return owner; }; diff --git a/src/ingestors/prohibition-daily/onchain-metadata.ts b/src/ingestors/prohibition-daily/onchain-metadata.ts index 55ed985..4929c48 100644 --- a/src/ingestors/prohibition-daily/onchain-metadata.ts +++ b/src/ingestors/prohibition-daily/onchain-metadata.ts @@ -1,16 +1,22 @@ -import { Alchemy, Contract } from 'alchemy-sdk'; +import { Contract } from 'alchemy-sdk'; import { PROHIBITION_DAILY_ABI } from './abi'; +import { AlchemyMultichainClient } from '../../../src/lib/rpc/alchemy-multichain'; +import { NETWORKS } from '../../../src/lib/simulation/simulation'; -const getContract = async (chainId: number, contractAddress: string, alchemy: Alchemy): Promise => { - const ethersProvider = await alchemy.config.getProvider(); +const getContract = async ( + chainId: number, + contractAddress: string, + alchemy: AlchemyMultichainClient, +): Promise => { + const ethersProvider = await alchemy.forNetwork(NETWORKS[chainId]).config.getProvider(); const contract = new Contract(contractAddress, PROHIBITION_DAILY_ABI, ethersProvider); return contract; }; - + export const getProhibitionContractMetadata = async ( chainId: number, contractAddress: string, - alchemy: Alchemy, + alchemy: AlchemyMultichainClient, ): Promise => { const contract = await getContract(chainId, contractAddress, alchemy); const metadata = await contract.functions.contractURI(); @@ -31,7 +37,7 @@ export const getProhibitionContractMetadata = async ( export const getProhibitionMintPriceInEth = async ( chainId: number, contractAddress: string, - alchemy: Alchemy, + alchemy: AlchemyMultichainClient, ): Promise => { const contract = await getContract(chainId, contractAddress, alchemy); const feePrice = await contract.functions.mintFee(1); diff --git a/src/ingestors/rarible/offchain-metadata.ts b/src/ingestors/rarible/offchain-metadata.ts index 675e77f..555cf96 100644 --- a/src/ingestors/rarible/offchain-metadata.ts +++ b/src/ingestors/rarible/offchain-metadata.ts @@ -1,122 +1,122 @@ -import { Alchemy, Contract } from "alchemy-sdk"; -import { RARIBLE_ABI } from "./abi"; +import { Contract } from 'alchemy-sdk'; +import { RARIBLE_ABI } from './abi'; +import { AlchemyMultichainClient } from '../../../src/lib/rpc/alchemy-multichain'; +import { NETWORKS } from '../../../src/lib/simulation/simulation'; interface ProfileData { - pName: string; - pImageUrl: string; - pDescription: string; - pWebsiteUrl: string; + pName: string; + pImageUrl: string; + pDescription: string; + pWebsiteUrl: string; } -export const raribleCreatorProfileDataGetter = async ( - ownerAddress: string -): Promise => { - const START_IDX = 0; - const endpoint = "https://rarible.com/marketplace/api/v4/profiles/list"; - const response = await fetch(endpoint, { - method: "POST", - headers: { - "Accept": "*/*", - "Content-Type": "application/json" - }, - body: JSON.stringify([ownerAddress]) - }); - - if (!response.ok) { - throw new Error(`Request failed with status code ${response.status} and text: ${response.statusText}.`); - } - - const profileData = await response.json(); - const pImageUrl = profileData[START_IDX].imageMedia.length === 0 ? "" : profileData[START_IDX].imageMedia[START_IDX].url; - - return { - pName: profileData[START_IDX].name, - pImageUrl, - pDescription: profileData[START_IDX].description, - pWebsiteUrl: `https://rarible.com/${profileData[START_IDX].shortUrl}`, - }; +export const raribleCreatorProfileDataGetter = async (ownerAddress: string): Promise => { + const START_IDX = 0; + const endpoint = 'https://rarible.com/marketplace/api/v4/profiles/list'; + const response = await fetch(endpoint, { + method: 'POST', + headers: { + Accept: '*/*', + 'Content-Type': 'application/json', + }, + body: JSON.stringify([ownerAddress]), + }); + + if (!response.ok) { + throw new Error(`Request failed with status code ${response.status} and text: ${response.statusText}.`); + } + + const profileData = await response.json(); + const pImageUrl = + profileData[START_IDX].imageMedia.length === 0 ? '' : profileData[START_IDX].imageMedia[START_IDX].url; + + return { + pName: profileData[START_IDX].name, + pImageUrl, + pDescription: profileData[START_IDX].description, + pWebsiteUrl: `https://rarible.com/${profileData[START_IDX].shortUrl}`, + }; }; export const raribleOnchainDataFromUrl = async ( - url: string -): Promise<{ chainId: number | undefined, contractAddress: string | undefined }> => { - const CHAIN_IDX = 4; - const CONTRACT_ADDRESS_IDX = 5; - const urlParts = url.split("/"); - const chainId = urlParts[CHAIN_IDX] === "base" ? 8453 : undefined; - - if (!chainId || !urlParts[CONTRACT_ADDRESS_IDX] || !urlParts[CONTRACT_ADDRESS_IDX].startsWith("0x")) { - return { chainId: undefined, contractAddress: undefined }; - } - - return { chainId, contractAddress: urlParts[CONTRACT_ADDRESS_IDX] }; + url: string, +): Promise<{ chainId: number | undefined; contractAddress: string | undefined }> => { + const CHAIN_IDX = 4; + const CONTRACT_ADDRESS_IDX = 5; + const urlParts = url.split('/'); + const chainId = urlParts[CHAIN_IDX] === 'base' ? 8453 : undefined; + + if (!chainId || !urlParts[CONTRACT_ADDRESS_IDX] || !urlParts[CONTRACT_ADDRESS_IDX].startsWith('0x')) { + return { chainId: undefined, contractAddress: undefined }; + } + + return { chainId, contractAddress: urlParts[CONTRACT_ADDRESS_IDX] }; }; export const raribleUrlForValidCollection = async ( - chainId: number, - contractAddress: string + chainId: number, + contractAddress: string, ): Promise => { - const apiKey = process.env.RARIBLE_API_KEY; - if (!apiKey) { - throw new Error("RARIBLE_API_KEY is not defined in the environment variables"); - } - - const chain = chainId === 8453 ? "BASE" : undefined; - if (!chain) { - throw new Error(`CHAIN ID is not valid: ${chainId}`); - } - - const endpoint = `https://api.rarible.org/v0.1/collections/${chain}%3A${contractAddress}`; - - const response = await fetch(endpoint, { - method: "GET", - headers: { - "Accept": "application/json", - "X-API-KEY": apiKey - } - }); - - if (response.status === 200) { - return `https://rarible.com/collection/${chain.toLowerCase()}/${contractAddress}/drops`; - } - if (response.status === 404) { - return ""; - } - - throw new Error(`Request failed with status code ${response.status} and text: ${response.statusText}.`); + const apiKey = process.env.RARIBLE_API_KEY; + if (!apiKey) { + throw new Error('RARIBLE_API_KEY is not defined in the environment variables'); + } + + const chain = chainId === 8453 ? 'BASE' : undefined; + if (!chain) { + throw new Error(`CHAIN ID is not valid: ${chainId}`); + } + + const endpoint = `https://api.rarible.org/v0.1/collections/${chain}%3A${contractAddress}`; + + const response = await fetch(endpoint, { + method: 'GET', + headers: { + Accept: 'application/json', + 'X-API-KEY': apiKey, + }, + }); + + if (response.status === 200) { + return `https://rarible.com/collection/${chain.toLowerCase()}/${contractAddress}/drops`; + } + if (response.status === 404) { + return ''; + } + + throw new Error(`Request failed with status code ${response.status} and text: ${response.statusText}.`); }; export const raribleUrlForValidBaseEthCollection = async ( - chainId: number, - contractAddress: string, - alchemy: Alchemy + chainId: number, + contractAddress: string, + alchemy: AlchemyMultichainClient, ): Promise => { - const START_IDX = 0; - const CONTRACT_ADDRESS_IDX = 5; - const TOKEN_ADDR_IDX = 6; - const ETH_MINT = "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE"; - - const collectionUrl = await raribleUrlForValidCollection(chainId, contractAddress); - if (!collectionUrl) { - return ""; - } - - const urlParts = collectionUrl.split("/"); - if (!urlParts) { - throw new Error("urlForValidRaribleBaseEthCollection: undefined urlParts of the collection url."); - } - - const collectionAddress = urlParts[CONTRACT_ADDRESS_IDX]; - const ethersProvider = await alchemy.config.getProvider(); - const contract = new Contract(collectionAddress, RARIBLE_ABI, ethersProvider); - const activeClaimId = await contract.functions.getActiveClaimConditionId(); - const claimCondition = await contract.functions.getClaimConditionById(parseInt(activeClaimId)); - const mintTokenAddress = claimCondition[START_IDX][TOKEN_ADDR_IDX]; - - if (mintTokenAddress === ETH_MINT) { - return collectionUrl; - } - - throw new Error("urlForValidRaribleBaseEthCollection: Not a Base Eth mint"); + const START_IDX = 0; + const CONTRACT_ADDRESS_IDX = 5; + const TOKEN_ADDR_IDX = 6; + const ETH_MINT = '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE'; + + const collectionUrl = await raribleUrlForValidCollection(chainId, contractAddress); + if (!collectionUrl) { + return ''; + } + + const urlParts = collectionUrl.split('/'); + if (!urlParts) { + throw new Error('urlForValidRaribleBaseEthCollection: undefined urlParts of the collection url.'); + } + + const collectionAddress = urlParts[CONTRACT_ADDRESS_IDX]; + const ethersProvider = await alchemy.forNetwork(NETWORKS[chainId]).config.getProvider(); + const contract = new Contract(collectionAddress, RARIBLE_ABI, ethersProvider); + const activeClaimId = await contract.functions.getActiveClaimConditionId(); + const claimCondition = await contract.functions.getClaimConditionById(parseInt(activeClaimId)); + const mintTokenAddress = claimCondition[START_IDX][TOKEN_ADDR_IDX]; + + if (mintTokenAddress === ETH_MINT) { + return collectionUrl; + } + + throw new Error('urlForValidRaribleBaseEthCollection: Not a Base Eth mint'); }; - diff --git a/src/ingestors/rarible/onchain-metadata.ts b/src/ingestors/rarible/onchain-metadata.ts index 6bb91e1..55522d3 100644 --- a/src/ingestors/rarible/onchain-metadata.ts +++ b/src/ingestors/rarible/onchain-metadata.ts @@ -1,161 +1,169 @@ -import { Alchemy, Contract } from 'alchemy-sdk'; +import { Contract } from 'alchemy-sdk'; import { RARIBLE_ABI } from './abi'; +import { AlchemyMultichainClient } from '../../../src/lib/rpc/alchemy-multichain'; +import { NETWORKS } from '../../../src/lib/simulation/simulation'; interface Tokendata { - name: string; - description: string; - imageURI: string; - unitMintPrice: number; - unitMintTokenAddress: string; - unitPerWallet: number; - conditionProof: string; - ownerAddress: string; - startDate: Date; - stopDate: Date; + name: string; + description: string; + imageURI: string; + unitMintPrice: number; + unitMintTokenAddress: string; + unitPerWallet: number; + conditionProof: string; + ownerAddress: string; + startDate: Date; + stopDate: Date; } enum ContractURIIndex { - CID = 2 + CID = 2, } enum ClaimConditionIndex { - StartTimestamp = 0, - MaxClaimableSupply = 1, - ConditionProof = 4, - MintPrice = 5, - TokenAddress = 6 + StartTimestamp = 0, + MaxClaimableSupply = 1, + ConditionProof = 4, + MintPrice = 5, + TokenAddress = 6, } class InvalidInputError extends Error { - constructor(message: string) { - super(message); - this.name = 'InvalidInputError'; - } + constructor(message: string) { + super(message); + this.name = 'InvalidInputError'; + } } class ContractInteractionError extends Error { - constructor(message: string) { - super(message); - this.name = 'ContractInteractionError'; - } + constructor(message: string) { + super(message); + this.name = 'ContractInteractionError'; + } } -const getContract = async (chainId: number, contractAddress: string, alchemy: Alchemy): Promise => { - try { - const ethersProvider = await alchemy.config.getProvider(); - return new Contract(contractAddress, RARIBLE_ABI, ethersProvider); - } catch (error: unknown) { - let errorMessage = 'Failed to initialize contract'; - if (error instanceof Error) { - errorMessage += `: ${error.message}`; - } else if (typeof error === 'string') { - errorMessage += `: ${error}`; - } else { - errorMessage += ': Unknown error occurred'; - } - throw new ContractInteractionError(errorMessage); +const getContract = async ( + chainId: number, + contractAddress: string, + alchemy: AlchemyMultichainClient, +): Promise => { + try { + const ethersProvider = await alchemy.forNetwork(NETWORKS[chainId]).config.getProvider(); + return new Contract(contractAddress, RARIBLE_ABI, ethersProvider); + } catch (error: unknown) { + let errorMessage = 'Failed to initialize contract'; + if (error instanceof Error) { + errorMessage += `: ${error.message}`; + } else if (typeof error === 'string') { + errorMessage += `: ${error}`; + } else { + errorMessage += ': Unknown error occurred'; } + throw new ContractInteractionError(errorMessage); + } }; const fetchIPFSMetadata = async (cid: string): Promise => { - const response = await fetch(`https://ipfs.io/ipfs/${cid}/0`, { - method: 'GET', - headers: { 'Accept': 'application/json' }, - }); - if (!response.ok) { - throw new Error(`IPFS request failed with status code ${response.status}: ${response.statusText}`); - } - return response.json(); + const response = await fetch(`https://ipfs.io/ipfs/${cid}/0`, { + method: 'GET', + headers: { Accept: 'application/json' }, + }); + if (!response.ok) { + throw new Error(`IPFS request failed with status code ${response.status}: ${response.statusText}`); + } + return response.json(); }; const getClaimCondition = async (contract: Contract, claimId: number): Promise => { - try { - return await contract.functions.getClaimConditionById(claimId); - } catch (error: unknown) { - let errorMessage = 'Failed to get claim condition'; - if (error instanceof Error) { - errorMessage += `: ${error.message}`; - } else if (typeof error === 'string') { - errorMessage += `: ${error}`; - } else { - errorMessage += ': Unknown error occurred'; - } - throw new ContractInteractionError(errorMessage); + try { + return await contract.functions.getClaimConditionById(claimId); + } catch (error: unknown) { + let errorMessage = 'Failed to get claim condition'; + if (error instanceof Error) { + errorMessage += `: ${error.message}`; + } else if (typeof error === 'string') { + errorMessage += `: ${error}`; + } else { + errorMessage += ': Unknown error occurred'; } + throw new ContractInteractionError(errorMessage); + } }; -const processClaimCondition = (claimCondition: any): { - mintTokenAddress: string; - mintPrice: number; - unitPerWallet: number; - conditionProof: string; - mintTimestamp: Date; +const processClaimCondition = ( + claimCondition: any, +): { + mintTokenAddress: string; + mintPrice: number; + unitPerWallet: number; + conditionProof: string; + mintTimestamp: Date; } => { - const [condition] = claimCondition; - return { - mintTokenAddress: condition[ClaimConditionIndex.TokenAddress], - mintPrice: parseInt(condition[ClaimConditionIndex.MintPrice]), - unitPerWallet: parseInt(condition[ClaimConditionIndex.ConditionProof]), - conditionProof: condition[ClaimConditionIndex.ConditionProof], - mintTimestamp: new Date(parseInt(condition[ClaimConditionIndex.StartTimestamp]) * 1000) - }; + const [condition] = claimCondition; + return { + mintTokenAddress: condition[ClaimConditionIndex.TokenAddress], + mintPrice: parseInt(condition[ClaimConditionIndex.MintPrice]), + unitPerWallet: parseInt(condition[ClaimConditionIndex.ConditionProof]), + conditionProof: condition[ClaimConditionIndex.ConditionProof], + mintTimestamp: new Date(parseInt(condition[ClaimConditionIndex.StartTimestamp]) * 1000), + }; }; export const raribleContractMetadataGetter = async ( - chainId: number, - contractAddress: string, - alchemy: Alchemy + chainId: number, + contractAddress: string, + alchemy: AlchemyMultichainClient, ): Promise => { - if (!chainId || !contractAddress || !alchemy) { - throw new InvalidInputError('Invalid input: chainId, contractAddress, and alchemy are required'); - } + if (!chainId || !contractAddress || !alchemy) { + throw new InvalidInputError('Invalid input: chainId, contractAddress, and alchemy are required'); + } - try { - const contract = await getContract(chainId, contractAddress, alchemy); - const [contractURI] = await contract.functions.contractURI(); - const cid = contractURI.split('/')[ContractURIIndex.CID]; + try { + const contract = await getContract(chainId, contractAddress, alchemy); + const [contractURI] = await contract.functions.contractURI(); + const cid = contractURI.split('/')[ContractURIIndex.CID]; - const [metadataJSON, [ownerAddress], activeClaimId] = await Promise.all([ - fetchIPFSMetadata(cid), - contract.functions.owner(), - contract.functions.getActiveClaimConditionId() - ]); + const [metadataJSON, [ownerAddress], activeClaimId] = await Promise.all([ + fetchIPFSMetadata(cid), + contract.functions.owner(), + contract.functions.getActiveClaimConditionId(), + ]); - const activeClaimCondition = await getClaimCondition(contract, parseInt(activeClaimId)); - const maxClaimableSupply = BigInt(activeClaimCondition[0][ClaimConditionIndex.MaxClaimableSupply]._hex).valueOf(); + const activeClaimCondition = await getClaimCondition(contract, parseInt(activeClaimId)); + const maxClaimableSupply = BigInt(activeClaimCondition[0][ClaimConditionIndex.MaxClaimableSupply]._hex).valueOf(); - let startCondition, stopCondition; - if (maxClaimableSupply > BigInt(0).valueOf()) { - startCondition = processClaimCondition(activeClaimCondition); - stopCondition = processClaimCondition(await getClaimCondition(contract, parseInt(activeClaimId) + 1)); - } else { - stopCondition = processClaimCondition(activeClaimCondition); - startCondition = processClaimCondition(await getClaimCondition(contract, parseInt(activeClaimId) - 1)); - } + let startCondition, stopCondition; + if (maxClaimableSupply > BigInt(0).valueOf()) { + startCondition = processClaimCondition(activeClaimCondition); + stopCondition = processClaimCondition(await getClaimCondition(contract, parseInt(activeClaimId) + 1)); + } else { + stopCondition = processClaimCondition(activeClaimCondition); + startCondition = processClaimCondition(await getClaimCondition(contract, parseInt(activeClaimId) - 1)); + } - return { - name: metadataJSON.name, - description: metadataJSON.description, - imageURI: metadataJSON.image, - unitMintPrice: startCondition.mintPrice, - unitPerWallet: startCondition.unitPerWallet, - unitMintTokenAddress: startCondition.mintTokenAddress, - conditionProof: startCondition.conditionProof, - ownerAddress: ownerAddress, - startDate: startCondition.mintTimestamp, - stopDate: stopCondition.mintTimestamp, - }; - } catch (error: unknown) { - let errorMessage = 'Failed to get contract metadata'; - if (error instanceof InvalidInputError || error instanceof ContractInteractionError) { - throw error; - } - if (error instanceof Error) { - errorMessage += `: ${error.message}`; - } else if (typeof error === 'string') { - errorMessage += `: ${error}`; - } else { - errorMessage += ': Unknown error occurred'; - } - throw new ContractInteractionError(errorMessage); + return { + name: metadataJSON.name, + description: metadataJSON.description, + imageURI: metadataJSON.image, + unitMintPrice: startCondition.mintPrice, + unitPerWallet: startCondition.unitPerWallet, + unitMintTokenAddress: startCondition.mintTokenAddress, + conditionProof: startCondition.conditionProof, + ownerAddress: ownerAddress, + startDate: startCondition.mintTimestamp, + stopDate: stopCondition.mintTimestamp, + }; + } catch (error: unknown) { + let errorMessage = 'Failed to get contract metadata'; + if (error instanceof InvalidInputError || error instanceof ContractInteractionError) { + throw error; } -}; \ No newline at end of file + if (error instanceof Error) { + errorMessage += `: ${error.message}`; + } else if (typeof error === 'string') { + errorMessage += `: ${error}`; + } else { + errorMessage += ': Unknown error occurred'; + } + throw new ContractInteractionError(errorMessage); + } +}; diff --git a/src/ingestors/rodeo/onchain-metadata.ts b/src/ingestors/rodeo/onchain-metadata.ts index ca4c9d5..4aa9045 100644 --- a/src/ingestors/rodeo/onchain-metadata.ts +++ b/src/ingestors/rodeo/onchain-metadata.ts @@ -1,10 +1,12 @@ -import { Alchemy, Contract } from 'alchemy-sdk'; +import { Alchemy, Contract, Network } from 'alchemy-sdk'; import { BigNumber } from 'alchemy-sdk'; import { RODEO_ABI } from './abi'; +import { AlchemyMultichainClient } from 'src/lib/rpc/alchemy-multichain'; +import { NETWORKS } from 'src/lib/simulation/simulation'; -const getContract = async (contractAddress: string, alchemy: Alchemy): Promise => { - const ethersProvider = await alchemy.config.getProvider(); +const getContract = async (contractAddress: string, alchemy: AlchemyMultichainClient): Promise => { + const ethersProvider = await alchemy.forNetwork(Network.BASE_MAINNET).config.getProvider(); const contract = new Contract(contractAddress, RODEO_ABI, ethersProvider); return contract; }; @@ -13,7 +15,7 @@ export const getRodeoFeeInEth = async ( salesTermId: number, referrer: string, mintContractAddress: string, - alchemy: Alchemy, + alchemy: AlchemyMultichainClient, ): Promise => { const contract = await getContract(mintContractAddress, alchemy); const feeConfig = await contract.functions.getFixedPriceSale(salesTermId, referrer); diff --git a/src/ingestors/transient-base/onchain-metadata.ts b/src/ingestors/transient-base/onchain-metadata.ts index 811c8ff..8cc4b73 100644 --- a/src/ingestors/transient-base/onchain-metadata.ts +++ b/src/ingestors/transient-base/onchain-metadata.ts @@ -1,14 +1,19 @@ -import { Alchemy, Contract } from 'alchemy-sdk'; +import { Alchemy, Contract, Network } from 'alchemy-sdk'; import { TRANSIENT_BASE_ABI } from './abi'; +import { AlchemyMultichainClient } from 'src/lib/rpc/alchemy-multichain'; -const getContract = async (contractAddress: string, alchemy: Alchemy): Promise => { - const ethersProvider = await alchemy.config.getProvider(); +const getContract = async (contractAddress: string, alchemy: AlchemyMultichainClient): Promise => { + const ethersProvider = await alchemy.forNetwork(Network.BASE_MAINNET).config.getProvider(); const contract = new Contract(contractAddress, TRANSIENT_BASE_ABI, ethersProvider); return contract; }; -export const getTransientProtocolFeeInEth = async (chainId: number, mintContractAddress: string, alchemy: Alchemy) => { +export const getTransientProtocolFeeInEth = async ( + chainId: number, + mintContractAddress: string, + alchemy: AlchemyMultichainClient, +) => { const contract = await getContract(mintContractAddress, alchemy); const protocolFee = await contract.functions.protocolFee(); return `${protocolFee}`; diff --git a/src/lib/resources.ts b/src/lib/resources.ts index 04daa10..a9d69d6 100644 --- a/src/lib/resources.ts +++ b/src/lib/resources.ts @@ -1,20 +1,22 @@ import { Alchemy, Network } from "alchemy-sdk"; import { MintIngestorResources } from "./types/mint-ingestor"; import axios from "axios"; +import { AlchemyMultichainClient } from './rpc/alchemy-multichain'; export const mintIngestorResources = (): MintIngestorResources => { - const ALCHEMY_API_KEY = process.env.ALCHEMY_API_KEY; - if (!ALCHEMY_API_KEY) { - throw new Error('ALCHEMY_API_KEY environment variable is not set (copy .env.sample to .env and fill in the correct values)'); - } - const settings = { - apiKey: ALCHEMY_API_KEY, - network: Network.BASE_MAINNET, // Replace with the correct network - }; - const alchemy = new Alchemy(settings); - - return { - alchemy, - fetcher: axios, - }; - }; \ No newline at end of file + const ALCHEMY_API_KEY = process.env.ALCHEMY_API_KEY; + if (!ALCHEMY_API_KEY) { + throw new Error( + 'ALCHEMY_API_KEY environment variable is not set (copy .env.sample to .env and fill in the correct values)', + ); + } + const settings = { + apiKey: ALCHEMY_API_KEY, + }; + const alchemy = new AlchemyMultichainClient(settings); + + return { + alchemy, + fetcher: axios, + }; +}; \ No newline at end of file diff --git a/src/lib/rpc/alchemy-multichain.ts b/src/lib/rpc/alchemy-multichain.ts new file mode 100644 index 0000000..dcdf1ae --- /dev/null +++ b/src/lib/rpc/alchemy-multichain.ts @@ -0,0 +1,64 @@ +import { Alchemy, AlchemySettings, Network } from 'alchemy-sdk'; + +/** + * This is a wrapper around the Alchemy class that allows you to use the same + * Alchemy object to make requests to multiple networks using different + * settings. + * + * When instantiating this class, you can pass in an `AlchemyMultiChainSettings` + * object to apply the same settings to all networks. You can also pass in an + * optional `overrides` object to apply different settings to specific + * networks. + */ +export class AlchemyMultichainClient { + readonly settings: AlchemyMultichainSettings; + readonly overrides: Partial>; + /** + * Lazy-loaded mapping of `Network` enum to `Alchemy` instance. + * + * @private + */ + private readonly instances: Map = new Map(); + + /** + * @param settings The settings to use for all networks. + * @param overrides Optional settings to use for specific networks. + */ + constructor(settings: AlchemyMultichainSettings, overrides?: Partial>) { + this.settings = settings; + this.overrides = overrides ?? {}; + } + + /** + * Returns an instance of `Alchemy` for the given `Network`. + * + * @param network + */ + forNetwork(network: Network): Alchemy { + return this.loadInstance(network); + } + + /** + * Checks if an instance of `Alchemy` exists for the given `Network`. If not, + * it creates one and stores it in the `instances` map. + * + * @private + * @param network + */ + private loadInstance(network: Network): Alchemy { + let alchemyInstance = this.instances.get(network); + if (!alchemyInstance) { + // Use overrides if they exist -- otherwise use the default settings. + const alchemySettings = + this.overrides && this.overrides[network] + ? { ...this.overrides[network], network } + : { ...this.settings, network }; + alchemyInstance = new Alchemy(alchemySettings); + this.instances.set(network, alchemyInstance); + } + return alchemyInstance; + } +} + +/** AlchemySettings with the `network` param omitted in order to avoid confusion. */ +export type AlchemyMultichainSettings = Omit; diff --git a/src/lib/types/mint-ingestor.ts b/src/lib/types/mint-ingestor.ts index aaa0e7e..da30312 100644 --- a/src/lib/types/mint-ingestor.ts +++ b/src/lib/types/mint-ingestor.ts @@ -1,4 +1,4 @@ -import { Alchemy } from 'alchemy-sdk'; +import { AlchemyMultichainClient } from '../rpc/alchemy-multichain'; import { MintTemplate } from './mint-template'; import { AxiosInstance } from 'axios'; @@ -19,7 +19,7 @@ interface MintIngestor { } type MintIngestorResources = { - alchemy: Alchemy; + alchemy: AlchemyMultichainClient; fetcher: AxiosInstance; }; @@ -32,6 +32,6 @@ export type MintIngestorOptions = { */ supportsUrlIsExpensive?: boolean; supportsContractIsExpensive?: boolean; -} +}; export { MintIngestor, MintIngestorResources }; diff --git a/test/ingestors/manifold.test.ts b/test/ingestors/manifold.test.ts index 42ef17f..547e213 100644 --- a/test/ingestors/manifold.test.ts +++ b/test/ingestors/manifold.test.ts @@ -12,7 +12,7 @@ describe('manifold', function () { new ManifoldIngestor(), resources, { - successUrls: ['https://app.manifold.xyz/c/spaceexplorer'], + successUrls: ['https://app.manifold.xyz/c/spaceexplorer', 'https://app.manifold.xyz/c/freedom-to-run'], failureUrls: ['https://app.manifold.xyz/', 'https://app.manifold.xyz/x/spaceexplorer/'], successContracts: [ // { chainId: 8453, contractAddress: '0xE2Cf639a5eBA5e8d1e291AEb44ac66c8c0727F98' } From d7c0d1143787cd2ec76539a18297318c4a64ac75 Mon Sep 17 00:00:00 2001 From: Chris Maddern Date: Thu, 29 Aug 2024 11:07:02 -0400 Subject: [PATCH 2/2] Export alchemy multichain --- src/lib/index.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/lib/index.ts b/src/lib/index.ts index 777c84a..bc62cd7 100644 --- a/src/lib/index.ts +++ b/src/lib/index.ts @@ -1,2 +1,3 @@ export * from './builder/mint-template-builder'; -export * from './types/'; \ No newline at end of file +export * from './types/'; +export * from './rpc/alchemy-multichain'; \ No newline at end of file