Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 | 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 24x 24x 24x 24x 24x 51x 51x 51x 51x 51x 51x 51x 51x 2x 2x 49x 49x 49x 49x 49x 49x 49x 49x 49x 49x 49x 49x 49x 49x 49x 49x 49x 49x 49x 49x 49x 49x 387x 387x 45x 45x 45x 49x 49x 2x 2x 47x 49x 49x 49x | import erc721abi from './abi/erc721.json';
import fetch from "node-fetch";
import { promises as fs } from 'fs';
import { Injectable } from '@nestjs/common';
import { HttpService } from '@nestjs/axios';
import { AbiCoder, JsonRpcProvider, Transaction, TransactionDescription, TransactionReceipt, ethers } from 'ethers';
import { hexToNumberString } from 'web3-utils';
import dotenv from 'dotenv';
dotenv.config();
import { config } from './config';
import { BaseService, TweetRequest } from './base.service';
import { createLogger } from './logging.utils';
const logger = createLogger('erc721sales.service')
// This can be an array if you want to filter by multiple topics
// 'Transfer' topic
const topics = '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef';
@Injectable()
export class Erc721SalesService extends BaseService {
provider = this.getWeb3Provider();
currentBlock:number = -1
constructor(
protected readonly http: HttpService,
) {
super(http)
Iif (!global.doNotStartAutomatically) {
this.startProvider()
}
//this.test()
}
async test() {
const tokenContract = new ethers.Contract(this.getContractAddress(), erc721abi, this.provider);
let filter = tokenContract.filters.Transfer();
const events = await tokenContract.queryFilter(filter,
18247007,
18247007)
for (let e of events) {
const t = await this.getTransactionDetails(e)
this.dispatch(t)
}
}
async startProvider() {
this.initDiscordClient();
const CHUNK_SIZE = 10
const tokenContract = new ethers.Contract(this.getContractAddress(), erc721abi, this.provider);
const filter = [topics];
try {
this.currentBlock = parseInt(await fs.readFile(this.getPositionFile(), { encoding: 'utf8' }))
} catch (err) {
}
Iif (isNaN(this.currentBlock) || this.currentBlock <= 0) {
this.currentBlock = await this.getWeb3Provider().getBlockNumber()
await this.updatePosition(this.currentBlock)
}
console.log(`position: ${this.currentBlock}`)
let retryCount = 0
let latestTweetedBlock = 0
let latestTweetedTx = ''
while (true) {
try {
const latestAvailableBlock = await this.provider.getBlockNumber()
Iif (this.currentBlock >= latestAvailableBlock) {
logger.info(`latest block reached (${latestAvailableBlock}), waiting the next available block...`)
await delay(10000)
continue
}
console.log(`checking ${this.currentBlock}`)
const events = await tokenContract.queryFilter(filter, this.currentBlock, this.currentBlock + CHUNK_SIZE)
for (let event of events) {
latestTweetedBlock = event.blockNumber
latestTweetedTx = event.transactionHash
await this.handleEvent(event)
}
this.currentBlock += CHUNK_SIZE
Iif (this.currentBlock > latestAvailableBlock) this.currentBlock = latestAvailableBlock + 1
await this.updatePosition(latestAvailableBlock)
} catch (err) {
console.log(err)
retryCount++
Iif (retryCount > 5) {
console.log(`stop retrying, failing on ${latestTweetedTx}, moving to next block`)
this.currentBlock = latestTweetedBlock + 1
retryCount = 0
}
}
}
}
async handleEvent(event) {
const res = await this.getTransactionDetails(event, false, true)
Iif (!res) return
// Only tweet transfers with value (Ignore w2w transfers)
if (res?.ether || res?.alternateValue) this.dispatch(res);
// If free mint is enabled we can tweet 0 value
else Iif (config.includeFreeMint) this.tweet(res);;
}
async getTransactionDetails(tx: any, ignoreENS:boolean=false, ignoreContracts:boolean=true): Promise<any> {
// uncomment this to test a specific transaction
// if (tx.transactionHash !== '0xcee5c725e2234fd0704e1408cdf7f71d881e67f8bf5d6696a98fdd7c0bcf52f3') return;
let tokenId: string;
let retryCount: number = 0
while (true) {
try {
// Get addresses of seller / buyer from topics
const coder = AbiCoder.defaultAbiCoder()
let from = coder.decode(['address'], tx?.topics[1])[0];
let to = coder.decode(['address'], tx?.topics[2])[0];
// ignore internal transfers to contract, another transfer event will handle this
// transaction afterward (the one that'll go to the buyer wallet)
const code = await this.provider.getCode(to)
// the ignoreContracts flag make the MEV bots like transaction ignored by the twitter
// bot, but not for statistics
if (to !== config.nftx_vault_contract_address && code !== '0x' && ignoreContracts) {
logger.info(`contract detected for ${tx.transactionHash} event index ${tx.index}`)
return
}
// not an erc721 transfer
// Get transaction receipt
const receipt: TransactionReceipt = await this.provider.getTransactionReceipt(tx.transactionHash);
// Get tokenId from topics
tokenId = this.getTokenId(tx, receipt);
Iif (!tokenId) break
// Get transaction hash
const { transactionHash } = tx;
const isMint = BigInt(from) === BigInt(0);
// Get transaction
const transaction = await this.provider.getTransaction(transactionHash);
const block = await this.provider.getBlock(transaction.blockNumber)
const transactionDate = block.date.toISOString()
logger.info(`handling ${transactionHash} token ${tokenId} log ${tx.index} — ${transactionDate} - from ${tx.blockNumber}`)
const { value } = transaction;
let ether = ethers.formatEther(value.toString());
// Get token image
const imageUrl = await this.getImageUri(tokenId)
// If ens is configured, get ens addresses
let ensTo: string;
let ensFrom: string;
if (config.ens && !ignoreENS) {
ensTo = await this.provider.lookupAddress(`${to}`);
ensFrom = await this.provider.lookupAddress(`${from}`);
}
// Set the values for address to & from -- Shorten non ens
const initialFrom = from
const initialTo = to
to = config.ens && !ignoreENS ? (ensTo ? ensTo : this.shortenAddress(to)) : this.shortenAddress(to);
from = (isMint && config.includeFreeMint) ? 'Mint' : config.ens ? (ensFrom ? ensFrom : this.shortenAddress(from)) : this.shortenAddress(from);
// Create response object
const tweetRequest: TweetRequest = {
logIndex: tx.index,
eventType: isMint ? 'mint' : 'sale',
initialFrom,
initialTo,
from,
erc20Token: 'ethereum',
to,
tokenId,
ether: parseFloat(ether),
transactionHash,
transactionDate,
alternateValue: 0,
platform: 'unknown',
};
// If the image was successfully obtained
if (imageUrl) tweetRequest.imageUrl = imageUrl;
// Try to use custom parsers
for (let parser of config.parsers) {
const result = await parser.parseLogs(transaction, receipt.logs, tokenId)
if (result) {
tweetRequest.alternateValue = result
tweetRequest.platform = parser.platform
break
}
}
Iif (this.getForcedPlatform()) {
tweetRequest.platform = this.getForcedPlatform()
}
if (transaction.to === '0x941A6d105802CCCaa06DE58a13a6F49ebDCD481C' && !tweetRequest.alternateValue) {
// nftx swap of "inner token" that weren't bought in the same transaction ignore this
logger.info(`nftx swap detected without ETH buy, ignoring ${tx.transactionHash} event index ${tx.index}`)
return
}
return tweetRequest
} catch (err) {
logger.info(`${tokenId} failed to send, retryCount: ${retryCount}`, err);
retryCount++
Iif (retryCount >= 10) {
logger.info("retried 10 times, giving up")
return null;
}
logger.info(`will retry after a delay ${retryCount}...`)
await new Promise( resolve => setTimeout(resolve, 500*retryCount) )
}
}
}
getTokenId(tx:any, receipt:TransactionReceipt) {
return hexToNumberString(tx?.topics[3])
}
getContractAddress() {
return config.contract_address
}
getForcedPlatform() {
return undefined
}
getPositionFile() {
return 'erc721.position.txt'
}
async getImageUri(tokenId) {
return config.use_forced_remote_image_path ?
config.forced_remote_image_path.replace(new RegExp('<tokenId>', 'g'), tokenId.padStart(4, '0'))
: config.use_local_images
? `${config.local_image_path}${tokenId.padStart(4, '0')}.png`
: await this.getTokenMetadata(tokenId);
}
}
function delay(ms: number) {
return new Promise( resolve => setTimeout(resolve, ms) );
} |