From 6c058277d45b24f99f5f3efb9eb046d92ad84d41 Mon Sep 17 00:00:00 2001 From: Peter Date: Fri, 22 Nov 2024 22:53:02 +0200 Subject: [PATCH 01/11] chore: test start --- test/ingestors/vv.test.ts | 79 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 test/ingestors/vv.test.ts diff --git a/test/ingestors/vv.test.ts b/test/ingestors/vv.test.ts new file mode 100644 index 0000000..e9d3bcf --- /dev/null +++ b/test/ingestors/vv.test.ts @@ -0,0 +1,79 @@ +import { expect } from 'chai'; +import { VvIngestor } from '../../src/ingestors/vv'; +import { mintIngestorResources } from '../../src/lib/resources'; +import { EVMMintInstructions } from '../../src/lib/types/mint-template'; +import { MintTemplateBuilder } from '../../src/lib/builder/mint-template-builder'; +import { basicIngestorTests } from '../shared/basic-ingestor-tests'; + +const resources = mintIngestorResources(); + +describe('vv', function () { + basicIngestorTests(new VvIngestor(), resources, { + successUrls: ['https://mint.vv.xyz/0xcb52f0fe1d559cd2869db7f29753e8951381b4a3/1'], + failureUrls: [ + 'https://mint.vv.xyz/0xcb52f0fe1d559cd2869db7f29753e8951381b4a3', + 'https://foundation.app/mint/eth/0xcc5C8eb0108d85f091e4468999E0D6fd4273eD99', + ], + successContracts: [{ chainId: 1, contractAddress: '0xcb52f0fe1d559cd2869db7f29753e8951381b4a3' }], + failureContracts: [{ chainId: 8453, contractAddress: '0xcb52f0fe1d559cd2869db7f29753e8951381b4a3' }], + }); + it('supportsUrl: Returns false for an unsupported URL', async function () { + const ingestor = new VvIngestor(); + const url = 'https://example.com'; + const resources = mintIngestorResources(); + const result = await ingestor.supportsUrl(resources, url); + expect(result).to.be.false; + }); + + it('supportsUrl: Returns true for a supported URL', async function () { + const ingestor = new VvIngestor(); + const url = 'https://mint.vv.xyz/0xcb52f0fe1d559cd2869db7f29753e8951381b4a3/1'; + const resources = mintIngestorResources(); + const result = await ingestor.supportsUrl(resources, url); + expect(result).to.be.true; + }); + + // TODO Contract works for latest token id + it('supportsContract: Returns true for a supported contract', async function () { + const ingestor = new VvIngestor(); + const resources = mintIngestorResources(); + const contract = { + chainId: 1, + contractAddress: '0xcb52f0fe1d559cd2869db7f29753e8951381b4a3', + }; + + const supported = await ingestor.supportsContract(resources, contract); + expect(supported).to.be.true; + }); + + it('createMintTemplateForUrl: Returns a mint template for a supported URL with frame contract', async function () { + const ingestor = new VvIngestor(); + const url = 'https://mint.vv.xyz/0xcb52f0fe1d559cd2869db7f29753e8951381b4a3/1'; + const resources = mintIngestorResources(); + const template = await ingestor.createMintTemplateForUrl(resources, url); + + // Verify that the mint template passed validation + const builder = new MintTemplateBuilder(template); + builder.validateMintTemplate(); + + expect(template.name).to.equal('Eternal Beauty'); + expect(template.description).to.contain('Beauty carved into the eternal marble.'); + const mintInstructions = template.mintInstructions as EVMMintInstructions; + + expect(mintInstructions.contractAddress).to.equal('0x62037b26ffF91929655AA3A060F327b47d1e2b3e'); + expect(mintInstructions.contractMethod).to.equal('mintFromFixedPriceSaleV2'); + expect(mintInstructions.contractParams).to.equal( + '["0x89E63F58da71E9CD4DA439C3D1194917c67eb869", 1, address, "0x0000000000000000000000000000000000000000"]', + ); + + expect(mintInstructions.priceWei).to.equal('0'); + + expect(template.featuredImageUrl).to.equal( + 'https://f8n-production-collection-assets.imgix.net/8453/0x89E63F58da71E9CD4DA439C3D1194917c67eb869/pre_reveal/nft.jpg', + ); + + expect(template.marketingUrl).to.equal(url); + expect(template.availableForPurchaseStart?.getTime()).to.equal(+new Date('2024-07-04T19:00:00.000Z')); + expect(template.availableForPurchaseEnd?.getTime()).to.equal(+new Date('2030-01-01T00:00:00.000Z')); + }); +}); From 5cb13f9d29725e9bb7c23edafd6242b87d356707 Mon Sep 17 00:00:00 2001 From: Peter Date: Sat, 23 Nov 2024 21:24:22 +0200 Subject: [PATCH 02/11] chore: udpate abi --- src/ingestors/vv/abi.ts | 48 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 src/ingestors/vv/abi.ts diff --git a/src/ingestors/vv/abi.ts b/src/ingestors/vv/abi.ts new file mode 100644 index 0000000..542af5f --- /dev/null +++ b/src/ingestors/vv/abi.ts @@ -0,0 +1,48 @@ +export const MINT_CONTRACT_ABI = [ + { + inputs: [ + { internalType: 'uint256', name: 'tokenId', type: 'uint256' }, + { internalType: 'uint256', name: 'amount', type: 'uint256' }, + ], + name: 'mint', + outputs: [], + stateMutability: 'payable', + type: 'function', + }, + { + inputs: [{ internalType: 'uint256', name: 'tokenId', type: 'uint256' }], + name: 'get', + outputs: [ + { internalType: 'string', name: 'name', type: 'string' }, + { internalType: 'string', name: 'description', type: 'string' }, + { internalType: 'address[]', name: 'artifact', type: 'address[]' }, + { internalType: 'uint32', name: 'renderer', type: 'uint32' }, + { internalType: 'uint32', name: 'mintedBlock', type: 'uint32' }, + { internalType: 'uint64', name: 'closeAt', type: 'uint64' }, + { internalType: 'uint128', name: 'data', type: 'uint128' }, + ], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'latestTokenId', + outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'contractURI', + outputs: [{ internalType: 'string', name: '', type: 'string' }], + stateMutability: 'view', + type: 'function', + }, + { + inputs: [], + name: 'owner', + outputs: [{ internalType: 'address', name: '', type: 'address' }], + stateMutability: 'view', + type: 'function', + }, +]; From 8efa25f8c281d31f4cf1132efc47d922286f779e Mon Sep 17 00:00:00 2001 From: Peter Date: Sat, 23 Nov 2024 21:24:43 +0200 Subject: [PATCH 03/11] chore: add vv to list --- src/ingestors/index.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/ingestors/index.ts b/src/ingestors/index.ts index fda2eac..6d15fe8 100644 --- a/src/ingestors/index.ts +++ b/src/ingestors/index.ts @@ -9,6 +9,7 @@ import { FoundationIngestor } from './foundation'; import { CoinbaseWalletIngestor } from './coinbase-wallet'; import { ZoraInternalIngestor } from './zora-internal'; import { RodeoIngestor } from './rodeo'; +import { VvIngestor } from './vv'; export type MintIngestionMap = { [key: string]: MintIngestor; @@ -24,7 +25,8 @@ export const ALL_MINT_INGESTORS: MintIngestionMap = { transient: new TransientIngestor(), highlight: new HighlightIngestor(), foundation: new FoundationIngestor(), - 'coinbase-wallet': new CoinbaseWalletIngestor() + 'coinbase-wallet': new CoinbaseWalletIngestor(), + vv: new VvIngestor(), }; export * from './'; From 203b8efcb90cbbabcf52c9bb75a769f219862e9c Mon Sep 17 00:00:00 2001 From: Peter Date: Sat, 23 Nov 2024 21:25:12 +0200 Subject: [PATCH 04/11] feat: onchain methods --- src/ingestors/vv/onchain-metadata.ts | 90 ++++++++++++++++++++++++++++ 1 file changed, 90 insertions(+) create mode 100644 src/ingestors/vv/onchain-metadata.ts diff --git a/src/ingestors/vv/onchain-metadata.ts b/src/ingestors/vv/onchain-metadata.ts new file mode 100644 index 0000000..8dd3691 --- /dev/null +++ b/src/ingestors/vv/onchain-metadata.ts @@ -0,0 +1,90 @@ +import { Alchemy, AlchemyProvider, Contract } from 'alchemy-sdk'; +import { MINT_CONTRACT_ABI } from './abi'; + +const CONTRACT_ADDRESS = '0x8087039152c472Fa74F47398628fF002994056EA'; + +const getContract = async ( + alchemy: Alchemy, + contractAddress: string, +): Promise<{ contract: Contract; provider: AlchemyProvider }> => { + const ethersProvider = await alchemy.config.getProvider(); + const contract = new Contract(contractAddress, MINT_CONTRACT_ABI, ethersProvider); + return { + contract, + provider: ethersProvider, + }; +}; + +export const getVvMintPriceInWei = async ( + alchemy: Alchemy, + contractAddress: string, + amount: number, +): Promise => { + try { + const { provider } = await getContract(alchemy, contractAddress); + const block = await provider.getBlock('latest'); + + // Extract the base fee per gas + const baseFeePerGas = block.baseFeePerGas; + + if (!baseFeePerGas) { + throw new Error('Unable to fetch baseFee'); + } + + return `${+baseFeePerGas * 60000}`; + } catch (error) { + console.log(error); + } +}; + +export const getVvLatestTokenId = async ( + alchemy: Alchemy, + contractAddress: string, +): Promise<{ startTimestamp: number; endTimestamp: number } | undefined> => { + try { + const { contract } = await getContract(alchemy, contractAddress); + const tokenId = await contract.functions.latestTokenId(); + console.log({ tokenId }); + return tokenId; + } catch (error) {} +}; + +export const getVvCollection = async (alchemy: Alchemy, contractAddress: string, vectorId?: number) => { + try { + const { contract } = await getContract(alchemy, contractAddress); + const collection = await contract.functions.get(vectorId); + const { name, description, artifact, renderer, mintedBlock, closeAt, data } = collection[0]; + return { name, description, artifact, renderer, mintedBlock, closeAt, data }; + } catch (error) { + console.log(error); + } +}; + +export const getVvCollectionMetadata = async (alchemy: Alchemy, contractAddress: string, vectorId?: number) => { + try { + const { contract } = await getContract(alchemy, contractAddress); + const uri = await contract.functions.contractURI(); + console.log(uri); + const rawContent = uri[0].split(',')[1]; + let jsonString = atob(rawContent); + const { name, symbol, description, image: imageBase64 } = JSON.parse(jsonString); + const rawImage = imageBase64.split(',')[1]; + const image = atob(rawImage); + + return { name, symbol, description, image }; + } catch (error) { + console.log(error); + } +}; + +export const getVvCollectionCreator = async (alchemy: Alchemy, contractAddress: string) => { + try { + const { contract, provider } = await getContract(alchemy, contractAddress); + const owner = await contract.functions.owner(); + const name = await provider.lookupAddress(owner[0]); + return { + creator: owner, + name, + }; + } catch (error) {} +}; From 967507ffcdf43ac57d5a64916d7eed282fea989b Mon Sep 17 00:00:00 2001 From: Peter Date: Sat, 23 Nov 2024 21:26:17 +0200 Subject: [PATCH 05/11] feat: ingestor v0.1 --- src/ingestors/vv/index.ts | 151 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 151 insertions(+) create mode 100644 src/ingestors/vv/index.ts diff --git a/src/ingestors/vv/index.ts b/src/ingestors/vv/index.ts new file mode 100644 index 0000000..c058603 --- /dev/null +++ b/src/ingestors/vv/index.ts @@ -0,0 +1,151 @@ +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 { MintTemplateBuilder } from '../../lib/builder/mint-template-builder'; +import { + getVvMintPriceInWei, + getVvLatestTokenId, + getVvCollectionCreator, + getVvCollectionMetadata, +} from './onchain-metadata'; +import { getVvCollection } from './onchain-metadata'; +import { MINT_CONTRACT_ABI } from './abi'; + +// https://mint.vv.xyz/0xcb52f0fe1d559cd2869db7f29753e8951381b4a3/1 +export class VvIngestor implements MintIngestor { + async supportsUrl(resources: MintIngestorResources, url: string): Promise { + const splitUrl = url.split('/'); + const tokenId = splitUrl.pop(); + const address = splitUrl.pop(); + if (!tokenId || !address) { + return false; + } + + const collection = await getVvCollection(resources.alchemy, address as string, +tokenId); + if (!collection) return false; + + console.log('here'); + + const urlPattern = /^https:\/\/mint\.vv\.xyz\/0x[a-fA-F0-9]{40}\/\d+$/; + return ( + new URL(url).hostname === 'www.mint.vv.xyz' || new URL(url).hostname === 'mint.vv.xyz' || urlPattern.test(url) + ); + } + + async supportsContract(resources: MintIngestorResources, contractOptions: MintContractOptions): Promise { + if (!(contractOptions.chainId === 1 || contractOptions.chainId === 8453)) { + return false; + } + const collection = await getVvCollection( + resources.alchemy, + contractOptions.contractAddress, + contractOptions.tokenId ? +contractOptions.tokenId : undefined, + ); + if (!collection) { + return false; + } + return true; + } + + async createMintForContract( + resources: MintIngestorResources, + contractOptions: MintContractOptions, + ): Promise { + const mintBuilder = new MintTemplateBuilder() + .setMintInstructionType(MintInstructionType.EVM_MINT) + .setPartnerName('Highlight'); + + if (contractOptions.url) { + mintBuilder.setMarketingUrl(contractOptions.url); + } + + const { contractAddress } = contractOptions; + const tokenId = contractOptions.tokenId ?? (await getVvLatestTokenId(resources.alchemy, contractAddress)); + + const collection = await getVvCollection(resources.alchemy, contractAddress, tokenId ? +tokenId : undefined); + + if (!collection) { + throw new MintIngestorError(MintIngestionErrorName.CouldNotResolveMint, 'Collection not found'); + } + + const metadata = await getVvCollectionMetadata(resources.alchemy, contractAddress); + + if (!metadata) { + throw new MintIngestorError(MintIngestionErrorName.CouldNotResolveMint, 'Collection metadata not found'); + } + + mintBuilder.setName(metadata.name).setDescription(metadata.description).setFeaturedImageUrl(metadata.image); + mintBuilder.setMintOutputContract({ chainId: 1, address: contractAddress }); + + const creatorData = await getVvCollectionCreator(resources.alchemy, contractAddress); + + if (!creatorData) { + throw new MintIngestorError(MintIngestionErrorName.MissingRequiredData, 'Error finding creator'); + } + + const { creator, name: creatorName } = creatorData; + + mintBuilder.setCreator({ + name: creatorName || '', + walletAddress: creator, + }); + + mintBuilder.setMintOutputContract({ chainId: 1, address: contractAddress }); + + const totalPriceWei = await getVvMintPriceInWei(resources.alchemy, contractAddress, 1); + + if (!totalPriceWei) { + throw new MintIngestorError(MintIngestionErrorName.MissingRequiredData, 'Price not available'); + } + + mintBuilder.setMintInstructions({ + chainId: 8453, + contractAddress, + contractMethod: 'mint', + contractParams: `[${tokenId}, quantity]`, + abi: MINT_CONTRACT_ABI, + priceWei: totalPriceWei, + supportsQuantity: true, + }); + + const { closeAt } = collection; + + // Tokens are open to be minted for 24 hours after token creation. + const startTimestamp = new Date(closeAt * 1000 - 24 * 60 * 60 * 1000); + const liveDate = +new Date() > +startTimestamp ? new Date() : startTimestamp; + mintBuilder + .setAvailableForPurchaseStart(new Date(startTimestamp || Date.now())) + .setAvailableForPurchaseEnd(new Date(closeAt * 1000)) + .setLiveDate(liveDate); + + return mintBuilder.build(); + } + + async createMintTemplateForUrl(resources: MintIngestorResources, url: string): Promise { + const isCompatible = await this.supportsUrl(resources, url); + if (!isCompatible) { + throw new MintIngestorError(MintIngestionErrorName.IncompatibleUrl, 'Incompatible URL'); + } + + // Example URL: https://highlight.xyz/mint/665fa33f07b3436991e55632 + const splits = url.split('/'); + const id = splits.pop(); + const contract = splits.pop(); + + if (!id || !contract) { + throw new MintIngestorError(MintIngestionErrorName.CouldNotResolveMint, 'Url error'); + } + + const collection = await getVvCollection(resources.alchemy, contract, +id); + + if (!collection) { + throw new MintIngestorError(MintIngestionErrorName.CouldNotResolveMint, 'No such collection'); + } + + return this.createMintForContract(resources, { + chainId: 1, + contractAddress: contract, + url, + }); + } +} From bee5a30fd2974f55d519f687534e32e08eb299b8 Mon Sep 17 00:00:00 2001 From: Peter Date: Sat, 23 Nov 2024 21:27:50 +0200 Subject: [PATCH 06/11] chore: remove console logs --- src/ingestors/vv/index.ts | 2 -- src/ingestors/vv/onchain-metadata.ts | 14 +++----------- 2 files changed, 3 insertions(+), 13 deletions(-) diff --git a/src/ingestors/vv/index.ts b/src/ingestors/vv/index.ts index c058603..a1f903e 100644 --- a/src/ingestors/vv/index.ts +++ b/src/ingestors/vv/index.ts @@ -24,8 +24,6 @@ export class VvIngestor implements MintIngestor { const collection = await getVvCollection(resources.alchemy, address as string, +tokenId); if (!collection) return false; - console.log('here'); - const urlPattern = /^https:\/\/mint\.vv\.xyz\/0x[a-fA-F0-9]{40}\/\d+$/; return ( new URL(url).hostname === 'www.mint.vv.xyz' || new URL(url).hostname === 'mint.vv.xyz' || urlPattern.test(url) diff --git a/src/ingestors/vv/onchain-metadata.ts b/src/ingestors/vv/onchain-metadata.ts index 8dd3691..1c898b8 100644 --- a/src/ingestors/vv/onchain-metadata.ts +++ b/src/ingestors/vv/onchain-metadata.ts @@ -32,9 +32,7 @@ export const getVvMintPriceInWei = async ( } return `${+baseFeePerGas * 60000}`; - } catch (error) { - console.log(error); - } + } catch (error) {} }; export const getVvLatestTokenId = async ( @@ -44,7 +42,6 @@ export const getVvLatestTokenId = async ( try { const { contract } = await getContract(alchemy, contractAddress); const tokenId = await contract.functions.latestTokenId(); - console.log({ tokenId }); return tokenId; } catch (error) {} }; @@ -55,16 +52,13 @@ export const getVvCollection = async (alchemy: Alchemy, contractAddress: string, const collection = await contract.functions.get(vectorId); const { name, description, artifact, renderer, mintedBlock, closeAt, data } = collection[0]; return { name, description, artifact, renderer, mintedBlock, closeAt, data }; - } catch (error) { - console.log(error); - } + } catch (error) {} }; export const getVvCollectionMetadata = async (alchemy: Alchemy, contractAddress: string, vectorId?: number) => { try { const { contract } = await getContract(alchemy, contractAddress); const uri = await contract.functions.contractURI(); - console.log(uri); const rawContent = uri[0].split(',')[1]; let jsonString = atob(rawContent); const { name, symbol, description, image: imageBase64 } = JSON.parse(jsonString); @@ -72,9 +66,7 @@ export const getVvCollectionMetadata = async (alchemy: Alchemy, contractAddress: const image = atob(rawImage); return { name, symbol, description, image }; - } catch (error) { - console.log(error); - } + } catch (error) {} }; export const getVvCollectionCreator = async (alchemy: Alchemy, contractAddress: string) => { From ec5736fc1a642e281230c84f557e8e0e8235579c Mon Sep 17 00:00:00 2001 From: Peter Date: Sat, 23 Nov 2024 22:05:16 +0200 Subject: [PATCH 07/11] fix: tests --- test/ingestors/vv.test.ts | 29 +++++++++++++++-------------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/test/ingestors/vv.test.ts b/test/ingestors/vv.test.ts index e9d3bcf..8abd525 100644 --- a/test/ingestors/vv.test.ts +++ b/test/ingestors/vv.test.ts @@ -15,7 +15,7 @@ describe('vv', function () { 'https://foundation.app/mint/eth/0xcc5C8eb0108d85f091e4468999E0D6fd4273eD99', ], successContracts: [{ chainId: 1, contractAddress: '0xcb52f0fe1d559cd2869db7f29753e8951381b4a3' }], - failureContracts: [{ chainId: 8453, contractAddress: '0xcb52f0fe1d559cd2869db7f29753e8951381b4a3' }], + failureContracts: [], }); it('supportsUrl: Returns false for an unsupported URL', async function () { const ingestor = new VvIngestor(); @@ -56,24 +56,25 @@ describe('vv', function () { const builder = new MintTemplateBuilder(template); builder.validateMintTemplate(); - expect(template.name).to.equal('Eternal Beauty'); - expect(template.description).to.contain('Beauty carved into the eternal marble.'); + expect(template.name).to.equal('Artifacts'); + expect(template.description).to.contain('To mint is a human right.'); const mintInstructions = template.mintInstructions as EVMMintInstructions; - expect(mintInstructions.contractAddress).to.equal('0x62037b26ffF91929655AA3A060F327b47d1e2b3e'); - expect(mintInstructions.contractMethod).to.equal('mintFromFixedPriceSaleV2'); - expect(mintInstructions.contractParams).to.equal( - '["0x89E63F58da71E9CD4DA439C3D1194917c67eb869", 1, address, "0x0000000000000000000000000000000000000000"]', - ); + expect(mintInstructions.contractAddress).to.equal('0xcb52f0fe1d559cd2869db7f29753e8951381b4a3'); + expect(mintInstructions.contractMethod).to.equal('mint'); + expect(mintInstructions.contractParams).to.equal('[1, quantity]'); - expect(mintInstructions.priceWei).to.equal('0'); + // Gas price at block #21167990 + expect(mintInstructions.priceWei).to.equal(String(60000 * 39874264171)); - expect(template.featuredImageUrl).to.equal( - 'https://f8n-production-collection-assets.imgix.net/8453/0x89E63F58da71E9CD4DA439C3D1194917c67eb869/pre_reveal/nft.jpg', - ); + expect(template.creator?.name).to.equal('visualizevalue.eth'); + expect(template.creator?.walletAddress).to.equal('0xc8f8e2F59Dd95fF67c3d39109ecA2e2A017D4c8a'); + + // Image is stored onchain as an svg + expect(template.featuredImageUrl).to.contain(' Date: Sat, 23 Nov 2024 22:05:48 +0200 Subject: [PATCH 08/11] fix: price calculations --- src/ingestors/vv/index.ts | 2 +- src/ingestors/vv/onchain-metadata.ts | 19 +++++++------------ 2 files changed, 8 insertions(+), 13 deletions(-) diff --git a/src/ingestors/vv/index.ts b/src/ingestors/vv/index.ts index a1f903e..a64bf53 100644 --- a/src/ingestors/vv/index.ts +++ b/src/ingestors/vv/index.ts @@ -90,7 +90,7 @@ export class VvIngestor implements MintIngestor { mintBuilder.setMintOutputContract({ chainId: 1, address: contractAddress }); - const totalPriceWei = await getVvMintPriceInWei(resources.alchemy, contractAddress, 1); + const totalPriceWei = await getVvMintPriceInWei(resources.alchemy, contractAddress, collection.mintedBlock); if (!totalPriceWei) { throw new MintIngestorError(MintIngestionErrorName.MissingRequiredData, 'Price not available'); diff --git a/src/ingestors/vv/onchain-metadata.ts b/src/ingestors/vv/onchain-metadata.ts index 1c898b8..88c4b8a 100644 --- a/src/ingestors/vv/onchain-metadata.ts +++ b/src/ingestors/vv/onchain-metadata.ts @@ -18,15 +18,13 @@ const getContract = async ( export const getVvMintPriceInWei = async ( alchemy: Alchemy, contractAddress: string, - amount: number, + blockNumber: number, ): Promise => { try { const { provider } = await getContract(alchemy, contractAddress); - const block = await provider.getBlock('latest'); + const block = await provider.getBlock(blockNumber); - // Extract the base fee per gas const baseFeePerGas = block.baseFeePerGas; - if (!baseFeePerGas) { throw new Error('Unable to fetch baseFee'); } @@ -35,22 +33,19 @@ export const getVvMintPriceInWei = async ( } catch (error) {} }; -export const getVvLatestTokenId = async ( - alchemy: Alchemy, - contractAddress: string, -): Promise<{ startTimestamp: number; endTimestamp: number } | undefined> => { +export const getVvLatestTokenId = async (alchemy: Alchemy, contractAddress: string): Promise => { try { const { contract } = await getContract(alchemy, contractAddress); const tokenId = await contract.functions.latestTokenId(); - return tokenId; + return +tokenId; } catch (error) {} }; export const getVvCollection = async (alchemy: Alchemy, contractAddress: string, vectorId?: number) => { try { const { contract } = await getContract(alchemy, contractAddress); - const collection = await contract.functions.get(vectorId); - const { name, description, artifact, renderer, mintedBlock, closeAt, data } = collection[0]; + const collection = await contract.functions.get(vectorId ?? (await getVvLatestTokenId(alchemy, contractAddress))); + const { name, description, artifact, renderer, mintedBlock, closeAt, data } = collection; return { name, description, artifact, renderer, mintedBlock, closeAt, data }; } catch (error) {} }; @@ -75,7 +70,7 @@ export const getVvCollectionCreator = async (alchemy: Alchemy, contractAddress: const owner = await contract.functions.owner(); const name = await provider.lookupAddress(owner[0]); return { - creator: owner, + creator: owner[0], name, }; } catch (error) {} From b50701ddf5e287fda3676f49e57cff2daedc2695 Mon Sep 17 00:00:00 2001 From: Peter Date: Sat, 23 Nov 2024 22:06:08 +0200 Subject: [PATCH 09/11] chore: temporary --- src/lib/resources.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/resources.ts b/src/lib/resources.ts index 04daa10..3d979be 100644 --- a/src/lib/resources.ts +++ b/src/lib/resources.ts @@ -9,7 +9,7 @@ export const mintIngestorResources = (): MintIngestorResources => { } const settings = { apiKey: ALCHEMY_API_KEY, - network: Network.BASE_MAINNET, // Replace with the correct network + network: Network.ETH_MAINNET, // Replace with the correct network }; const alchemy = new Alchemy(settings); From f03e7cd7824ee44cef54d38670d8bfa35b0315aa Mon Sep 17 00:00:00 2001 From: Peter Date: Sun, 24 Nov 2024 11:57:54 +0200 Subject: [PATCH 10/11] chore: fix comment --- src/ingestors/vv/index.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/ingestors/vv/index.ts b/src/ingestors/vv/index.ts index a64bf53..bce39d8 100644 --- a/src/ingestors/vv/index.ts +++ b/src/ingestors/vv/index.ts @@ -11,7 +11,6 @@ import { import { getVvCollection } from './onchain-metadata'; import { MINT_CONTRACT_ABI } from './abi'; -// https://mint.vv.xyz/0xcb52f0fe1d559cd2869db7f29753e8951381b4a3/1 export class VvIngestor implements MintIngestor { async supportsUrl(resources: MintIngestorResources, url: string): Promise { const splitUrl = url.split('/'); @@ -125,7 +124,7 @@ export class VvIngestor implements MintIngestor { throw new MintIngestorError(MintIngestionErrorName.IncompatibleUrl, 'Incompatible URL'); } - // Example URL: https://highlight.xyz/mint/665fa33f07b3436991e55632 + // Example URL: https://mint.vv.xyz/0xcb52f0fe1d559cd2869db7f29753e8951381b4a3/1 const splits = url.split('/'); const id = splits.pop(); const contract = splits.pop(); From 438b93a05110f05ff6207039cdb76c802949029e Mon Sep 17 00:00:00 2001 From: Peter Date: Sun, 24 Nov 2024 12:04:09 +0200 Subject: [PATCH 11/11] chore: add comments --- src/ingestors/vv/index.ts | 6 ++++-- src/ingestors/vv/onchain-metadata.ts | 12 ++++++++++++ 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/src/ingestors/vv/index.ts b/src/ingestors/vv/index.ts index bce39d8..7dea584 100644 --- a/src/ingestors/vv/index.ts +++ b/src/ingestors/vv/index.ts @@ -57,6 +57,8 @@ export class VvIngestor implements MintIngestor { } const { contractAddress } = contractOptions; + + // Use latestTokenId as default, see: https://docs.mint.vv.xyz/guide/contracts/mint#token-count const tokenId = contractOptions.tokenId ?? (await getVvLatestTokenId(resources.alchemy, contractAddress)); const collection = await getVvCollection(resources.alchemy, contractAddress, tokenId ? +tokenId : undefined); @@ -72,7 +74,7 @@ export class VvIngestor implements MintIngestor { } mintBuilder.setName(metadata.name).setDescription(metadata.description).setFeaturedImageUrl(metadata.image); - mintBuilder.setMintOutputContract({ chainId: 1, address: contractAddress }); + mintBuilder.setMintOutputContract({ chainId: contractOptions.chainId ?? 1, address: contractAddress }); const creatorData = await getVvCollectionCreator(resources.alchemy, contractAddress); @@ -96,7 +98,7 @@ export class VvIngestor implements MintIngestor { } mintBuilder.setMintInstructions({ - chainId: 8453, + chainId: contractOptions.chainId ?? 1, contractAddress, contractMethod: 'mint', contractParams: `[${tokenId}, quantity]`, diff --git a/src/ingestors/vv/onchain-metadata.ts b/src/ingestors/vv/onchain-metadata.ts index 88c4b8a..dc70928 100644 --- a/src/ingestors/vv/onchain-metadata.ts +++ b/src/ingestors/vv/onchain-metadata.ts @@ -29,6 +29,7 @@ export const getVvMintPriceInWei = async ( throw new Error('Unable to fetch baseFee'); } + // Mint price is calculated as baseFee * 60_000, see: https://docs.mint.vv.xyz/guide/contracts/mint#purchasing-tokens return `${+baseFeePerGas * 60000}`; } catch (error) {} }; @@ -44,6 +45,7 @@ export const getVvLatestTokenId = async (alchemy: Alchemy, contractAddress: stri export const getVvCollection = async (alchemy: Alchemy, contractAddress: string, vectorId?: number) => { try { const { contract } = await getContract(alchemy, contractAddress); + // Use latestTokenId as default, see: https://docs.mint.vv.xyz/guide/contracts/mint#token-count const collection = await contract.functions.get(vectorId ?? (await getVvLatestTokenId(alchemy, contractAddress))); const { name, description, artifact, renderer, mintedBlock, closeAt, data } = collection; return { name, description, artifact, renderer, mintedBlock, closeAt, data }; @@ -54,10 +56,17 @@ export const getVvCollectionMetadata = async (alchemy: Alchemy, contractAddress: try { const { contract } = await getContract(alchemy, contractAddress); const uri = await contract.functions.contractURI(); + + // Decode base64 const rawContent = uri[0].split(',')[1]; let jsonString = atob(rawContent); + const { name, symbol, description, image: imageBase64 } = JSON.parse(jsonString); + + // Decode again image const rawImage = imageBase64.split(',')[1]; + + // Image is stored as a svg in the contract const image = atob(rawImage); return { name, symbol, description, image }; @@ -68,7 +77,10 @@ export const getVvCollectionCreator = async (alchemy: Alchemy, contractAddress: try { const { contract, provider } = await getContract(alchemy, contractAddress); const owner = await contract.functions.owner(); + + // Lookup ens name for creator const name = await provider.lookupAddress(owner[0]); + return { creator: owner[0], name,