async def update_collection(address: str): requester = Requester() awsRequester = AwsRequester(accessKeyId=os.environ['AWS_KEY'], accessKeySecret=os.environ['AWS_SECRET']) ethClient = RestEthClient(url='https://nd-foldvvlb25awde7kbqfvpgvrrm.ethereum.managedblockchain.eu-west-1.amazonaws.com', requester=awsRequester) with open('./contracts/IERC721.json') as contractJsonFile: erc721ContractJson = json.load(contractJsonFile) erc721ContractAbi = erc721ContractJson['abi'] with open('./contracts/IERC721Metadata.json') as contractJsonFile: erc721MetadataContractJson = json.load(contractJsonFile) erc721MetadataContractAbi = erc721MetadataContractJson['abi'] with open('./contracts/IERC721Enumerable.json') as contractJsonFile: erc721EnumerableContractJson = json.load(contractJsonFile) erc721EnumerableContractAbi = erc721EnumerableContractJson['abi'] erc721TotalSupplyFunctionAbi = [internalAbi for internalAbi in erc721EnumerableContractAbi if internalAbi.get('name') == 'totalSupply'][0] erc721MetadataUriFunctionAbi = [internalAbi for internalAbi in erc721MetadataContractAbi if internalAbi.get('name') == 'tokenURI'][0] totalSupply = (await ethClient.call_function(toAddress=address, contractAbi=erc721EnumerableContractAbi, functionAbi=erc721TotalSupplyFunctionAbi))[0] for tokenId in range(int(totalSupply)): try: await ethClient.call_function(toAddress=address, contractAbi=erc721MetadataContractAbi, functionAbi=erc721MetadataUriFunctionAbi, arguments={'tokenId': int(tokenId)}) except BadRequestException: continue await requester.get(url=f'https://api.opensea.io/api/v1/asset/${address}/${tokenId}?force_update=true', headers={'X-API-KEY': os.environ['OPENSEA_API_KEY']}) await awsRequester.close_connections()
async def process_tokens_from_old_transfers(startBlockNumber: int, endBlockNumber: int, batchSize: int): databaseConnectionString = Database.create_psql_connection_string(username=os.environ["DB_USERNAME"], password=os.environ["DB_PASSWORD"], host=os.environ["DB_HOST"], port=os.environ["DB_PORT"], name=os.environ["DB_NAME"]) database = Database(connectionString=databaseConnectionString) saver = Saver(database=database) retriever = Retriever(database=database) s3manager = S3Manager(region='eu-west-1', accessKeyId=os.environ['AWS_KEY'], accessKeySecret=os.environ['AWS_SECRET']) workQueue = SqsMessageQueue(region='eu-west-1', accessKeyId=os.environ['AWS_KEY'], accessKeySecret=os.environ['AWS_SECRET'], queueUrl='https://sqs.eu-west-1.amazonaws.com/097520841056/notd-work-queue') tokenQueue = SqsMessageQueue(region='eu-west-1', accessKeyId=os.environ['AWS_KEY'], accessKeySecret=os.environ['AWS_SECRET'], queueUrl='https://sqs.eu-west-1.amazonaws.com/097520841056/notd-token-queue') awsRequester = AwsRequester(accessKeyId=os.environ['AWS_KEY'], accessKeySecret=os.environ['AWS_SECRET']) ethClient = RestEthClient(url='https://nd-foldvvlb25awde7kbqfvpgvrrm.ethereum.managedblockchain.eu-west-1.amazonaws.com', requester=awsRequester) requester = Requester() tokenMetadataProcessor = TokenMetadataProcessor(requester=requester, ethClient=ethClient, s3manager=s3manager, bucketName=os.environ['S3_BUCKET']) openseaApiKey = os.environ['OPENSEA_API_KEY'] tokenOwnershipProcessor = TokenOwnershipProcessor(retriever=retriever) collectionProcessor = CollectionProcessor(requester=requester, ethClient=ethClient, openseaApiKey=openseaApiKey, s3manager=s3manager, bucketName=os.environ['S3_BUCKET']) tokenManager = TokenManager(saver=saver, retriever=retriever, tokenQueue=tokenQueue, collectionProcessor=collectionProcessor, tokenMetadataProcessor=tokenMetadataProcessor, tokenOwnershipProcessor=tokenOwnershipProcessor) revueApiKey = os.environ['REVUE_API_KEY'] await database.connect() await workQueue.connect() await s3manager.connect() await tokenQueue.connect() cache = set() registryCache = set() currentBlockNumber = startBlockNumber while currentBlockNumber < endBlockNumber: start = currentBlockNumber end = min(currentBlockNumber + batchSize, endBlockNumber) currentBlockNumber = end logging.info(f'Working on {start}-{end}...') query = ( sqlalchemy.select(TokenTransfersTable.c.registryAddress, TokenTransfersTable.c.tokenId) .where(TokenTransfersTable.c.blockNumber >= start) .where(TokenTransfersTable.c.blockNumber < end) ) result = await database.execute(query=query,) tokensToProcess = set() collectionsToProcess = set() for (registryAddress, tokenId) in result: if (registryAddress, tokenId) in cache: continue cache.add((registryAddress, tokenId)) tokensToProcess.add((registryAddress, tokenId)) if registryAddress in registryCache: continue registryCache.add(registryAddress) collectionsToProcess.add(registryAddress) print('len(tokensToProcess)', len(tokensToProcess)) print('len(collectionsToProcess)', len(collectionsToProcess)) try: await _update_token_metadatas(tokensToProcess=tokensToProcess, tokenManager=tokenManager, retriever=retriever) await _update_collections(collectionsToProcess=collectionsToProcess, tokenManager=tokenManager, retriever=retriever) except: logging.error(f'Failed during: {start}-{end}') raise await database.disconnect() await workQueue.disconnect() await tokenQueue.disconnect() await s3manager.disconnect()
async def main(): requestIdHolder = RequestIdHolder() name = os.environ.get('NAME', 'notd-api') version = os.environ.get('VERSION', 'local') environment = os.environ.get('ENV', 'dev') isRunningDebugMode = environment == 'dev' if isRunningDebugMode: logging.init_basic_logging() else: logging.init_json_logging(name=name, version=version, environment=environment, requestIdHolder=requestIdHolder) databaseConnectionString = Database.create_psql_connection_string(username=os.environ["DB_USERNAME"], password=os.environ["DB_PASSWORD"], host=os.environ["DB_HOST"], port=os.environ["DB_PORT"], name=os.environ["DB_NAME"]) database = Database(connectionString=databaseConnectionString) saver = Saver(database=database) retriever = Retriever(database=database) s3manager = S3Manager(region='eu-west-1', accessKeyId=os.environ['AWS_KEY'], accessKeySecret=os.environ['AWS_SECRET']) workQueue = SqsMessageQueue(region='eu-west-1', accessKeyId=os.environ['AWS_KEY'], accessKeySecret=os.environ['AWS_SECRET'], queueUrl='https://sqs.eu-west-1.amazonaws.com/097520841056/notd-work-queue') tokenQueue = SqsMessageQueue(region='eu-west-1', accessKeyId=os.environ['AWS_KEY'], accessKeySecret=os.environ['AWS_SECRET'], queueUrl='https://sqs.eu-west-1.amazonaws.com/097520841056/notd-token-queue') awsRequester = AwsRequester(accessKeyId=os.environ['AWS_KEY'], accessKeySecret=os.environ['AWS_SECRET']) ethClient = RestEthClient(url='https://nd-foldvvlb25awde7kbqfvpgvrrm.ethereum.managedblockchain.eu-west-1.amazonaws.com', requester=awsRequester) blockProcessor = BlockProcessor(ethClient=ethClient) requester = Requester() tokenMetadataProcessor = TokenMetadataProcessor(requester=requester, ethClient=ethClient, s3manager=s3manager, bucketName=os.environ['S3_BUCKET']) openseaApiKey = os.environ['OPENSEA_API_KEY'] collectionProcessor = CollectionProcessor(requester=requester, ethClient=ethClient, openseaApiKey=openseaApiKey, s3manager=s3manager, bucketName=os.environ['S3_BUCKET']) tokenOwnershipProcessor = TokenOwnershipProcessor(retriever=retriever) collectionActivityProcessor = CollectionActivityProcessor(retriever=retriever) revueApiKey = os.environ['REVUE_API_KEY'] tokenManager = TokenManager(saver=saver, retriever=retriever, tokenQueue=tokenQueue, collectionProcessor=collectionProcessor, tokenMetadataProcessor=tokenMetadataProcessor, tokenOwnershipProcessor=tokenOwnershipProcessor, collectionActivityProcessor=collectionActivityProcessor) notdManager = NotdManager(blockProcessor=blockProcessor, saver=saver, retriever=retriever, workQueue=workQueue, tokenManager=tokenManager, requester=requester, revueApiKey=revueApiKey) processor = NotdMessageProcessor(notdManager=notdManager) slackClient = SlackClient(webhookUrl=os.environ['SLACK_WEBHOOK_URL'], requester=requester, defaultSender='worker', defaultChannel='notd-notifications') workQueueProcessor = MessageQueueProcessor(queue=workQueue, messageProcessor=processor, slackClient=slackClient, requestIdHolder=requestIdHolder) tokenQueueProcessor = MessageQueueProcessor(queue=tokenQueue, messageProcessor=processor, slackClient=slackClient, requestIdHolder=requestIdHolder) await database.connect() await s3manager.connect() await workQueue.connect() await tokenQueue.connect() try: while True: hasProcessedWork = await workQueueProcessor.execute_batch(batchSize=3, longPollSeconds=1, shouldProcessInParallel=True) if hasProcessedWork: continue hasProcessedToken = await tokenQueueProcessor.execute_batch(batchSize=10, longPollSeconds=1, shouldProcessInParallel=True) if hasProcessedToken: continue logging.info('No message received.. sleeping') time.sleep(60) finally: await database.disconnect() await s3manager.disconnect() await workQueue.disconnect() await tokenQueue.disconnect() await requester.close_connections()
async def process_token_ownerships(startTokenId: int, endTokenId: int, batchSize: int): databaseConnectionString = Database.create_psql_connection_string(username=os.environ["DB_USERNAME"], password=os.environ["DB_PASSWORD"], host=os.environ["DB_HOST"], port=os.environ["DB_PORT"], name=os.environ["DB_NAME"]) database = Database(connectionString=databaseConnectionString) saver = Saver(database=database) retriever = Retriever(database=database) s3manager = S3Manager(region='eu-west-1', accessKeyId=os.environ['AWS_KEY'], accessKeySecret=os.environ['AWS_SECRET']) workQueue = SqsMessageQueue(region='eu-west-1', accessKeyId=os.environ['AWS_KEY'], accessKeySecret=os.environ['AWS_SECRET'], queueUrl='https://sqs.eu-west-1.amazonaws.com/097520841056/notd-work-queue') tokenQueue = SqsMessageQueue(region='eu-west-1', accessKeyId=os.environ['AWS_KEY'], accessKeySecret=os.environ['AWS_SECRET'], queueUrl='https://sqs.eu-west-1.amazonaws.com/097520841056/notd-token-queue') awsRequester = AwsRequester(accessKeyId=os.environ['AWS_KEY'], accessKeySecret=os.environ['AWS_SECRET']) ethClient = RestEthClient(url='https://nd-foldvvlb25awde7kbqfvpgvrrm.ethereum.managedblockchain.eu-west-1.amazonaws.com', requester=awsRequester) requester = Requester() tokenMetadataProcessor = TokenMetadataProcessor(requester=requester, ethClient=ethClient, s3manager=s3manager, bucketName=os.environ['S3_BUCKET']) openseaApiKey = os.environ['OPENSEA_API_KEY'] tokenOwnershipProcessor = TokenOwnershipProcessor(retriever=retriever) collectionProcessor = CollectionProcessor(requester=requester, ethClient=ethClient, openseaApiKey=openseaApiKey, s3manager=s3manager, bucketName=os.environ['S3_BUCKET']) tokenManager = TokenManager(saver=saver, retriever=retriever, tokenQueue=tokenQueue, collectionProcessor=collectionProcessor, tokenMetadataProcessor=tokenMetadataProcessor, tokenOwnershipProcessor=tokenOwnershipProcessor) revueApiKey = os.environ['REVUE_API_KEY'] slackClient = SlackClient(webhookUrl=os.environ['SLACK_WEBHOOK_URL'], requester=requester, defaultSender='worker', defaultChannel='notd-notifications') await database.connect() await workQueue.connect() await s3manager.connect() await tokenQueue.connect() await database.connect() await slackClient.post(text=f'process_token_ownerships → 🚧 started: {startTokenId}-{endTokenId}') try: currentTokenId = startTokenId while currentTokenId < endTokenId: start = currentTokenId end = min(currentTokenId + batchSize, endTokenId) currentTokenId = end logging.info(f'Working on {start}-{end}') query = TokenMetadatasTable.select() \ .where(TokenMetadatasTable.c.tokenMetadataId >= start) \ .where(TokenMetadatasTable.c.tokenMetadataId < end) tokenMetadatas = await retriever.query_token_metadatas(query=query) await asyncio.gather(*[process_token_ownership(tokenManager=tokenManager, registryAddress=tokenMetadata.registryAddress, tokenId=tokenMetadata.tokenId) for tokenMetadata in tokenMetadatas]) await slackClient.post(text=f'process_token_ownerships → ✅ completed : {startTokenId}-{endTokenId}') except Exception as exception: await slackClient.post(text=f'process_token_ownerships → � error: {startTokenId}-{endTokenId}\n```{str(exception)}```') raise exception finally: await database.disconnect() await workQueue.disconnect() await tokenQueue.disconnect() await s3manager.disconnect()
async def process_collection(address: str, shouldDefer: bool): databaseConnectionString = Database.create_psql_connection_string(username=os.environ["DB_USERNAME"], password=os.environ["DB_PASSWORD"], host=os.environ["DB_HOST"], port=os.environ["DB_PORT"], name=os.environ["DB_NAME"]) database = Database(connectionString=databaseConnectionString) saver = Saver(database=database) retriever = Retriever(database=database) s3manager = S3Manager(region='eu-west-1', accessKeyId=os.environ['AWS_KEY'], accessKeySecret=os.environ['AWS_SECRET']) workQueue = SqsMessageQueue(region='eu-west-1', accessKeyId=os.environ['AWS_KEY'], accessKeySecret=os.environ['AWS_SECRET'], queueUrl='https://sqs.eu-west-1.amazonaws.com/097520841056/notd-work-queue') tokenQueue = SqsMessageQueue(region='eu-west-1', accessKeyId=os.environ['AWS_KEY'], accessKeySecret=os.environ['AWS_SECRET'], queueUrl='https://sqs.eu-west-1.amazonaws.com/097520841056/notd-token-queue') requester = Requester() awsRequester = AwsRequester(accessKeyId=os.environ['AWS_KEY'], accessKeySecret=os.environ['AWS_SECRET']) ethClient = RestEthClient(url='https://nd-foldvvlb25awde7kbqfvpgvrrm.ethereum.managedblockchain.eu-west-1.amazonaws.com', requester=awsRequester) blockProcessor = BlockProcessor(ethClient=ethClient) tokenMetadataProcessor = TokenMetadataProcessor(requester=requester, ethClient=ethClient, s3manager=s3manager, bucketName=os.environ['S3_BUCKET']) openseaApiKey = os.environ['OPENSEA_API_KEY'] collectionProcessor = CollectionProcessor(requester=requester, ethClient=ethClient, openseaApiKey=openseaApiKey, s3manager=s3manager, bucketName=os.environ['S3_BUCKET']) revueApiKey = os.environ['REVUE_API_KEY'] tokenManager = TokenManager(saver=saver, retriever=retriever, tokenQueue=tokenQueue, collectionProcessor=collectionProcessor, tokenMetadataProcessor=tokenMetadataProcessor) notdManager = NotdManager(blockProcessor=blockProcessor, saver=saver, retriever=retriever, workQueue=workQueue, tokenManager=tokenManager, requester=requester, revueApiKey=revueApiKey) await database.connect() await s3manager.connect() await workQueue.connect() await tokenQueue.connect() retrievedCollectionTokenMetadatas = await retriever.list_token_metadatas( fieldFilters=[ StringFieldFilter(fieldName=TokenTransfersTable.c.registryAddress.key, eq=address), ], ) for tokenMetadata in retrievedCollectionTokenMetadatas: if shouldDefer: await notdManager.update_token_metadata_deferred(registryAddress=address, tokenId=tokenMetadata.tokenId, shouldForce=True) else: await notdManager.update_token_metadata(registryAddress=address, tokenId=tokenMetadata.tokenId, shouldForce=True) await database.disconnect() await s3manager.disconnect() await workQueue.disconnect() await tokenQueue.disconnect() await requester.close_connections()
async def main(): s3manager = S3Manager(region='eu-west-1', accessKeyId=os.environ['AWS_KEY'], accessKeySecret=os.environ['AWS_SECRET']) requester = Requester() ethClient = RestEthClient( url=f'https://mainnet.infura.io/v3/{os.environ["INFURA_PROJECT_ID"]}', requester=requester) blockProcessor = BlockProcessor(ethClient=ethClient) requester = Requester() tokenMetadataProcessor = TokenMetadataProcessor( requester=requester, ethClient=ethClient, s3manager=s3manager, bucketName=os.environ['S3_BUCKET']) await s3manager.connect() result = tokenMetadataProcessor.get_default_token_metadata( registryAddress='0x57E9a39aE8eC404C08f88740A9e6E306f50c937f', tokenId=165) expected = RetrievedTokenMetadata( registryAddress='0x57E9a39aE8eC404C08f88740A9e6E306f50c937f', tokenId='165', metadataUrl=None, imageUrl=None, animationUrl=None, youtubeUrl=None, backgroundColor=None, name='#165', description=None, frameImageUrl=None, attributes=[]) assert (result == expected) collection = Collection( address='0x495f947276749Ce646f68AC8c248420045cb7b5e', name='OpenSea Shared Storefront', symbol='OPENSTORE', description=None, imageUrl=None, twitterUsername=None, instagramUsername=None, wikiUrl=None, openseaSlug=None, url=None, discordUrl=None, bannerImageUrl=None, doesSupportErc721=False, doesSupportErc1155=True, collectionId=9, createdDate=datetime.datetime(2022, 2, 10, 15, 35, 27, 493515), updatedDate=datetime.datetime(2022, 3, 10, 11, 20, 43, 69095)) result = await tokenMetadataProcessor.retrieve_token_metadata( registryAddress='0x495f947276749Ce646f68AC8c248420045cb7b5e', tokenId= '1027194722486925029699656604254821663009080914619475021175016778814206246913', collection=collection) expected = RetrievedTokenMetadata( registryAddress='0x495f947276749Ce646f68AC8c248420045cb7b5e', tokenId= '1027194722486925029699656604254821663009080914619475021175016778814206246913', metadataUrl= 'https://api.opensea.io/api/v1/metadata/0x495f947276749Ce646f68AC8c248420045cb7b5e/1027194722486925029699656604254821663009080914619475021175016778814206246913', name='LegacyPunks #117 Richard the Lionheart', description= 'Richard the Lionheart (1157-1199 AD), actually Richard I of England - but more commonly known as Lionheart due to his reputation as great military leader and warrior, was King of England from 1189 until his death in 1199 AD. Richard spent the majority of his life at war and eventually died in battle. He also ruled as Duke of Normandy, Aquitaine and Gascony, Lord of Cyprus, and Count of Poitiers, Anjou, Maine, and Nantes, and was overlord of Brittany at various times during the same period. Richard was a popular ruler and is perhaps best known for his attempt to retake Jerusalem from Saladin during the Third Crusade. Although, this Crusade did not successfully recapture Jerusalem, it achieved partial success in recapturing Acre, Jaffa, and reversing most of Saladin’s conquests. Regardless, Richard is much considered the winner of the Third Crusade after his famous victory at the Battle of Arsuf.', imageUrl= 'https://lh3.googleusercontent.com/3u7F2lRRhj9y4dd2-o9R8sfU7dYW6UQN4Vc4BOoH1JyrT5cAHPDF40Q2aPycYn1ZDoin3CnCVRgD2yHe82KYlmVX8Lbbw_YaFp1YyQ', animationUrl=None, youtubeUrl=None, backgroundColor=None, frameImageUrl=None, attributes=[]) assert (result == expected) collection = Collection( address='0x93317e87a3a47821803caadc54ae418af80603da', name='Cameo Pass', symbol='CAMEOPASS', description= 'Cameo Pass is a collection of NFTs granting access to Cameo in the metaverse,...', imageUrl= 'https://openseauserdata.com/files/7572aef06110b4e365817f60d37eff0d.svg', twitterUsername=None, instagramUsername=None, wikiUrl=None, openseaSlug='cameo-pass', url='http://pass.cameo.com', discordUrl='https://discord.gg/ssZeP6WBYg', bannerImageUrl= 'https://lh3.googleusercontent.com/HiSZHoavhCSzrmc2Un2y6QzSWk_wAWLz4sBr9G1Wjz6bGH8mDce_8rNlH-vRvM1xwbqS-_pSCsK99rrr8P-Es4FCkjvxETij8lojNw=s2500', doesSupportErc721=False, doesSupportErc1155=True, collectionId=64040, createdDate=datetime.datetime(2022, 4, 2, 21, 37, 1, 727907), updatedDate=datetime.datetime(2022, 4, 2, 21, 37, 1, 727907)) result = await tokenMetadataProcessor.retrieve_token_metadata( registryAddress='0x93317e87a3a47821803caadc54ae418af80603da', tokenId='2', collection=collection) expected = RetrievedTokenMetadata( registryAddress='0x93317e87a3a47821803caadc54ae418af80603da', tokenId='2', metadataUrl= 'ipfs://Qmbitpu7WNj76fhArU2yU2wqQbTnW7ASNJcm8Kf4F4GzL1/0000000000000000000000000000000000000000000000000000000000000002.json', name='gn', description='gn. Art by Vinnie Hager.', imageUrl='ipfs://QmQuqAAahKUw6BHXbEYAvz9N8UTaHPH5X1iK18LaLdUSqo/2.gif', animationUrl=None, youtubeUrl=None, backgroundColor=None, frameImageUrl=None, attributes=[]) assert (result == expected) collection = Collection( address='0x97fB6806AcbA833c5Ca80135D7d75BF3794b9df7', name='Lifetime Pass', symbol='LP', description= "The Lifetime Pass gives users lifetime access to Astroworld's Virtual Theme Park....", imageUrl= 'https://gateway.pinata.cloud/ipfs/QmewxBAuXkvkV2YcskXK7GBcspX7MuMvNdMjbiRBNbekdf/', twitterUsername=None, instagramUsername=None, wikiUrl=None, openseaSlug='astroworld-lifetime-pass-v2', url=None, discordUrl=None, bannerImageUrl=None, doesSupportErc721=True, doesSupportErc1155=False, collectionId=65389, createdDate=datetime.datetime(2022, 4, 7, 9, 24, 30, 369275), updatedDate=datetime.datetime(2022, 4, 7, 9, 24, 30, 369275)) result = await tokenMetadataProcessor.retrieve_token_metadata( registryAddress='0x97fB6806AcbA833c5Ca80135D7d75BF3794b9df7', tokenId='2', collection=collection) expected = RetrievedTokenMetadata( registryAddress='0x97fB6806AcbA833c5Ca80135D7d75BF3794b9df7', tokenId='2', metadataUrl='ipfs://QmQNWCbw2SDPt21qs4gENSAhbxMYDrmf8ur6pxKz8Ra1MZ', name='Astroworld Lifetime Pass', description= "The Lifetime Pass gives users lifetime access to Astroworld's Virtual Theme Park. This NFT must be in your wallet at the time of each entry. Additionally, Astroworld LP holders will have exclusive access to Astroworld BETA and Astroworld related events that take place in Astroworld or IRL.", imageUrl= 'https://gateway.pinata.cloud/ipfs/QmewxBAuXkvkV2YcskXK7GBcspX7MuMvNdMjbiRBNbekdf/', animationUrl=None, youtubeUrl=None, backgroundColor=None, frameImageUrl=None, attributes=[]) assert (result == expected) collection = Collection( address='0x4581649aF66BCCAeE81eebaE3DDc0511FE4C5312', name='TheAlienBoy', symbol='TABOY', description= 'Each Alien Boy is unique and algorithmically generated by combining 182 properties ...', imageUrl= 'https://lh3.googleusercontent.com/mnNWlPXlDTp8N1JP2kiCrTy-w0lGU0S1AX5sMm0ZhWjSTpIh4RTwXERNjz83aKI1uKjyqf8yZJlsId6TeUgvJRhwaWScJPdWZSlISJc=s120', twitterUsername='******', instagramUsername=None, wikiUrl=None, openseaSlug='thealienboy', url='https://www.thealienboy.com/', discordUrl='https://discord.gg/thealienboy', bannerImageUrl= 'https://lh3.googleusercontent.com/bWgbzHdOrgOtpDUm0LTIo4eH4JjzeDUsTce4qA3mrBkGw-wXYnWDQVxaKXBG28Y2kQ634cAFBYEfsBMpqggMirS9Vu6GKxAOeKnqJ00=s2500', doesSupportErc721=True, doesSupportErc1155=False, collectionId=40, createdDate=datetime.datetime(2022, 1, 31, 12, 23, 58, 978642), updatedDate=datetime.datetime(2022, 1, 31, 12, 23, 58, 978642)) result = await tokenMetadataProcessor.retrieve_token_metadata( registryAddress='0x4581649aF66BCCAeE81eebaE3DDc0511FE4C5312', tokenId='6574', collection=collection) expected = RetrievedTokenMetadata( registryAddress='0x4581649aF66BCCAeE81eebaE3DDc0511FE4C5312', tokenId='6574', metadataUrl='https://api.thealienboy.com/metadata/6574', imageUrl= 'https://gateway.pinata.cloud/ipfs/Qmburch4fp76L6oTfDjZF4Ybk3SzpFbbU52xjN8x42rVTS', animationUrl=None, youtubeUrl=None, backgroundColor=None, name='Alien Boy 6574', description= 'your Alien Boy has a unique DNA. yours is #0804080734090200350000 \nkeep it safe boy, you may need it someday.', frameImageUrl=None, attributes=[{ 'trait_type': 'background', 'value': 'Tiger Orange' }, { 'trait_type': 'skin', 'value': 'Red' }, { 'trait_type': 'face', 'value': 'Scratch' }, { 'trait_type': 'eyes', 'value': 'Bloodshot' }, { 'trait_type': 'body', 'value': 'Miami Suit' }, { 'trait_type': 'mouth', 'value': 'Slime' }, { 'trait_type': 'accessory', 'value': 'Neon Sunglasses' }, { 'trait_type': 'head', 'value': 'Stickers' }, { 'trait_type': 'first_encounter', 'value': '1623776953', 'display_type': 'date' }]) assert (result == expected) collection = Collection( address='0xC36442b4a4522E871399CD717aBDD847Ab11FE88', name='Uniswap V3 Positions NFT-V1', symbol='UNI-V3-POS', description='', imageUrl= 'https://lh3.googleusercontent.com/8My7rmWHJCAi35QSp2IMf50NRNIRJelpEdassqIxiL6Q_m2SE2qG7QKIT_1wfXH2afDcACiWVGrt2jkfHqHKlElttUTdL6dmC9G0Mpk=s120', twitterUsername='******', instagramUsername=None, wikiUrl=None, openseaSlug='uniswap-v3-positions', url='https://uniswap.org/', discordUrl='https://discord.gg/FCfyBSbCU5', bannerImageUrl= 'https://lh3.googleusercontent.com/Xq98abLTjlFfzdIxsXNL0sVE2W-3FGcJ2TFUkphz9dh9wEH4rcUesMhE7RzEh_ivPCdL5KxkNVfyE5gb870OgqOLQnBP6sIL54-G0A=s2500', doesSupportErc721=True, doesSupportErc1155=False, collectionId=26, createdDate=datetime.datetime(2022, 1, 31, 0, 24, 59, 737286), updatedDate=datetime.datetime(2022, 1, 31, 0, 24, 59, 737286)) result = await tokenMetadataProcessor.retrieve_token_metadata( registryAddress='0xC36442b4a4522E871399CD717aBDD847Ab11FE88', tokenId='48820', collection=collection) expected = RetrievedTokenMetadata( registryAddress='0xC36442b4a4522E871399CD717aBDD847Ab11FE88', tokenId='48820', metadataUrl= 'data:application/json;base64,eyJuYW1lIjoiVW5pc3dhcCAtIDAuMyUgLSBVTkkvV0VUSCAtIDc2Ljk5ODw+MTE4LjYwIiwgImRlc2NyaXB0aW9uIjoiVGhpcyBORlQgcmVwcmVzZW50cyBhIGxpcXVpZGl0eSBwb3NpdGlvbiBpbiBhIFVuaXN3YXAgVjMgVU5JLVdFVEggcG9vbC4gVGhlIG93bmVyIG9mIHRoaXMgTkZUIGNhbiBtb2RpZnkgb3IgcmVkZWVtIHRoZSBwb3NpdGlvbi5cblxuUG9vbCBBZGRyZXNzOiAweDFkNDIwNjRmYzRiZWI1ZjhhYWY4NWY0NjE3YWU4YjNiNWI4YmQ4MDFcblVOSSBBZGRyZXNzOiAweDFmOTg0MGE4NWQ1YWY1YmYxZDE3NjJmOTI1YmRhZGRjNDIwMWY5ODRcbldFVEggQWRkcmVzczogMHhjMDJhYWEzOWIyMjNmZThkMGEwZTVjNGYyN2VhZDkwODNjNzU2Y2MyXG5GZWUgVGllcjogMC4zJVxuVG9rZW4gSUQ6IDQ4ODIwXG5cbuKaoO+4jyBESVNDTEFJTUVSOiBEdWUgZGlsaWdlbmNlIGlzIGltcGVyYXRpdmUgd2hlbiBhc3Nlc3NpbmcgdGhpcyBORlQuIE1ha2Ugc3VyZSB0b2tlbiBhZGRyZXNzZXMgbWF0Y2ggdGhlIGV4cGVjdGVkIHRva2VucywgYXMgdG9rZW4gc3ltYm9scyBtYXkgYmUgaW1pdGF0ZWQuIiwgImltYWdlIjogImRhdGE6aW1hZ2Uvc3ZnK3htbDtiYXNlNjQsUEhOMlp5QjNhV1IwYUQwaU1qa3dJaUJvWldsbmFIUTlJalV3TUNJZ2RtbGxkMEp2ZUQwaU1DQXdJREk1TUNBMU1EQWlJSGh0Ykc1elBTSm9kSFJ3T2k4dmQzZDNMbmN6TG05eVp5OHlNREF3TDNOMlp5SWdlRzFzYm5NNmVHeHBibXM5SjJoMGRIQTZMeTkzZDNjdWR6TXViM0puTHpFNU9Ua3ZlR3hwYm1zblBqeGtaV1p6UGp4bWFXeDBaWElnYVdROUltWXhJajQ4Wm1WSmJXRm5aU0J5WlhOMWJIUTlJbkF3SWlCNGJHbHVhenBvY21WbVBTSmtZWFJoT21sdFlXZGxMM04yWnl0NGJXdzdZbUZ6WlRZMExGQklUakphZVVJellWZFNNR0ZFTUc1TmFtdDNTbmxDYjFwWGJHNWhTRkU1U25wVmQwMURZMmRrYld4c1pEQktkbVZFTUc1TlEwRjNTVVJKTlUxRFFURk5SRUZ1U1Vob2RHSkhOWHBRVTJSdlpFaFNkMDlwT0haa00yUXpURzVqZWt4dE9YbGFlVGg1VFVSQmQwd3pUakphZVdNclVFaEtiRmt6VVdka01teHJaRWRuT1VwNlNUVk5TRUkwU25sQ2IxcFhiRzVoU0ZFNVNucFZkMDFJUWpSS2VVSnRZVmQ0YzFCVFkycE5WMWsxVDBSUmQwcDVPQ3RRUXpsNlpHMWpLeUl2UGp4bVpVbHRZV2RsSUhKbGMzVnNkRDBpY0RFaUlIaHNhVzVyT21oeVpXWTlJbVJoZEdFNmFXMWhaMlV2YzNabkszaHRiRHRpWVhObE5qUXNVRWhPTWxwNVFqTmhWMUl3WVVRd2JrMXFhM2RLZVVKdldsZHNibUZJVVRsS2VsVjNUVU5qWjJSdGJHeGtNRXAyWlVRd2JrMURRWGRKUkVrMVRVTkJNVTFFUVc1SlNHaDBZa2MxZWxCVFpHOWtTRkozVDJrNGRtUXpaRE5NYm1ONlRHMDVlVnA1T0hsTlJFRjNURE5PTWxwNVl5dFFSMDV3WTIxT2MxcFRRbXBsUkRCdVRWUk5lVXA1UW1wbFZEQnVUWHByZWtwNVFubFFVMk40VFdwQ2QyVkRZMmRhYld4ellrUXdia2t5VFhkTmJVWm9XVk5qZGxCcWQzWmpNMXB1VUdjOVBTSXZQanhtWlVsdFlXZGxJSEpsYzNWc2REMGljRElpSUhoc2FXNXJPbWh5WldZOUltUmhkR0U2YVcxaFoyVXZjM1puSzNodGJEdGlZWE5sTmpRc1VFaE9NbHA1UWpOaFYxSXdZVVF3YmsxcWEzZEtlVUp2V2xkc2JtRklVVGxLZWxWM1RVTmpaMlJ0Ykd4a01FcDJaVVF3YmsxRFFYZEpSRWsxVFVOQk1VMUVRVzVKU0doMFlrYzFlbEJUWkc5a1NGSjNUMms0ZG1RelpETk1ibU42VEcwNWVWcDVPSGxOUkVGM1RETk9NbHA1WXl0UVIwNXdZMjFPYzFwVFFtcGxSREJ1VG5wRmJrbEhUalZRVTJONlRYcE5ia2xJU1RsS2VrVjVUVWhDTkVwNVFtMWhWM2h6VUZOamFrMUVSbTFQVkdjd1NuazRLMUJET1hwa2JXTXJJaUF2UGp4bVpVbHRZV2RsSUhKbGMzVnNkRDBpY0RNaUlIaHNhVzVyT21oeVpXWTlJbVJoZEdFNmFXMWhaMlV2YzNabkszaHRiRHRpWVhObE5qUXNVRWhPTWxwNVFqTmhWMUl3WVVRd2JrMXFhM2RLZVVKdldsZHNibUZJVVRsS2VsVjNUVU5qWjJSdGJHeGtNRXAyWlVRd2JrMURRWGRKUkVrMVRVTkJNVTFFUVc1SlNHaDBZa2MxZWxCVFpHOWtTRkozVDJrNGRtUXpaRE5NYm1ONlRHMDVlVnA1T0hsTlJFRjNURE5PTWxwNVl5dFFSMDV3WTIxT2MxcFRRbXBsUkRCdVRucFpia2xIVGpWUVUyTjZUVVJOYmtsSVNUbEtla1YzVFVoQ05FcDVRbTFoVjNoelVGTmphazU2VlRKWk1rMTVTbms0SzFCRE9YcGtiV01ySWlBdlBqeG1aVUpzWlc1a0lHMXZaR1U5SW05MlpYSnNZWGtpSUdsdVBTSndNQ0lnYVc0eVBTSndNU0lnTHo0OFptVkNiR1Z1WkNCdGIyUmxQU0psZUdOc2RYTnBiMjRpSUdsdU1qMGljRElpSUM4K1BHWmxRbXhsYm1RZ2JXOWtaVDBpYjNabGNteGhlU0lnYVc0eVBTSndNeUlnY21WemRXeDBQU0ppYkdWdVpFOTFkQ0lnTHo0OFptVkhZWFZ6YzJsaGJrSnNkWElnYVc0OUltSnNaVzVrVDNWMElpQnpkR1JFWlhacFlYUnBiMjQ5SWpReUlpQXZQand2Wm1sc2RHVnlQaUE4WTJ4cGNGQmhkR2dnYVdROUltTnZjbTVsY25NaVBqeHlaV04wSUhkcFpIUm9QU0l5T1RBaUlHaGxhV2RvZEQwaU5UQXdJaUJ5ZUQwaU5ESWlJSEo1UFNJME1pSWdMejQ4TDJOc2FYQlFZWFJvUGp4d1lYUm9JR2xrUFNKMFpYaDBMWEJoZEdndFlTSWdaRDBpVFRRd0lERXlJRWd5TlRBZ1FUSTRJREk0SURBZ01DQXhJREkzT0NBME1DQldORFl3SUVFeU9DQXlPQ0F3SURBZ01TQXlOVEFnTkRnNElFZzBNQ0JCTWpnZ01qZ2dNQ0F3SURFZ01USWdORFl3SUZZME1DQkJNamdnTWpnZ01DQXdJREVnTkRBZ01USWdlaUlnTHo0OGNHRjBhQ0JwWkQwaWJXbHVhVzFoY0NJZ1pEMGlUVEl6TkNBME5EUkRNak0wSURRMU55NDVORGtnTWpReUxqSXhJRFEyTXlBeU5UTWdORFl6SWlBdlBqeG1hV3gwWlhJZ2FXUTlJblJ2Y0MxeVpXZHBiMjR0WW14MWNpSStQR1psUjJGMWMzTnBZVzVDYkhWeUlHbHVQU0pUYjNWeVkyVkhjbUZ3YUdsaklpQnpkR1JFWlhacFlYUnBiMjQ5SWpJMElpQXZQand2Wm1sc2RHVnlQanhzYVc1bFlYSkhjbUZrYVdWdWRDQnBaRDBpWjNKaFpDMTFjQ0lnZURFOUlqRWlJSGd5UFNJd0lpQjVNVDBpTVNJZ2VUSTlJakFpUGp4emRHOXdJRzltWm5ObGREMGlNQzR3SWlCemRHOXdMV052Ykc5eVBTSjNhR2wwWlNJZ2MzUnZjQzF2Y0dGamFYUjVQU0l4SWlBdlBqeHpkRzl3SUc5bVpuTmxkRDBpTGpraUlITjBiM0F0WTI5c2IzSTlJbmRvYVhSbElpQnpkRzl3TFc5d1lXTnBkSGs5SWpBaUlDOCtQQzlzYVc1bFlYSkhjbUZrYVdWdWRENDhiR2x1WldGeVIzSmhaR2xsYm5RZ2FXUTlJbWR5WVdRdFpHOTNiaUlnZURFOUlqQWlJSGd5UFNJeElpQjVNVDBpTUNJZ2VUSTlJakVpUGp4emRHOXdJRzltWm5ObGREMGlNQzR3SWlCemRHOXdMV052Ykc5eVBTSjNhR2wwWlNJZ2MzUnZjQzF2Y0dGamFYUjVQU0l4SWlBdlBqeHpkRzl3SUc5bVpuTmxkRDBpTUM0NUlpQnpkRzl3TFdOdmJHOXlQU0ozYUdsMFpTSWdjM1J2Y0MxdmNHRmphWFI1UFNJd0lpQXZQand2YkdsdVpXRnlSM0poWkdsbGJuUStQRzFoYzJzZ2FXUTlJbVpoWkdVdGRYQWlJRzFoYzJ0RGIyNTBaVzUwVlc1cGRITTlJbTlpYW1WamRFSnZkVzVrYVc1blFtOTRJajQ4Y21WamRDQjNhV1IwYUQwaU1TSWdhR1ZwWjJoMFBTSXhJaUJtYVd4c1BTSjFjbXdvSTJkeVlXUXRkWEFwSWlBdlBqd3ZiV0Z6YXo0OGJXRnpheUJwWkQwaVptRmtaUzFrYjNkdUlpQnRZWE5yUTI5dWRHVnVkRlZ1YVhSelBTSnZZbXBsWTNSQ2IzVnVaR2x1WjBKdmVDSStQSEpsWTNRZ2QybGtkR2c5SWpFaUlHaGxhV2RvZEQwaU1TSWdabWxzYkQwaWRYSnNLQ05uY21Ga0xXUnZkMjRwSWlBdlBqd3ZiV0Z6YXo0OGJXRnpheUJwWkQwaWJtOXVaU0lnYldGemEwTnZiblJsYm5SVmJtbDBjejBpYjJKcVpXTjBRbTkxYm1ScGJtZENiM2dpUGp4eVpXTjBJSGRwWkhSb1BTSXhJaUJvWldsbmFIUTlJakVpSUdacGJHdzlJbmRvYVhSbElpQXZQand2YldGemF6NDhiR2x1WldGeVIzSmhaR2xsYm5RZ2FXUTlJbWR5WVdRdGMzbHRZbTlzSWo0OGMzUnZjQ0J2Wm1aelpYUTlJakF1TnlJZ2MzUnZjQzFqYjJ4dmNqMGlkMmhwZEdVaUlITjBiM0F0YjNCaFkybDBlVDBpTVNJZ0x6NDhjM1J2Y0NCdlptWnpaWFE5SWk0NU5TSWdjM1J2Y0MxamIyeHZjajBpZDJocGRHVWlJSE4wYjNBdGIzQmhZMmwwZVQwaU1DSWdMejQ4TDJ4cGJtVmhja2R5WVdScFpXNTBQanh0WVhOcklHbGtQU0ptWVdSbExYTjViV0p2YkNJZ2JXRnphME52Ym5SbGJuUlZibWwwY3owaWRYTmxjbE53WVdObFQyNVZjMlVpUGp4eVpXTjBJSGRwWkhSb1BTSXlPVEJ3ZUNJZ2FHVnBaMmgwUFNJeU1EQndlQ0lnWm1sc2JEMGlkWEpzS0NObmNtRmtMWE41YldKdmJDa2lJQzgrUEM5dFlYTnJQand2WkdWbWN6NDhaeUJqYkdsd0xYQmhkR2c5SW5WeWJDZ2pZMjl5Ym1WeWN5a2lQanh5WldOMElHWnBiR3c5SWpGbU9UZzBNQ0lnZUQwaU1IQjRJaUI1UFNJd2NIZ2lJSGRwWkhSb1BTSXlPVEJ3ZUNJZ2FHVnBaMmgwUFNJMU1EQndlQ0lnTHo0OGNtVmpkQ0J6ZEhsc1pUMGlabWxzZEdWeU9pQjFjbXdvSTJZeEtTSWdlRDBpTUhCNElpQjVQU0l3Y0hnaUlIZHBaSFJvUFNJeU9UQndlQ0lnYUdWcFoyaDBQU0kxTURCd2VDSWdMejRnUEdjZ2MzUjViR1U5SW1acGJIUmxjanAxY213b0kzUnZjQzF5WldkcGIyNHRZbXgxY2lrN0lIUnlZVzV6Wm05eWJUcHpZMkZzWlNneExqVXBPeUIwY21GdWMyWnZjbTB0YjNKcFoybHVPbU5sYm5SbGNpQjBiM0E3SWo0OGNtVmpkQ0JtYVd4c1BTSnViMjVsSWlCNFBTSXdjSGdpSUhrOUlqQndlQ0lnZDJsa2RHZzlJakk1TUhCNElpQm9aV2xuYUhROUlqVXdNSEI0SWlBdlBqeGxiR3hwY0hObElHTjRQU0kxTUNVaUlHTjVQU0l3Y0hnaUlISjRQU0l4T0RCd2VDSWdjbms5SWpFeU1IQjRJaUJtYVd4c1BTSWpNREF3SWlCdmNHRmphWFI1UFNJd0xqZzFJaUF2UGp3dlp6NDhjbVZqZENCNFBTSXdJaUI1UFNJd0lpQjNhV1IwYUQwaU1qa3dJaUJvWldsbmFIUTlJalV3TUNJZ2NuZzlJalF5SWlCeWVUMGlORElpSUdacGJHdzlJbkpuWW1Fb01Dd3dMREFzTUNraUlITjBjbTlyWlQwaWNtZGlZU2d5TlRVc01qVTFMREkxTlN3d0xqSXBJaUF2UGp3dlp6NDhkR1Y0ZENCMFpYaDBMWEpsYm1SbGNtbHVaejBpYjNCMGFXMXBlbVZUY0dWbFpDSStQSFJsZUhSUVlYUm9JSE4wWVhKMFQyWm1jMlYwUFNJdE1UQXdKU0lnWm1sc2JEMGlkMmhwZEdVaUlHWnZiblF0Wm1GdGFXeDVQU0luUTI5MWNtbGxjaUJPWlhjbkxDQnRiMjV2YzNCaFkyVWlJR1p2Ym5RdGMybDZaVDBpTVRCd2VDSWdlR3hwYm1zNmFISmxaajBpSTNSbGVIUXRjR0YwYUMxaElqNHdlR013TW1GaFlUTTVZakl5TTJabE9HUXdZVEJsTldNMFpqSTNaV0ZrT1RBNE0yTTNOVFpqWXpJZzRvQ2lJRmRGVkVnZ1BHRnVhVzFoZEdVZ1lXUmthWFJwZG1VOUluTjFiU0lnWVhSMGNtbGlkWFJsVG1GdFpUMGljM1JoY25SUFptWnpaWFFpSUdaeWIyMDlJakFsSWlCMGJ6MGlNVEF3SlNJZ1ltVm5hVzQ5SWpCeklpQmtkWEk5SWpNd2N5SWdjbVZ3WldGMFEyOTFiblE5SW1sdVpHVm1hVzVwZEdVaUlDOCtQQzkwWlhoMFVHRjBhRDRnUEhSbGVIUlFZWFJvSUhOMFlYSjBUMlptYzJWMFBTSXdKU0lnWm1sc2JEMGlkMmhwZEdVaUlHWnZiblF0Wm1GdGFXeDVQU0luUTI5MWNtbGxjaUJPWlhjbkxDQnRiMjV2YzNCaFkyVWlJR1p2Ym5RdGMybDZaVDBpTVRCd2VDSWdlR3hwYm1zNmFISmxaajBpSTNSbGVIUXRjR0YwYUMxaElqNHdlR013TW1GaFlUTTVZakl5TTJabE9HUXdZVEJsTldNMFpqSTNaV0ZrT1RBNE0yTTNOVFpqWXpJZzRvQ2lJRmRGVkVnZ1BHRnVhVzFoZEdVZ1lXUmthWFJwZG1VOUluTjFiU0lnWVhSMGNtbGlkWFJsVG1GdFpUMGljM1JoY25SUFptWnpaWFFpSUdaeWIyMDlJakFsSWlCMGJ6MGlNVEF3SlNJZ1ltVm5hVzQ5SWpCeklpQmtkWEk5SWpNd2N5SWdjbVZ3WldGMFEyOTFiblE5SW1sdVpHVm1hVzVwZEdVaUlDOCtJRHd2ZEdWNGRGQmhkR2crUEhSbGVIUlFZWFJvSUhOMFlYSjBUMlptYzJWMFBTSTFNQ1VpSUdacGJHdzlJbmRvYVhSbElpQm1iMjUwTFdaaGJXbHNlVDBpSjBOdmRYSnBaWElnVG1WM0p5d2diVzl1YjNOd1lXTmxJaUJtYjI1MExYTnBlbVU5SWpFd2NIZ2lJSGhzYVc1ck9taHlaV1k5SWlOMFpYaDBMWEJoZEdndFlTSStNSGd4WmprNE5EQmhPRFZrTldGbU5XSm1NV1F4TnpZeVpqa3lOV0prWVdSa1l6UXlNREZtT1RnMElPS0FvaUJWVGtrZ1BHRnVhVzFoZEdVZ1lXUmthWFJwZG1VOUluTjFiU0lnWVhSMGNtbGlkWFJsVG1GdFpUMGljM1JoY25SUFptWnpaWFFpSUdaeWIyMDlJakFsSWlCMGJ6MGlNVEF3SlNJZ1ltVm5hVzQ5SWpCeklpQmtkWEk5SWpNd2N5SWdjbVZ3WldGMFEyOTFiblE5SW1sdVpHVm1hVzVwZEdVaUlDOCtQQzkwWlhoMFVHRjBhRDQ4ZEdWNGRGQmhkR2dnYzNSaGNuUlBabVp6WlhROUlpMDFNQ1VpSUdacGJHdzlJbmRvYVhSbElpQm1iMjUwTFdaaGJXbHNlVDBpSjBOdmRYSnBaWElnVG1WM0p5d2diVzl1YjNOd1lXTmxJaUJtYjI1MExYTnBlbVU5SWpFd2NIZ2lJSGhzYVc1ck9taHlaV1k5SWlOMFpYaDBMWEJoZEdndFlTSStNSGd4WmprNE5EQmhPRFZrTldGbU5XSm1NV1F4TnpZeVpqa3lOV0prWVdSa1l6UXlNREZtT1RnMElPS0FvaUJWVGtrZ1BHRnVhVzFoZEdVZ1lXUmthWFJwZG1VOUluTjFiU0lnWVhSMGNtbGlkWFJsVG1GdFpUMGljM1JoY25SUFptWnpaWFFpSUdaeWIyMDlJakFsSWlCMGJ6MGlNVEF3SlNJZ1ltVm5hVzQ5SWpCeklpQmtkWEk5SWpNd2N5SWdjbVZ3WldGMFEyOTFiblE5SW1sdVpHVm1hVzVwZEdVaUlDOCtQQzkwWlhoMFVHRjBhRDQ4TDNSbGVIUStQR2NnYldGemF6MGlkWEpzS0NObVlXUmxMWE41YldKdmJDa2lQanh5WldOMElHWnBiR3c5SW01dmJtVWlJSGc5SWpCd2VDSWdlVDBpTUhCNElpQjNhV1IwYUQwaU1qa3djSGdpSUdobGFXZG9kRDBpTWpBd2NIZ2lJQzgrSUR4MFpYaDBJSGs5SWpjd2NIZ2lJSGc5SWpNeWNIZ2lJR1pwYkd3OUluZG9hWFJsSWlCbWIyNTBMV1poYldsc2VUMGlKME52ZFhKcFpYSWdUbVYzSnl3Z2JXOXViM053WVdObElpQm1iMjUwTFhkbGFXZG9kRDBpTWpBd0lpQm1iMjUwTFhOcGVtVTlJak0yY0hnaVBsVk9TUzlYUlZSSVBDOTBaWGgwUGp4MFpYaDBJSGs5SWpFeE5YQjRJaUI0UFNJek1uQjRJaUJtYVd4c1BTSjNhR2wwWlNJZ1ptOXVkQzFtWVcxcGJIazlJaWREYjNWeWFXVnlJRTVsZHljc0lHMXZibTl6Y0dGalpTSWdabTl1ZEMxM1pXbG5hSFE5SWpJd01DSWdabTl1ZEMxemFYcGxQU0l6Tm5CNElqNHdMak1sUEM5MFpYaDBQand2Wno0OGNtVmpkQ0I0UFNJeE5pSWdlVDBpTVRZaUlIZHBaSFJvUFNJeU5UZ2lJR2hsYVdkb2REMGlORFk0SWlCeWVEMGlNallpSUhKNVBTSXlOaUlnWm1sc2JEMGljbWRpWVNnd0xEQXNNQ3d3S1NJZ2MzUnliMnRsUFNKeVoySmhLREkxTlN3eU5UVXNNalUxTERBdU1pa2lJQzgrUEdjZ2JXRnphejBpZFhKc0tDTm1ZV1JsTFdSdmQyNHBJaUJ6ZEhsc1pUMGlkSEpoYm5ObWIzSnRPblJ5WVc1emJHRjBaU2czTW5CNExERTRPWEI0S1NJK1BISmxZM1FnZUQwaUxURTJjSGdpSUhrOUlpMHhObkI0SWlCM2FXUjBhRDBpTVRnd2NIZ2lJR2hsYVdkb2REMGlNVGd3Y0hnaUlHWnBiR3c5SW01dmJtVWlJQzgrUEhCaGRHZ2daRDBpVFRFZ01VTTVJRGd4SURZMUlERXpOeUF4TkRVZ01UUTFJaUJ6ZEhKdmEyVTlJbkpuWW1Fb01Dd3dMREFzTUM0ektTSWdjM1J5YjJ0bExYZHBaSFJvUFNJek1uQjRJaUJtYVd4c1BTSnViMjVsSWlCemRISnZhMlV0YkdsdVpXTmhjRDBpY205MWJtUWlJQzgrUEM5blBqeG5JRzFoYzJzOUluVnliQ2dqWm1Ga1pTMWtiM2R1S1NJZ2MzUjViR1U5SW5SeVlXNXpabTl5YlRwMGNtRnVjMnhoZEdVb056SndlQ3d4T0Rsd2VDa2lQanh5WldOMElIZzlJaTB4Tm5CNElpQjVQU0l0TVRad2VDSWdkMmxrZEdnOUlqRTRNSEI0SWlCb1pXbG5hSFE5SWpFNE1IQjRJaUJtYVd4c1BTSnViMjVsSWlBdlBqeHdZWFJvSUdROUlrMHhJREZET1NBNE1TQTJOU0F4TXpjZ01UUTFJREUwTlNJZ2MzUnliMnRsUFNKeVoySmhLREkxTlN3eU5UVXNNalUxTERFcElpQm1hV3hzUFNKdWIyNWxJaUJ6ZEhKdmEyVXRiR2x1WldOaGNEMGljbTkxYm1RaUlDOCtQQzluUGp4amFYSmpiR1VnWTNnOUlqY3pjSGdpSUdONVBTSXhPVEJ3ZUNJZ2NqMGlOSEI0SWlCbWFXeHNQU0ozYUdsMFpTSWdMejQ4WTJseVkyeGxJR040UFNJM00zQjRJaUJqZVQwaU1Ua3djSGdpSUhJOUlqSTBjSGdpSUdacGJHdzlJbTV2Ym1VaUlITjBjbTlyWlQwaWQyaHBkR1VpSUM4K0lEeG5JSE4wZVd4bFBTSjBjbUZ1YzJadmNtMDZkSEpoYm5Oc1lYUmxLREk1Y0hnc0lETTROSEI0S1NJK1BISmxZM1FnZDJsa2RHZzlJamt4Y0hnaUlHaGxhV2RvZEQwaU1qWndlQ0lnY25nOUlqaHdlQ0lnY25rOUlqaHdlQ0lnWm1sc2JEMGljbWRpWVNnd0xEQXNNQ3d3TGpZcElpQXZQangwWlhoMElIZzlJakV5Y0hnaUlIazlJakUzY0hnaUlHWnZiblF0Wm1GdGFXeDVQU0luUTI5MWNtbGxjaUJPWlhjbkxDQnRiMjV2YzNCaFkyVWlJR1p2Ym5RdGMybDZaVDBpTVRKd2VDSWdabWxzYkQwaWQyaHBkR1VpUGp4MGMzQmhiaUJtYVd4c1BTSnlaMkpoS0RJMU5Td3lOVFVzTWpVMUxEQXVOaWtpUGtsRU9pQThMM1J6Y0dGdVBqUTRPREl3UEM5MFpYaDBQand2Wno0Z1BHY2djM1I1YkdVOUluUnlZVzV6Wm05eWJUcDBjbUZ1YzJ4aGRHVW9Namx3ZUN3Z05ERTBjSGdwSWo0OGNtVmpkQ0IzYVdSMGFEMGlNVFF3Y0hnaUlHaGxhV2RvZEQwaU1qWndlQ0lnY25nOUlqaHdlQ0lnY25rOUlqaHdlQ0lnWm1sc2JEMGljbWRpWVNnd0xEQXNNQ3d3TGpZcElpQXZQangwWlhoMElIZzlJakV5Y0hnaUlIazlJakUzY0hnaUlHWnZiblF0Wm1GdGFXeDVQU0luUTI5MWNtbGxjaUJPWlhjbkxDQnRiMjV2YzNCaFkyVWlJR1p2Ym5RdGMybDZaVDBpTVRKd2VDSWdabWxzYkQwaWQyaHBkR1VpUGp4MGMzQmhiaUJtYVd4c1BTSnlaMkpoS0RJMU5Td3lOVFVzTWpVMUxEQXVOaWtpUGsxcGJpQlVhV05yT2lBOEwzUnpjR0Z1UGkwME56YzJNRHd2ZEdWNGRENDhMMmMrSUR4bklITjBlV3hsUFNKMGNtRnVjMlp2Y20wNmRISmhibk5zWVhSbEtESTVjSGdzSURRME5IQjRLU0krUEhKbFkzUWdkMmxrZEdnOUlqRTBNSEI0SWlCb1pXbG5hSFE5SWpJMmNIZ2lJSEo0UFNJNGNIZ2lJSEo1UFNJNGNIZ2lJR1pwYkd3OUluSm5ZbUVvTUN3d0xEQXNNQzQyS1NJZ0x6NDhkR1Y0ZENCNFBTSXhNbkI0SWlCNVBTSXhOM0I0SWlCbWIyNTBMV1poYldsc2VUMGlKME52ZFhKcFpYSWdUbVYzSnl3Z2JXOXViM053WVdObElpQm1iMjUwTFhOcGVtVTlJakV5Y0hnaUlHWnBiR3c5SW5kb2FYUmxJajQ4ZEhOd1lXNGdabWxzYkQwaWNtZGlZU2d5TlRVc01qVTFMREkxTlN3d0xqWXBJajVOWVhnZ1ZHbGphem9nUEM5MGMzQmhiajR0TkRNME5EQThMM1JsZUhRK1BDOW5QanhuSUhOMGVXeGxQU0owY21GdWMyWnZjbTA2ZEhKaGJuTnNZWFJsS0RJeU5uQjRMQ0EwTXpOd2VDa2lQanh5WldOMElIZHBaSFJvUFNJek5uQjRJaUJvWldsbmFIUTlJak0yY0hnaUlISjRQU0k0Y0hnaUlISjVQU0k0Y0hnaUlHWnBiR3c5SW01dmJtVWlJSE4wY205clpUMGljbWRpWVNneU5UVXNNalUxTERJMU5Td3dMaklwSWlBdlBqeHdZWFJvSUhOMGNtOXJaUzFzYVc1bFkyRndQU0p5YjNWdVpDSWdaRDBpVFRnZ09VTTRMakF3TURBMElESXlMamswT1RRZ01UWXVNakE1T1NBeU9DQXlOeUF5T0NJZ1ptbHNiRDBpYm05dVpTSWdjM1J5YjJ0bFBTSjNhR2wwWlNJZ0x6NDhZMmx5WTJ4bElITjBlV3hsUFNKMGNtRnVjMlp2Y20wNmRISmhibk5zWVhSbE0yUW9PSEI0TENBeE5DNHlOWEI0TENBd2NIZ3BJaUJqZUQwaU1IQjRJaUJqZVQwaU1IQjRJaUJ5UFNJMGNIZ2lJR1pwYkd3OUluZG9hWFJsSWk4K1BDOW5Qand2YzNablBnPT0ifQ==', imageUrl= 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjkwIiBoZWlnaHQ9IjUwMCIgdmlld0JveD0iMCAwIDI5MCA1MDAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9J2h0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsnPjxkZWZzPjxmaWx0ZXIgaWQ9ImYxIj48ZmVJbWFnZSByZXN1bHQ9InAwIiB4bGluazpocmVmPSJkYXRhOmltYWdlL3N2Zyt4bWw7YmFzZTY0LFBITjJaeUIzYVdSMGFEMG5Namt3SnlCb1pXbG5hSFE5SnpVd01DY2dkbWxsZDBKdmVEMG5NQ0F3SURJNU1DQTFNREFuSUhodGJHNXpQU2RvZEhSd09pOHZkM2QzTG5jekxtOXlaeTh5TURBd0wzTjJaeWMrUEhKbFkzUWdkMmxrZEdnOUp6STVNSEI0SnlCb1pXbG5hSFE5SnpVd01IQjRKeUJtYVd4c1BTY2pNV1k1T0RRd0p5OCtQQzl6ZG1jKyIvPjxmZUltYWdlIHJlc3VsdD0icDEiIHhsaW5rOmhyZWY9ImRhdGE6aW1hZ2Uvc3ZnK3htbDtiYXNlNjQsUEhOMlp5QjNhV1IwYUQwbk1qa3dKeUJvWldsbmFIUTlKelV3TUNjZ2RtbGxkMEp2ZUQwbk1DQXdJREk1TUNBMU1EQW5JSGh0Ykc1elBTZG9kSFJ3T2k4dmQzZDNMbmN6TG05eVp5OHlNREF3TDNOMlp5YytQR05wY21Oc1pTQmplRDBuTVRNeUp5QmplVDBuTXprekp5QnlQU2N4TWpCd2VDY2dabWxzYkQwbkkyTXdNbUZoWVNjdlBqd3ZjM1puUGc9PSIvPjxmZUltYWdlIHJlc3VsdD0icDIiIHhsaW5rOmhyZWY9ImRhdGE6aW1hZ2Uvc3ZnK3htbDtiYXNlNjQsUEhOMlp5QjNhV1IwYUQwbk1qa3dKeUJvWldsbmFIUTlKelV3TUNjZ2RtbGxkMEp2ZUQwbk1DQXdJREk1TUNBMU1EQW5JSGh0Ykc1elBTZG9kSFJ3T2k4dmQzZDNMbmN6TG05eVp5OHlNREF3TDNOMlp5YytQR05wY21Oc1pTQmplRDBuTnpFbklHTjVQU2N6TXpNbklISTlKekV5TUhCNEp5Qm1hV3hzUFNjak1ERm1PVGcwSnk4K1BDOXpkbWMrIiAvPjxmZUltYWdlIHJlc3VsdD0icDMiIHhsaW5rOmhyZWY9ImRhdGE6aW1hZ2Uvc3ZnK3htbDtiYXNlNjQsUEhOMlp5QjNhV1IwYUQwbk1qa3dKeUJvWldsbmFIUTlKelV3TUNjZ2RtbGxkMEp2ZUQwbk1DQXdJREk1TUNBMU1EQW5JSGh0Ykc1elBTZG9kSFJ3T2k4dmQzZDNMbmN6TG05eVp5OHlNREF3TDNOMlp5YytQR05wY21Oc1pTQmplRDBuTnpZbklHTjVQU2N6TURNbklISTlKekV3TUhCNEp5Qm1hV3hzUFNjak56VTJZMk15Snk4K1BDOXpkbWMrIiAvPjxmZUJsZW5kIG1vZGU9Im92ZXJsYXkiIGluPSJwMCIgaW4yPSJwMSIgLz48ZmVCbGVuZCBtb2RlPSJleGNsdXNpb24iIGluMj0icDIiIC8+PGZlQmxlbmQgbW9kZT0ib3ZlcmxheSIgaW4yPSJwMyIgcmVzdWx0PSJibGVuZE91dCIgLz48ZmVHYXVzc2lhbkJsdXIgaW49ImJsZW5kT3V0IiBzdGREZXZpYXRpb249IjQyIiAvPjwvZmlsdGVyPiA8Y2xpcFBhdGggaWQ9ImNvcm5lcnMiPjxyZWN0IHdpZHRoPSIyOTAiIGhlaWdodD0iNTAwIiByeD0iNDIiIHJ5PSI0MiIgLz48L2NsaXBQYXRoPjxwYXRoIGlkPSJ0ZXh0LXBhdGgtYSIgZD0iTTQwIDEyIEgyNTAgQTI4IDI4IDAgMCAxIDI3OCA0MCBWNDYwIEEyOCAyOCAwIDAgMSAyNTAgNDg4IEg0MCBBMjggMjggMCAwIDEgMTIgNDYwIFY0MCBBMjggMjggMCAwIDEgNDAgMTIgeiIgLz48cGF0aCBpZD0ibWluaW1hcCIgZD0iTTIzNCA0NDRDMjM0IDQ1Ny45NDkgMjQyLjIxIDQ2MyAyNTMgNDYzIiAvPjxmaWx0ZXIgaWQ9InRvcC1yZWdpb24tYmx1ciI+PGZlR2F1c3NpYW5CbHVyIGluPSJTb3VyY2VHcmFwaGljIiBzdGREZXZpYXRpb249IjI0IiAvPjwvZmlsdGVyPjxsaW5lYXJHcmFkaWVudCBpZD0iZ3JhZC11cCIgeDE9IjEiIHgyPSIwIiB5MT0iMSIgeTI9IjAiPjxzdG9wIG9mZnNldD0iMC4wIiBzdG9wLWNvbG9yPSJ3aGl0ZSIgc3RvcC1vcGFjaXR5PSIxIiAvPjxzdG9wIG9mZnNldD0iLjkiIHN0b3AtY29sb3I9IndoaXRlIiBzdG9wLW9wYWNpdHk9IjAiIC8+PC9saW5lYXJHcmFkaWVudD48bGluZWFyR3JhZGllbnQgaWQ9ImdyYWQtZG93biIgeDE9IjAiIHgyPSIxIiB5MT0iMCIgeTI9IjEiPjxzdG9wIG9mZnNldD0iMC4wIiBzdG9wLWNvbG9yPSJ3aGl0ZSIgc3RvcC1vcGFjaXR5PSIxIiAvPjxzdG9wIG9mZnNldD0iMC45IiBzdG9wLWNvbG9yPSJ3aGl0ZSIgc3RvcC1vcGFjaXR5PSIwIiAvPjwvbGluZWFyR3JhZGllbnQ+PG1hc2sgaWQ9ImZhZGUtdXAiIG1hc2tDb250ZW50VW5pdHM9Im9iamVjdEJvdW5kaW5nQm94Ij48cmVjdCB3aWR0aD0iMSIgaGVpZ2h0PSIxIiBmaWxsPSJ1cmwoI2dyYWQtdXApIiAvPjwvbWFzaz48bWFzayBpZD0iZmFkZS1kb3duIiBtYXNrQ29udGVudFVuaXRzPSJvYmplY3RCb3VuZGluZ0JveCI+PHJlY3Qgd2lkdGg9IjEiIGhlaWdodD0iMSIgZmlsbD0idXJsKCNncmFkLWRvd24pIiAvPjwvbWFzaz48bWFzayBpZD0ibm9uZSIgbWFza0NvbnRlbnRVbml0cz0ib2JqZWN0Qm91bmRpbmdCb3giPjxyZWN0IHdpZHRoPSIxIiBoZWlnaHQ9IjEiIGZpbGw9IndoaXRlIiAvPjwvbWFzaz48bGluZWFyR3JhZGllbnQgaWQ9ImdyYWQtc3ltYm9sIj48c3RvcCBvZmZzZXQ9IjAuNyIgc3RvcC1jb2xvcj0id2hpdGUiIHN0b3Atb3BhY2l0eT0iMSIgLz48c3RvcCBvZmZzZXQ9Ii45NSIgc3RvcC1jb2xvcj0id2hpdGUiIHN0b3Atb3BhY2l0eT0iMCIgLz48L2xpbmVhckdyYWRpZW50PjxtYXNrIGlkPSJmYWRlLXN5bWJvbCIgbWFza0NvbnRlbnRVbml0cz0idXNlclNwYWNlT25Vc2UiPjxyZWN0IHdpZHRoPSIyOTBweCIgaGVpZ2h0PSIyMDBweCIgZmlsbD0idXJsKCNncmFkLXN5bWJvbCkiIC8+PC9tYXNrPjwvZGVmcz48ZyBjbGlwLXBhdGg9InVybCgjY29ybmVycykiPjxyZWN0IGZpbGw9IjFmOTg0MCIgeD0iMHB4IiB5PSIwcHgiIHdpZHRoPSIyOTBweCIgaGVpZ2h0PSI1MDBweCIgLz48cmVjdCBzdHlsZT0iZmlsdGVyOiB1cmwoI2YxKSIgeD0iMHB4IiB5PSIwcHgiIHdpZHRoPSIyOTBweCIgaGVpZ2h0PSI1MDBweCIgLz4gPGcgc3R5bGU9ImZpbHRlcjp1cmwoI3RvcC1yZWdpb24tYmx1cik7IHRyYW5zZm9ybTpzY2FsZSgxLjUpOyB0cmFuc2Zvcm0tb3JpZ2luOmNlbnRlciB0b3A7Ij48cmVjdCBmaWxsPSJub25lIiB4PSIwcHgiIHk9IjBweCIgd2lkdGg9IjI5MHB4IiBoZWlnaHQ9IjUwMHB4IiAvPjxlbGxpcHNlIGN4PSI1MCUiIGN5PSIwcHgiIHJ4PSIxODBweCIgcnk9IjEyMHB4IiBmaWxsPSIjMDAwIiBvcGFjaXR5PSIwLjg1IiAvPjwvZz48cmVjdCB4PSIwIiB5PSIwIiB3aWR0aD0iMjkwIiBoZWlnaHQ9IjUwMCIgcng9IjQyIiByeT0iNDIiIGZpbGw9InJnYmEoMCwwLDAsMCkiIHN0cm9rZT0icmdiYSgyNTUsMjU1LDI1NSwwLjIpIiAvPjwvZz48dGV4dCB0ZXh0LXJlbmRlcmluZz0ib3B0aW1pemVTcGVlZCI+PHRleHRQYXRoIHN0YXJ0T2Zmc2V0PSItMTAwJSIgZmlsbD0id2hpdGUiIGZvbnQtZmFtaWx5PSInQ291cmllciBOZXcnLCBtb25vc3BhY2UiIGZvbnQtc2l6ZT0iMTBweCIgeGxpbms6aHJlZj0iI3RleHQtcGF0aC1hIj4weGMwMmFhYTM5YjIyM2ZlOGQwYTBlNWM0ZjI3ZWFkOTA4M2M3NTZjYzIg4oCiIFdFVEggPGFuaW1hdGUgYWRkaXRpdmU9InN1bSIgYXR0cmlidXRlTmFtZT0ic3RhcnRPZmZzZXQiIGZyb209IjAlIiB0bz0iMTAwJSIgYmVnaW49IjBzIiBkdXI9IjMwcyIgcmVwZWF0Q291bnQ9ImluZGVmaW5pdGUiIC8+PC90ZXh0UGF0aD4gPHRleHRQYXRoIHN0YXJ0T2Zmc2V0PSIwJSIgZmlsbD0id2hpdGUiIGZvbnQtZmFtaWx5PSInQ291cmllciBOZXcnLCBtb25vc3BhY2UiIGZvbnQtc2l6ZT0iMTBweCIgeGxpbms6aHJlZj0iI3RleHQtcGF0aC1hIj4weGMwMmFhYTM5YjIyM2ZlOGQwYTBlNWM0ZjI3ZWFkOTA4M2M3NTZjYzIg4oCiIFdFVEggPGFuaW1hdGUgYWRkaXRpdmU9InN1bSIgYXR0cmlidXRlTmFtZT0ic3RhcnRPZmZzZXQiIGZyb209IjAlIiB0bz0iMTAwJSIgYmVnaW49IjBzIiBkdXI9IjMwcyIgcmVwZWF0Q291bnQ9ImluZGVmaW5pdGUiIC8+IDwvdGV4dFBhdGg+PHRleHRQYXRoIHN0YXJ0T2Zmc2V0PSI1MCUiIGZpbGw9IndoaXRlIiBmb250LWZhbWlseT0iJ0NvdXJpZXIgTmV3JywgbW9ub3NwYWNlIiBmb250LXNpemU9IjEwcHgiIHhsaW5rOmhyZWY9IiN0ZXh0LXBhdGgtYSI+MHgxZjk4NDBhODVkNWFmNWJmMWQxNzYyZjkyNWJkYWRkYzQyMDFmOTg0IOKAoiBVTkkgPGFuaW1hdGUgYWRkaXRpdmU9InN1bSIgYXR0cmlidXRlTmFtZT0ic3RhcnRPZmZzZXQiIGZyb209IjAlIiB0bz0iMTAwJSIgYmVnaW49IjBzIiBkdXI9IjMwcyIgcmVwZWF0Q291bnQ9ImluZGVmaW5pdGUiIC8+PC90ZXh0UGF0aD48dGV4dFBhdGggc3RhcnRPZmZzZXQ9Ii01MCUiIGZpbGw9IndoaXRlIiBmb250LWZhbWlseT0iJ0NvdXJpZXIgTmV3JywgbW9ub3NwYWNlIiBmb250LXNpemU9IjEwcHgiIHhsaW5rOmhyZWY9IiN0ZXh0LXBhdGgtYSI+MHgxZjk4NDBhODVkNWFmNWJmMWQxNzYyZjkyNWJkYWRkYzQyMDFmOTg0IOKAoiBVTkkgPGFuaW1hdGUgYWRkaXRpdmU9InN1bSIgYXR0cmlidXRlTmFtZT0ic3RhcnRPZmZzZXQiIGZyb209IjAlIiB0bz0iMTAwJSIgYmVnaW49IjBzIiBkdXI9IjMwcyIgcmVwZWF0Q291bnQ9ImluZGVmaW5pdGUiIC8+PC90ZXh0UGF0aD48L3RleHQ+PGcgbWFzaz0idXJsKCNmYWRlLXN5bWJvbCkiPjxyZWN0IGZpbGw9Im5vbmUiIHg9IjBweCIgeT0iMHB4IiB3aWR0aD0iMjkwcHgiIGhlaWdodD0iMjAwcHgiIC8+IDx0ZXh0IHk9IjcwcHgiIHg9IjMycHgiIGZpbGw9IndoaXRlIiBmb250LWZhbWlseT0iJ0NvdXJpZXIgTmV3JywgbW9ub3NwYWNlIiBmb250LXdlaWdodD0iMjAwIiBmb250LXNpemU9IjM2cHgiPlVOSS9XRVRIPC90ZXh0Pjx0ZXh0IHk9IjExNXB4IiB4PSIzMnB4IiBmaWxsPSJ3aGl0ZSIgZm9udC1mYW1pbHk9IidDb3VyaWVyIE5ldycsIG1vbm9zcGFjZSIgZm9udC13ZWlnaHQ9IjIwMCIgZm9udC1zaXplPSIzNnB4Ij4wLjMlPC90ZXh0PjwvZz48cmVjdCB4PSIxNiIgeT0iMTYiIHdpZHRoPSIyNTgiIGhlaWdodD0iNDY4IiByeD0iMjYiIHJ5PSIyNiIgZmlsbD0icmdiYSgwLDAsMCwwKSIgc3Ryb2tlPSJyZ2JhKDI1NSwyNTUsMjU1LDAuMikiIC8+PGcgbWFzaz0idXJsKCNmYWRlLWRvd24pIiBzdHlsZT0idHJhbnNmb3JtOnRyYW5zbGF0ZSg3MnB4LDE4OXB4KSI+PHJlY3QgeD0iLTE2cHgiIHk9Ii0xNnB4IiB3aWR0aD0iMTgwcHgiIGhlaWdodD0iMTgwcHgiIGZpbGw9Im5vbmUiIC8+PHBhdGggZD0iTTEgMUM5IDgxIDY1IDEzNyAxNDUgMTQ1IiBzdHJva2U9InJnYmEoMCwwLDAsMC4zKSIgc3Ryb2tlLXdpZHRoPSIzMnB4IiBmaWxsPSJub25lIiBzdHJva2UtbGluZWNhcD0icm91bmQiIC8+PC9nPjxnIG1hc2s9InVybCgjZmFkZS1kb3duKSIgc3R5bGU9InRyYW5zZm9ybTp0cmFuc2xhdGUoNzJweCwxODlweCkiPjxyZWN0IHg9Ii0xNnB4IiB5PSItMTZweCIgd2lkdGg9IjE4MHB4IiBoZWlnaHQ9IjE4MHB4IiBmaWxsPSJub25lIiAvPjxwYXRoIGQ9Ik0xIDFDOSA4MSA2NSAxMzcgMTQ1IDE0NSIgc3Ryb2tlPSJyZ2JhKDI1NSwyNTUsMjU1LDEpIiBmaWxsPSJub25lIiBzdHJva2UtbGluZWNhcD0icm91bmQiIC8+PC9nPjxjaXJjbGUgY3g9IjczcHgiIGN5PSIxOTBweCIgcj0iNHB4IiBmaWxsPSJ3aGl0ZSIgLz48Y2lyY2xlIGN4PSI3M3B4IiBjeT0iMTkwcHgiIHI9IjI0cHgiIGZpbGw9Im5vbmUiIHN0cm9rZT0id2hpdGUiIC8+IDxnIHN0eWxlPSJ0cmFuc2Zvcm06dHJhbnNsYXRlKDI5cHgsIDM4NHB4KSI+PHJlY3Qgd2lkdGg9IjkxcHgiIGhlaWdodD0iMjZweCIgcng9IjhweCIgcnk9IjhweCIgZmlsbD0icmdiYSgwLDAsMCwwLjYpIiAvPjx0ZXh0IHg9IjEycHgiIHk9IjE3cHgiIGZvbnQtZmFtaWx5PSInQ291cmllciBOZXcnLCBtb25vc3BhY2UiIGZvbnQtc2l6ZT0iMTJweCIgZmlsbD0id2hpdGUiPjx0c3BhbiBmaWxsPSJyZ2JhKDI1NSwyNTUsMjU1LDAuNikiPklEOiA8L3RzcGFuPjQ4ODIwPC90ZXh0PjwvZz4gPGcgc3R5bGU9InRyYW5zZm9ybTp0cmFuc2xhdGUoMjlweCwgNDE0cHgpIj48cmVjdCB3aWR0aD0iMTQwcHgiIGhlaWdodD0iMjZweCIgcng9IjhweCIgcnk9IjhweCIgZmlsbD0icmdiYSgwLDAsMCwwLjYpIiAvPjx0ZXh0IHg9IjEycHgiIHk9IjE3cHgiIGZvbnQtZmFtaWx5PSInQ291cmllciBOZXcnLCBtb25vc3BhY2UiIGZvbnQtc2l6ZT0iMTJweCIgZmlsbD0id2hpdGUiPjx0c3BhbiBmaWxsPSJyZ2JhKDI1NSwyNTUsMjU1LDAuNikiPk1pbiBUaWNrOiA8L3RzcGFuPi00Nzc2MDwvdGV4dD48L2c+IDxnIHN0eWxlPSJ0cmFuc2Zvcm06dHJhbnNsYXRlKDI5cHgsIDQ0NHB4KSI+PHJlY3Qgd2lkdGg9IjE0MHB4IiBoZWlnaHQ9IjI2cHgiIHJ4PSI4cHgiIHJ5PSI4cHgiIGZpbGw9InJnYmEoMCwwLDAsMC42KSIgLz48dGV4dCB4PSIxMnB4IiB5PSIxN3B4IiBmb250LWZhbWlseT0iJ0NvdXJpZXIgTmV3JywgbW9ub3NwYWNlIiBmb250LXNpemU9IjEycHgiIGZpbGw9IndoaXRlIj48dHNwYW4gZmlsbD0icmdiYSgyNTUsMjU1LDI1NSwwLjYpIj5NYXggVGljazogPC90c3Bhbj4tNDM0NDA8L3RleHQ+PC9nPjxnIHN0eWxlPSJ0cmFuc2Zvcm06dHJhbnNsYXRlKDIyNnB4LCA0MzNweCkiPjxyZWN0IHdpZHRoPSIzNnB4IiBoZWlnaHQ9IjM2cHgiIHJ4PSI4cHgiIHJ5PSI4cHgiIGZpbGw9Im5vbmUiIHN0cm9rZT0icmdiYSgyNTUsMjU1LDI1NSwwLjIpIiAvPjxwYXRoIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgZD0iTTggOUM4LjAwMDA0IDIyLjk0OTQgMTYuMjA5OSAyOCAyNyAyOCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSJ3aGl0ZSIgLz48Y2lyY2xlIHN0eWxlPSJ0cmFuc2Zvcm06dHJhbnNsYXRlM2QoOHB4LCAxNC4yNXB4LCAwcHgpIiBjeD0iMHB4IiBjeT0iMHB4IiByPSI0cHgiIGZpbGw9IndoaXRlIi8+PC9nPjwvc3ZnPg==', animationUrl=None, youtubeUrl=None, backgroundColor=None, name='Uniswap - 0.3% - UNI/WETH - 76.998<>118.60', description= 'This NFT represents a liquidity position in a Uniswap V3 UNI-WETH pool. The owner of this NFT can modify or redeem the position.\n\nPool Address: 0x1d42064fc4beb5f8aaf85f4617ae8b3b5b8bd801\nUNI Address: 0x1f9840a85d5af5bf1d1762f925bdaddc4201f984\nWETH Address: 0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2\nFee Tier: 0.3%\nToken ID: 48820\n\n⚠️ DISCLAIMER: Due diligence is imperative when assessing this NFT. Make sure token addresses match the expected tokens, as token symbols may be imitated.', frameImageUrl=None, attributes=[]) assert (result == expected) collection = Collection( address='0xE3f92992BB4F0f0D173623A52b2922d65172601d', name='Knights of Degen', symbol='KNIGHTS', description= '8,888 NFT Degens who love sports, betting, alpha and nfts ⚔️', imageUrl= 'https://lh3.googleusercontent.com/yxdb_995UrIS6W9YIHMfMCRsdYRqcITlBvO5w7OoUx35rkClUeq9rPCvTMXdtw_zION07O_qRZSuNfZu6R6o8bI_KmbrfThhFtA4SBc=s120', twitterUsername='******', instagramUsername=None, wikiUrl=None, openseaSlug='knights-of-degen-official', url='https://www.knightsofdegen.io/', discordUrl='https://discord.gg/knightsofdegen', bannerImageUrl= 'https://lh3.googleusercontent.com/328JHSQ2nPpzfTxpZjV5xiiBp6R17GgUdjHJ7BF0mU-SH0Ou1LiiYmcET0WeHt26LD4tVXenIvYZD7VC8jV__bqRCiA_CzmWfwg7=s2500', doesSupportErc721=True, doesSupportErc1155=False, collectionId=41, createdDate=datetime.datetime(2022, 1, 31, 12, 27, 18, 712443), updatedDate=datetime.datetime(2022, 1, 31, 12, 27, 18, 712443)) result = await tokenMetadataProcessor.retrieve_token_metadata( registryAddress='0xE3f92992BB4F0f0D173623A52b2922d65172601d', tokenId='4655', collection=collection) expected = RetrievedTokenMetadata( registryAddress='0xE3f92992BB4F0f0D173623A52b2922d65172601d', tokenId='4655', metadataUrl= 'ipfs://QmZk6BfbKYswziCEdYaMqh7yQqtcmCnHhoNu3zDGKQjKpJ/4655', imageUrl= 'ipfs://QmdFkX7SkgL9cDYPwt4CNpsQ5CSz74CF3MwswmGLKEgDMp/4655.png', animationUrl=None, youtubeUrl=None, backgroundColor=None, name='Knights of Degen #4655', description= 'The Knights of Degen are 8,888 NFT degenerate gamblers that live their truest and darkest lives, free from oppression, and spend their days in the Degen Sports Bars watching and wagering on all sporting events.', frameImageUrl=None, attributes=[{ 'trait_type': 'Background', 'value': 'Teal' }, { 'trait_type': 'Eyes', 'value': 'Blue' }, { 'trait_type': 'Helmet', 'value': 'Valkyrie Helmet - Silver' }, { 'trait_type': 'Nose', 'value': 'Normal' }, { 'trait_type': 'Armor', 'value': 'Chainmail - Silver' }, { 'trait_type': 'Mouth', 'value': 'Tongue Out' }, { 'trait_type': 'Character', 'value': 'Human' }, { 'trait_type': 'Weapon', 'value': 'None' }]) assert (result == expected) await s3manager.disconnect()
workQueue = SqsMessageQueue( region='eu-west-1', accessKeyId=os.environ['AWS_KEY'], accessKeySecret=os.environ['AWS_SECRET'], queueUrl='https://sqs.eu-west-1.amazonaws.com/097520841056/mdtp-work-queue' ) s3Manager = S3Manager(region='eu-west-1', accessKeyId=os.environ['AWS_KEY'], accessKeySecret=os.environ['AWS_SECRET']) # NOTE(krishan711): The AWS eth instance is much slower with getLogs so fails when calling ContractStore.get_latest_update_block_number # awsRequester = AwsRequester(accessKeyId=os.environ['AWS_KEY'], accessKeySecret=os.environ['AWS_SECRET']) # ethClient = RestEthClient(url='https://nd-foldvvlb25awde7kbqfvpgvrrm.ethereum.managedblockchain.eu-west-1.amazonaws.com', requester=awsRequester) requester = Requester() ethClient = RestEthClient(url=os.environ['ALCHEMY_MAINNET_URL'], requester=requester) rinkebyEthClient = RestEthClient(url=os.environ['ALCHEMY_URL'], requester=requester) mumbaiEthClient = RestEthClient(url='https://matic-mumbai.chainstacklabs.com', requester=requester) contractStore = create_contract_store(ethClient=ethClient, rinkebyEthClient=rinkebyEthClient, mumbaiEthClient=mumbaiEthClient) infuraIpfsAuth = BasicAuthentication( username=os.environ['INFURA_IPFS_PROJECT_ID'], password=os.environ['INFURA_IPFS_PROJECT_SECRET']) infuraIpfsRequester = Requester( headers={'authorization': f'Basic {infuraIpfsAuth.to_string()}'}) ipfsManager = IpfsManager(requester=infuraIpfsRequester)
async def reprocess_metadata(startId: Optional[int], endId: Optional[int], batchSize: Optional[int]): databaseConnectionString = Database.create_psql_connection_string( username=os.environ["DB_USERNAME"], password=os.environ["DB_PASSWORD"], host=os.environ["DB_HOST"], port=os.environ["DB_PORT"], name=os.environ["DB_NAME"]) database = Database(connectionString=databaseConnectionString) saver = Saver(database=database) retriever = Retriever(database=database) s3manager = S3Manager(region='eu-west-1', accessKeyId=os.environ['AWS_KEY'], accessKeySecret=os.environ['AWS_SECRET']) tokenQueue = SqsMessageQueue( region='eu-west-1', accessKeyId=os.environ['AWS_KEY'], accessKeySecret=os.environ['AWS_SECRET'], queueUrl= 'https://sqs.eu-west-1.amazonaws.com/097520841056/notd-token-queue') awsRequester = AwsRequester(accessKeyId=os.environ['AWS_KEY'], accessKeySecret=os.environ['AWS_SECRET']) requester = Requester() ethClient = RestEthClient( url= 'https://nd-foldvvlb25awde7kbqfvpgvrrm.ethereum.managedblockchain.eu-west-1.amazonaws.com', requester=awsRequester) tokenMetadataProcessor = TokenMetadataProcessor( requester=requester, ethClient=ethClient, s3manager=s3manager, bucketName=os.environ['S3_BUCKET']) openseaApiKey = os.environ['OPENSEA_API_KEY'] collectionProcessor = CollectionProcessor( requester=requester, ethClient=ethClient, openseaApiKey=openseaApiKey, s3manager=s3manager, bucketName=os.environ['S3_BUCKET']) tokenManger = TokenManager(saver=saver, retriever=retriever, tokenQueue=tokenQueue, collectionProcessor=collectionProcessor, tokenMetadataProcessor=tokenMetadataProcessor) await s3manager.connect() await tokenQueue.connect() await database.connect() if not startId: startId = 0 if not endId: maxTokenMetadata = await retriever.list_token_metadatas( limit=1, orders=[ Order(fieldName=TokenMetadatasTable.c.tokenMetadataId.key, direction=Direction.DESCENDING) ]) print(maxTokenMetadata) endId = maxTokenMetadata[0].tokenMetadataId + 1 currentId = startId while currentId < endId: start = currentId end = min(currentId + batchSize, endId) query = TokenMetadatasTable.select() query = query.where(TokenMetadatasTable.c.tokenMetadataId >= start) query = query.where(TokenMetadatasTable.c.tokenMetadataId < end) query = query.where( TokenMetadatasTable.c.updatedDate < datetime.datetime(2022, 2, 13)) query = query.order_by(TokenMetadatasTable.c.tokenMetadataId.asc()) tokenMetadatasToChange = [ token_metadata_from_row(row) for row in await database.execute(query=query) ] logging.info(f'Working on {start} - {end}') logging.info(f'Updating {len(tokenMetadatasToChange)} transfers...') await asyncio.gather(*[ _reprocess_metadata_from_s3( tokenMetadataProcessor=tokenMetadataProcessor, s3manager=s3manager, tokenManger=tokenManger, tokenMetadata=tokenMetadata) for tokenMetadata in tokenMetadatasToChange ]) currentId = currentId + batchSize await s3manager.disconnect() await tokenQueue.disconnect() await awsRequester.close_connections() await requester.close_connections() await database.disconnect()
async def reprocess_transfers(startBlockNumber: int, endBlockNumber: int): databaseConnectionString = Database.create_psql_connection_string( username=os.environ["DB_USERNAME"], password=os.environ["DB_PASSWORD"], host=os.environ["DB_HOST"], port=os.environ["DB_PORT"], name=os.environ["DB_NAME"]) database = Database(connectionString=databaseConnectionString) saver = Saver(database=database) retriever = Retriever(database=database) requester = Requester() slackClient = SlackClient(webhookUrl=os.environ['SLACK_WEBHOOK_URL'], requester=requester, defaultSender='worker', defaultChannel='notd-notifications') awsRequester = AwsRequester(accessKeyId=os.environ['AWS_KEY'], accessKeySecret=os.environ['AWS_SECRET']) ethClient = RestEthClient( url= 'https://nd-foldvvlb25awde7kbqfvpgvrrm.ethereum.managedblockchain.eu-west-1.amazonaws.com', requester=awsRequester) blockProcessor = BlockProcessor(ethClient=ethClient) notdManager = NotdManager(blockProcessor=None, saver=saver, retriever=retriever, workQueue=None, tokenManager=None, requester=requester, revueApiKey=None) await database.connect() await slackClient.post( text= f'reprocess_transfers → 🚧 started: {startBlockNumber}-{endBlockNumber}' ) try: currentBlockNumber = startBlockNumber while currentBlockNumber < endBlockNumber: logging.info(f'Working on {currentBlockNumber}') try: retrievedTokenTransfers = await blockProcessor.process_block( blockNumber=currentBlockNumber) except Exception as exception: logging.info( f'Got exception whilst getting blocks: {str(exception)}. Will retry in 10 secs.' ) time.sleep(60) currentBlockNumber -= 1 continue await notdManager._save_block_transfers( blockNumber=currentBlockNumber, retrievedTokenTransfers=retrievedTokenTransfers) currentBlockNumber = currentBlockNumber + 1 await slackClient.post( text= f'reprocess_transfers → ✅ completed : {startBlockNumber}-{endBlockNumber}' ) except Exception as exception: await slackClient.post( text= f'reprocess_transfers → � error: {startBlockNumber}-{endBlockNumber}\n```{str(exception)}```' ) raise exception finally: await database.disconnect() await requester.close_connections() await awsRequester.close_connections()
async def run(registryAddress: Optional[str]): databaseConnectionString = Database.create_psql_connection_string( username=os.environ["DB_USERNAME"], password=os.environ["DB_PASSWORD"], host=os.environ["DB_HOST"], port=os.environ["DB_PORT"], name=os.environ["DB_NAME"]) database = Database(connectionString=databaseConnectionString) saver = Saver(database=database) retriever = Retriever(database=database) workQueue = SqsMessageQueue( region='eu-west-1', accessKeyId=os.environ['AWS_KEY'], accessKeySecret=os.environ['AWS_SECRET'], queueUrl= 'https://sqs.eu-west-1.amazonaws.com/097520841056/notd-work-queue') tokenQueue = SqsMessageQueue( region='eu-west-1', accessKeyId=os.environ['AWS_KEY'], accessKeySecret=os.environ['AWS_SECRET'], queueUrl= 'https://sqs.eu-west-1.amazonaws.com/097520841056/notd-token-queue') requester = Requester() awsRequester = AwsRequester(accessKeyId=os.environ['AWS_KEY'], accessKeySecret=os.environ['AWS_SECRET']) ethClient = RestEthClient( url= 'https://nd-foldvvlb25awde7kbqfvpgvrrm.ethereum.managedblockchain.eu-west-1.amazonaws.com', requester=awsRequester) blockProcessor = BlockProcessor(ethClient=ethClient) tokenManager = TokenManager(saver=saver, retriever=retriever, tokenQueue=tokenQueue, collectionProcessor=None, tokenMetadataProcessor=None, tokenOwnershipProcessor=None) # NOTE(krishan711): use tokenqueue so its lower prioritized work notdManager = NotdManager(blockProcessor=blockProcessor, saver=saver, retriever=retriever, workQueue=tokenQueue, tokenManager=tokenManager, requester=requester, revueApiKey=None) await database.connect() await workQueue.connect() await tokenQueue.connect() if registryAddress: registryAddresses = [registryAddress] else: query = sqlalchemy.select(TokenCollectionsTable.c.address).filter( TokenCollectionsTable.c.doesSupportErc1155 == True).order_by( TokenCollectionsTable.c.collectionId.desc()) results = await database.execute(query=query) registryAddresses = [ registryAddress for (registryAddress, ) in results ] print( f'Starting to reprocess blocks for {len(registryAddresses)} collections' ) for index, registryAddress in enumerate(registryAddresses): print( f'Reprocessing blocks for collection: {registryAddress} (index: {index})' ) minDate = datetime.datetime(2022, 4, 8, 9, 0) query = ( sqlalchemy.select(sqlalchemy.distinct(BlocksTable.c.blockNumber)) \ .join(TokenTransfersTable, TokenTransfersTable.c.blockNumber == BlocksTable.c.blockNumber) \ .filter(TokenTransfersTable.c.registryAddress == registryAddress) .filter(BlocksTable.c.updatedDate < minDate) ) results = await database.execute(query=query) blockNumbers = set(blockNumber for (blockNumber, ) in results) print(f'Processing {len(blockNumbers)} blocks') if len(blockNumbers) == 0: continue # await notdManager.process_blocks_deferred(blockNumbers=blockNumbers) for blockNumberChunk in list_util.generate_chunks( lst=list(blockNumbers), chunkSize=5): await asyncio.gather(*[ notdManager.process_block(blockNumber=blockNumber) for blockNumber in blockNumberChunk ]) query = ( sqlalchemy.select(TokenMetadatasTable.c.tokenId) \ .filter(TokenMetadatasTable.c.registryAddress == registryAddress) ) results = await database.execute(query=query) collectionTokenIds = [(registryAddress, tokenId) for (tokenId, ) in results] await tokenManager.update_token_ownerships_deferred( collectionTokenIds=collectionTokenIds) await database.disconnect() await workQueue.disconnect() await tokenQueue.disconnect() await requester.close_connections() await awsRequester.close_connections()
async def reprocess_collections(startId: int, endId: int, batchSize: int): databaseConnectionString = Database.create_psql_connection_string( username=os.environ["DB_USERNAME"], password=os.environ["DB_PASSWORD"], host=os.environ["DB_HOST"], port=os.environ["DB_PORT"], name=os.environ["DB_NAME"]) database = Database(connectionString=databaseConnectionString) saver = Saver(database) retriever = Retriever(database) openseaApiKey = os.environ['OPENSEA_API_KEY'] awsRequester = AwsRequester(accessKeyId=os.environ['AWS_KEY'], accessKeySecret=os.environ['AWS_SECRET']) requester = Requester() # ethClient = RestEthClient(url=f'https://mainnet.infura.io/v3/{os.environ["INFURA_PROJECT_ID"]}', requester=requester) ethClient = RestEthClient( url= 'https://nd-foldvvlb25awde7kbqfvpgvrrm.ethereum.managedblockchain.eu-west-1.amazonaws.com', requester=awsRequester) s3manager = S3Manager(region='eu-west-1', accessKeyId=os.environ['AWS_KEY'], accessKeySecret=os.environ['AWS_SECRET']) requester = Requester() collectionProcessor = CollectionProcessor( requester=requester, ethClient=ethClient, openseaApiKey=openseaApiKey, s3manager=s3manager, bucketName=os.environ['S3_BUCKET']) tokenManager = TokenManager(saver=saver, retriever=retriever, tokenQueue=None, collectionProcessor=collectionProcessor, tokenMetadataProcessor=None) await database.connect() await s3manager.connect() currentId = startId while currentId < endId: start = currentId end = min(currentId + batchSize, endId) logging.info(f'Working on {start} to {end}...') async with database.transaction(): query = TokenCollectionsTable.select() query = query.where(TokenCollectionsTable.c.collectionId >= start) query = query.where(TokenCollectionsTable.c.collectionId < end) collectionsToChange = [ collection_from_row(row) async for row in database.iterate(query=query) ] logging.info(f'Updating {len(collectionsToChange)} collections...') for collection in collectionsToChange: logging.info(f'Updating collection: {collection.address}') try: await tokenManager.update_collection( address=collection.address, shouldForce=True) except Exception as e: logging.exception( f'Error processing {collection.collectionId}: {e}') currentId = currentId + batchSize await database.disconnect() await s3manager.disconnect() await requester.close_connections() await awsRequester.close_connections()
async def main(): requestIdHolder = RequestIdHolder() name = os.environ.get('NAME', 'mdtp-worker') version = os.environ.get('VERSION', 'local') environment = os.environ.get('ENV', 'dev') isRunningDebugMode = environment == 'dev' if isRunningDebugMode: logging.init_basic_logging() else: logging.init_json_logging(name=name, version=version, environment=environment, requestIdHolder=requestIdHolder) databaseConnectionString = Database.create_psql_connection_string( username=os.environ["DB_USERNAME"], password=os.environ["DB_PASSWORD"], host=os.environ["DB_HOST"], port=os.environ["DB_PORT"], name=os.environ["DB_NAME"]) database = Database(connectionString=databaseConnectionString) saver = Saver(database=database) retriever = Retriever(database=database) workQueue = SqsMessageQueue( region='eu-west-1', accessKeyId=os.environ['AWS_KEY'], accessKeySecret=os.environ['AWS_SECRET'], queueUrl= 'https://sqs.eu-west-1.amazonaws.com/097520841056/mdtp-work-queue') s3Manager = S3Manager(region='eu-west-1', accessKeyId=os.environ['AWS_KEY'], accessKeySecret=os.environ['AWS_SECRET']) # NOTE(krishan711): The AWS eth instance is much slower with getLogs so fails when calling ContractStore.get_latest_update_block_number # awsRequester = AwsRequester(accessKeyId=os.environ['AWS_KEY'], accessKeySecret=os.environ['AWS_SECRET']) # ethClient = RestEthClient(url='https://nd-foldvvlb25awde7kbqfvpgvrrm.ethereum.managedblockchain.eu-west-1.amazonaws.com', requester=awsRequester) requester = Requester() ethClient = RestEthClient(url=os.environ['ALCHEMY_MAINNET_URL'], requester=requester) rinkebyEthClient = RestEthClient(url=os.environ['ALCHEMY_URL'], requester=requester) mumbaiEthClient = RestEthClient( url='https://matic-mumbai.chainstacklabs.com', requester=requester) contractStore = create_contract_store(ethClient=ethClient, rinkebyEthClient=rinkebyEthClient, mumbaiEthClient=mumbaiEthClient) infuraIpfsAuth = BasicAuthentication( username=os.environ['INFURA_IPFS_PROJECT_ID'], password=os.environ['INFURA_IPFS_PROJECT_SECRET']) infuraIpfsRequester = Requester( headers={'authorization': f'Basic {infuraIpfsAuth.to_string()}'}) ipfsManager = IpfsManager(requester=infuraIpfsRequester) imageManager = ImageManager(requester=requester, s3Manager=s3Manager, ipfsManager=ipfsManager) manager = MdtpManager(requester=requester, retriever=retriever, saver=saver, s3Manager=s3Manager, contractStore=contractStore, workQueue=workQueue, imageManager=imageManager, ipfsManager=ipfsManager) processor = MdtpMessageProcessor(manager=manager) slackClient = SlackClient(webhookUrl=os.environ['SLACK_WEBHOOK_URL'], requester=requester, defaultSender='worker', defaultChannel='mdtp-notifications') messageQueueProcessor = MessageQueueProcessor(queue=workQueue, messageProcessor=processor, slackClient=slackClient) await database.connect() await s3Manager.connect() await workQueue.connect() await messageQueueProcessor.run() await requester.close_connections() await workQueue.disconnect() await s3Manager.disconnect() await database.disconnect()
workQueue = SqsMessageQueue( region='eu-west-1', accessKeyId=os.environ['AWS_KEY'], accessKeySecret=os.environ['AWS_SECRET'], queueUrl='https://sqs.eu-west-1.amazonaws.com/097520841056/notd-work-queue' ) tokenQueue = SqsMessageQueue( region='eu-west-1', accessKeyId=os.environ['AWS_KEY'], accessKeySecret=os.environ['AWS_SECRET'], queueUrl='https://sqs.eu-west-1.amazonaws.com/097520841056/notd-token-queue' ) awsRequester = AwsRequester(accessKeyId=os.environ['AWS_KEY'], accessKeySecret=os.environ['AWS_SECRET']) ethClient = RestEthClient( url= 'https://nd-foldvvlb25awde7kbqfvpgvrrm.ethereum.managedblockchain.eu-west-1.amazonaws.com', requester=awsRequester) blockProcessor = BlockProcessor(ethClient=ethClient) requester = Requester() tokenMetadataProcessor = TokenMetadataProcessor( requester=requester, ethClient=ethClient, s3manager=s3manager, bucketName=os.environ['S3_BUCKET']) openseaApiKey = os.environ['OPENSEA_API_KEY'] collectionProcessor = CollectionProcessor(requester=requester, ethClient=ethClient, openseaApiKey=openseaApiKey, s3manager=s3manager, bucketName=os.environ['S3_BUCKET']) tokenOwnershipProcessor = TokenOwnershipProcessor(retriever=retriever)
async def reprocess_bad_blocks(startBlockNumber: int, endBlockNumber: int, batchSize: int): databaseConnectionString = Database.create_psql_connection_string( username=os.environ["DB_USERNAME"], password=os.environ["DB_PASSWORD"], host=os.environ["DB_HOST"], port=os.environ["DB_PORT"], name=os.environ["DB_NAME"]) database = Database(connectionString=databaseConnectionString) saver = Saver(database=database) retriever = Retriever(database=database) workQueue = SqsMessageQueue( region='eu-west-1', accessKeyId=os.environ['AWS_KEY'], accessKeySecret=os.environ['AWS_SECRET'], queueUrl= 'https://sqs.eu-west-1.amazonaws.com/097520841056/notd-work-queue') tokenQueue = SqsMessageQueue( region='eu-west-1', accessKeyId=os.environ['AWS_KEY'], accessKeySecret=os.environ['AWS_SECRET'], queueUrl= 'https://sqs.eu-west-1.amazonaws.com/097520841056/notd-token-queue') requester = Requester() slackClient = SlackClient(webhookUrl=os.environ['SLACK_WEBHOOK_URL'], requester=requester, defaultSender='worker', defaultChannel='notd-notifications') awsRequester = AwsRequester(accessKeyId=os.environ['AWS_KEY'], accessKeySecret=os.environ['AWS_SECRET']) ethClient = RestEthClient( url= 'https://nd-foldvvlb25awde7kbqfvpgvrrm.ethereum.managedblockchain.eu-west-1.amazonaws.com', requester=awsRequester) blockProcessor = BlockProcessor(ethClient=ethClient) # NOTE(krishan711): use tokenqueue so its lower prioritized work notdManager = NotdManager(blockProcessor=None, saver=saver, retriever=retriever, workQueue=tokenQueue, tokenManager=None, requester=requester, revueApiKey=None) await database.connect() await workQueue.connect() await tokenQueue.connect() await slackClient.post( text= f'reprocess_bad_blocks → 🚧 started: {startBlockNumber}-{endBlockNumber}' ) try: currentBlockNumber = startBlockNumber while currentBlockNumber < endBlockNumber: start = currentBlockNumber end = min(start + batchSize, endBlockNumber) logging.info(f'Working on {start}-{end}') blockNumbers = set(range(start, end)) processedBlocksQuey = (sqlalchemy.select( BlocksTable.c.blockNumber).where( BlocksTable.c.blockNumber >= start).where( BlocksTable.c.blockNumber < end)) results = await database.execute(query=processedBlocksQuey) processedBlocks = {blockNumber for (blockNumber, ) in results} logging.info(f'Ignoring {len(processedBlocks)} processedBlocks') blockNumbers = list(blockNumbers - processedBlocks) # blockUncleCounts = [] # for chunk in list_util.generate_chunks(lst=blockNumbers, chunkSize=10): # blockUncleCounts += await asyncio.gather(*[blockProcessor.ethClient.get_block_uncle_count(blockNumber=blockNumber) for blockNumber in chunk]) # blocksWithUncles = {blockNumber for (blockNumber, uncleCount) in zip(blockNumbers, blockUncleCounts) if uncleCount > 0} blocksWithUncles = set() logging.info(f'Found {len(blocksWithUncles)} blocks with uncles') blocksWithDuplicatesQuery = (sqlalchemy.select( TokenTransfersTable.c.blockNumber, sqlalchemy.func.count( sqlalchemy.func.distinct(TokenTransfersTable.c.blockHash)) ).where(TokenTransfersTable.c.blockNumber >= start).where( TokenTransfersTable.c.blockNumber < end).group_by( TokenTransfersTable.c.blockNumber)) results = await database.execute(query=blocksWithDuplicatesQuery) blocksWithDuplicates = { blockNumber for (blockNumber, blockHashCount) in results if blockHashCount > 1 } logging.info( f'Found {len(blocksWithDuplicates)} blocks with multiple blockHashes' ) badBlockTransactionsQuery = (sqlalchemy.select( TokenTransfersTable.c.transactionHash).where( TokenTransfersTable.c.blockNumber.in_( blocksWithDuplicates))) results = await database.execute(query=badBlockTransactionsQuery) badBlockTransactions = { transactionHash for (transactionHash, ) in results } logging.info( f'Found {len(badBlockTransactions)} transactions in bad blocks' ) badBlockTransactionActualBlocks = set() for chunk in list_util.generate_chunks( lst=list(badBlockTransactions), chunkSize=10): transactionReceipts = await asyncio.gather(*[ blockProcessor.get_transaction_receipt( transactionHash=transactionHash) for transactionHash in chunk ]) badBlockTransactionActualBlocks.update({ transactionReceipt['blockNumber'] for transactionReceipt in transactionReceipts if transactionReceipt is not None }) badBlockTransactionBlocksQuery = (sqlalchemy.select( sqlalchemy.func.distinct( TokenTransfersTable.c.blockNumber)).where( TokenTransfersTable.c.transactionHash.in_( badBlockTransactions))) results = await database.execute( query=badBlockTransactionBlocksQuery) badBlockTransactionBlocks = { blockNumber for (blockNumber, ) in results } allBadBlocks = blocksWithUncles.union( badBlockTransactionActualBlocks).union( blocksWithDuplicates).union(badBlockTransactionBlocks) logging.info(f'Found {len(allBadBlocks)} blocks to reprocess') await notdManager.process_blocks_deferred(blockNumbers=allBadBlocks ) insertQuery = BlocksTable.insert().from_select( [ BlocksTable.c.createdDate.key, BlocksTable.c.updatedDate.key, BlocksTable.c.blockNumber.key, BlocksTable.c.blockHash.key, BlocksTable.c.blockDate.key ], sqlalchemy.select( sqlalchemy.func.min(TokenTransfersTable.c.blockDate) + datetime.timedelta(minutes=15), sqlalchemy.func.min(TokenTransfersTable.c.blockDate) + datetime.timedelta(minutes=15), TokenTransfersTable.c.blockNumber, TokenTransfersTable.c.blockHash, sqlalchemy.func.min( TokenTransfersTable.c.blockDate)).where( TokenTransfersTable.c.blockNumber.in_( set(blockNumbers) - allBadBlocks)). where(TokenTransfersTable.c.blockNumber >= start).where( TokenTransfersTable.c.blockNumber < end).group_by( TokenTransfersTable.c.blockNumber, TokenTransfersTable.c.blockHash)) async with database.create_transaction() as connection: await database.execute(connection=connection, query=insertQuery) currentBlockNumber = end await slackClient.post( text= f'reprocess_bad_blocks → ✅ completed : {startBlockNumber}-{endBlockNumber}' ) except Exception as exception: await slackClient.post( text= f'reprocess_bad_blocks → � error: {startBlockNumber}-{endBlockNumber}\n```{str(exception)}```' ) raise exception finally: await database.disconnect() await workQueue.disconnect() await tokenQueue.disconnect() await requester.close_connections() await awsRequester.close_connections()
async def main(): s3Client = boto3.client(service_name='s3', region_name='eu-west-1', aws_access_key_id=os.environ['AWS_KEY'], aws_secret_access_key=os.environ['AWS_SECRET']) s3manager = S3Manager(s3Client=s3Client) requester = Requester() ethClient = RestEthClient( url=f'https://mainnet.infura.io/v3/{os.environ["INFURA_PROJECT_ID"]}', requester=requester) requester = Requester() openseaApiKey = os.environ['OPENSEA_API_KEY'] collectionProcessor = CollectionProcessor( requester=requester, ethClient=ethClient, s3manager=s3manager, openseaApiKey=openseaApiKey, bucketName=os.environ['S3_BUCKET']) #HAs No contractURI result = await collectionProcessor.retrieve_collection( '0xE3f92992BB4F0f0D173623A52b2922d65172601d') expected = RetrievedCollection( address='0xE3f92992BB4F0f0D173623A52b2922d65172601d', name='Knights of Degen', symbol='KNIGHTS', description= '8,888 NFT Degens who love sports, betting, alpha and nfts ⚔️', imageUrl= 'https://lh3.googleusercontent.com/yxdb_995UrIS6W9YIHMfMCRsdYRqcITlBvO5w7OoUx35rkClUeq9rPCvTMXdtw_zION07O_qRZSuNfZu6R6o8bI_KmbrfThhFtA4SBc=s120', twitterUsername='******', instagramUsername=None, wikiUrl=None, openseaSlug='knights-of-degen-official', url='https://www.knightsofdegen.io/', discordUrl='https://discord.gg/knightsofdegen', bannerImageUrl= 'https://lh3.googleusercontent.com/328JHSQ2nPpzfTxpZjV5xiiBp6R17GgUdjHJ7BF0mU-SH0Ou1LiiYmcET0WeHt26LD4tVXenIvYZD7VC8jV__bqRCiA_CzmWfwg7=s2500', doesSupportErc721=True, doesSupportErc1155=False) assert (result == expected) result = await collectionProcessor.retrieve_collection( '0xd153f0014db6d1F339c6340d2C9F59214355D9d7') expected = RetrievedCollection( address='0xd153f0014db6d1F339c6340d2C9F59214355D9d7', name='Crypto Hobos', symbol='CryptoHobos', description= 'Crypto Hobos Pet Partners: https://opensea.io/collection/crypto-hobos-pet-partners\n\nThe first generative NFT project to be painted by hand, Crypto Hobos fuses the fine art and profile picture genres of the NFT space.\nArtist Valiahmed Popov has destroyed the source paintings of the over 200 traits that comprise the project, and so the 8000 original Crypto Hobos now exist solely on the Ethereum blockchain in the form of ERC-721 tokens.', imageUrl= 'https://lh3.googleusercontent.com/VLkOkaJ9QuVwHahQqHUrI5ZDVEqNBLCK_xEnMv4rBZ7kciwtlj_klhcwmvi5mM77hn5uSS10uVZH_9uNjkwpshppFhmNFc3a-r3YMuI=s120', twitterUsername='******', instagramUsername='******', wikiUrl=None, openseaSlug='crypto-hobos', url='https://cryptohobos.io/', discordUrl='https://discord.gg/uFpbRYxMeA', bannerImageUrl= 'https://lh3.googleusercontent.com/kiqCwFkf5RWIl_jiJJ5hiElKTV4UYXlcATldxYZ4zr0E2XHiaDOwixPGLs-Led2DFBgQx26dd3AmhON914jllitPE0zZu2dYWF4w=s2500', doesSupportErc721=True, doesSupportErc1155=False) assert (result == expected) #Has ContractURI result = await collectionProcessor.retrieve_collection( '0x3E3bF91740a8363D9433c8d3535B9b3C9E55f669') expected = RetrievedCollection( address='0x3E3bF91740a8363D9433c8d3535B9b3C9E55f669', name='Civit Illustrations', symbol='CIV20', description='Series of digital illustrations and animations', imageUrl='QmXKBRxpTFdRsqLDRUxXNoTJRChwcEQAqQVF8LXKaXmMTf', twitterUsername='******', instagramUsername=None, wikiUrl=None, openseaSlug='civit-illustrations', url= 'https://app.rarible.com/collection/0x3e3bf91740a8363d9433c8d3535b9b3c9e55f669', discordUrl='https://discord.gg/CIVIT', bannerImageUrl= 'https://lh3.googleusercontent.com/zJOeUEKzl2Rs2JiGOxlZQR5WUru-6I-a8n_sp22USaBVxt0LxeYTe-xSfQhjIK8N6u1SyRaEfRWnyp9j-nDpPfeJWxNgSeDwziAI6Q=s2500', doesSupportErc721=True, doesSupportErc1155=False) assert (result == expected) result = await collectionProcessor.retrieve_collection( '0xDb68Df0e86Bc7C6176E6a2255a5365f51113BCe8') expected = RetrievedCollection( address='0xDb68Df0e86Bc7C6176E6a2255a5365f51113BCe8', name='Rope Makers United', symbol='RMU', description='Rope Makers United Storefront', imageUrl='https://rope.lol/images/RopeLogo3D.gif', twitterUsername='******', instagramUsername=None, wikiUrl=None, openseaSlug='rope-makers-united', url='https://rope.lol', discordUrl='https://discord.gg/kWE5G2', bannerImageUrl=None, doesSupportErc721=False, doesSupportErc1155=True) assert (result == expected) # Has no Name or Symbol result = await collectionProcessor.retrieve_collection( '0x12F01AF7FBEAFB088E1d3384BFf67390f41E8404') expected = RetrievedCollection( address='0x12F01AF7FBEAFB088E1d3384BFf67390f41E8404', name='FVCK_BAEIGE//', symbol=None, description='Collaborative contract between Baeige and Fvckrender', imageUrl= 'https://lh3.googleusercontent.com/BJECOBeDJqpaVLWgxza8DYaP9SQGq6h7kLFsOUAlTk3G7naycl4GsjjALsnCPayhHTlctEkvChvpxhGWfDh0hiH2-xd9eUU_yBqmYQ=s120', twitterUsername=None, instagramUsername=None, wikiUrl=None, openseaSlug='unidentified-contract-b46angemew', url=None, discordUrl=None, bannerImageUrl= 'https://lh3.googleusercontent.com/BJECOBeDJqpaVLWgxza8DYaP9SQGq6h7kLFsOUAlTk3G7naycl4GsjjALsnCPayhHTlctEkvChvpxhGWfDh0hiH2-xd9eUU_yBqmYQ=s2500', doesSupportErc721=False, doesSupportErc1155=True) assert (result == expected) result = await collectionProcessor.retrieve_collection( '0x236E7Af5FcAb94770E621c97a1E58b4d0143E95B') expected = RetrievedCollection( address='0x236E7Af5FcAb94770E621c97a1E58b4d0143E95B', name="Ethernity's Master Collection", symbol=None, description= "Ethernity's Master Collection of Exclusive NFTs on Opensea.", imageUrl= 'https://lh3.googleusercontent.com/GId53RudYB4l7e6Irj3_5JGyC3bwybdotwXAEgGaq4KCqjtfQChbolTQSJRYzb1bYrHM_G9xH4Il9vh_CM3ZPtMTwSgtkU5Wu7RVVQ=s120', twitterUsername='******', instagramUsername=None, wikiUrl=None, openseaSlug='ethernity-master', url='http://ethernity.io', discordUrl='https://discord.gg/EthernityChain', bannerImageUrl= 'https://lh3.googleusercontent.com/xWwplVCKh2mIkbsHObxlVWPkW-kT0vlptGihpjaqyU4zTvAD90BwkcnTe25sPQuNlreb3cfT_LgRlYiuqLVJD0YYYWipq1s42A0T=s2500', doesSupportErc721=False, doesSupportErc1155=True) assert (result == expected) #dynamic contractURI result = await collectionProcessor.retrieve_collection( '0x700CE4AB68aD109224Be3aC85f5A99213bf04f67') expected = RetrievedCollection( address='0x700CE4AB68aD109224Be3aC85f5A99213bf04f67', name='Microdoses', symbol='DOSES', description= 'Smaller graphics and topical comics from Killer Acid, in small editions. ', imageUrl='ipfs://ipfs/QmQ52XALRGEfRKCjDryeqXkq5nnSnDuRaiGaLwhs3R77Dz', twitterUsername=None, instagramUsername=None, wikiUrl=None, openseaSlug='microdoses', url= 'https://app.rarible.com/collection/0x700ce4ab68ad109224be3ac85f5a99213bf04f67', discordUrl=None, bannerImageUrl=None, doesSupportErc721=False, doesSupportErc1155=True) result = await collectionProcessor.retrieve_collection( '0x48531836e57bc28d6fee33840f43826b889aa2fc') expected = RetrievedCollection( address='0x48531836e57bc28d6fee33840f43826b889aa2fc', name='Super Crypto Man', symbol='PIPPI', description= 'New generation of Japanese sticker culture × NFT.Making 3D Collective NFT with Cryptoworld as a theme. ', imageUrl='ipfs://ipfs/QmUvzee6ZvY41S5W842fm8wiFr8ueqQSDimfLQhHyp76Tk', twitterUsername='******', instagramUsername=None, wikiUrl=None, openseaSlug='super-crypto-man', url= 'https://app.rarible.com/collection/0x48531836e57bc28d6fee33840f43826b889aa2fc', discordUrl=None, bannerImageUrl= 'https://lh3.googleusercontent.com/TvePJaCzrlt4m-kNSiD0HUkryq5wTYlU0vkugYC9F0CBhThEO2PGCN0m6BRawoNDF2RWNn3E0AixFsSefikyr-5tmWIZkLHoPmxQ-w=s2500', doesSupportErc721=False, doesSupportErc1155=True) result = await collectionProcessor.retrieve_collection( '0xDD97c0b7ED3DC93d09A681dE0E7228b5dfEAE463') expected = RetrievedCollection( address='0xDD97c0b7ED3DC93d09A681dE0E7228b5dfEAE463', name='Beanterra', symbol='BEANEL', description= 'Beanels of Beanterra - Diverse and lovable creatures that roam the realm filled with lush landscapes, snow-capped mountains, and deep blue oceans.', imageUrl= 'https://lh3.googleusercontent.com/ZT-Zq2CWSG0mBzzeeTCsAdMFVyu3au6sPyN_v3CVYXguuI19EL0JrB9QVgg-3tFMJxvg2FJbtpjM8iz5n77tC_gg4t37y-9UP7dQmmA=s120', twitterUsername=None, instagramUsername=None, wikiUrl=None, openseaSlug='beanterra', url='https://beanterra.io/', discordUrl='https://discord.gg/beanterra', bannerImageUrl= 'https://lh3.googleusercontent.com/tBVqF5do5ZpKVqqM6qgcyU44u_Utin0JAH_ygwaqVXMDq3KtsfxG10xXtYliGz6rhjEfngx4eb8LA8IcHn7U2oH_5WlRt9JIUZea7Oc=s2500', doesSupportErc721=True, doesSupportErc1155=False)